/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.properties;

import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.QName;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.util.DateUtils;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.oxf.xml.XPathUtils;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.orbeon.saxon.om.Name10Checker;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;

/**
 * Represent property sets grouped as global and per-processor properties.
 */
public class PropertyStore {

    private final PropertySet globalPropertySet = new PropertySet();
    private final Map<QName, PropertySet> processorPropertySets = new HashMap<QName, PropertySet>();

    public static final Map<QName, Converter> SUPPORTED_TYPES = new HashMap<QName, Converter>();

    static {
        SUPPORTED_TYPES.put(XMLConstants.XS_STRING_QNAME, new StringConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_INTEGER_QNAME, new IntegerConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_BOOLEAN_QNAME, new BooleanConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_DATE_QNAME, new DateConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_DATETIME_QNAME, new DateConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_QNAME_QNAME, new QNameConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_ANYURI_QNAME, new URIConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_NCNAME_QNAME, new NCNameConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_NMTOKEN_QNAME, new NMTOKENConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_NMTOKENS_QNAME, new NMTOKENSConverter());
        SUPPORTED_TYPES.put(XMLConstants.XS_NONNEGATIVEINTEGER_QNAME, new NonNegativeIntegerConverter());
    }

    /**
     * Convert a property's string value to an object.
     *
     * @param stringValue   string value
     * @param type          type
     * @param element       Element on which the property is defined. Used for QName resolution if needed.
     * @return              object, or null
     */
    public static Object getObjectFromStringValue(final String stringValue, final QName type, final Element element) {
        final Converter converter = SUPPORTED_TYPES.get(type);
        return (converter == null) ? null : converter.convert(stringValue, element);
    }

    /**
     * Construct a new property store.
     *
     * @param propertiesDocument Document containing the properties definitions
     */
    public PropertyStore(final Document propertiesDocument) {

        // NOTE: the use of "attributes" and "attribute" is for special use of the property store by certain processors
        for (final Iterator i = XPathUtils.selectIterator(propertiesDocument, "/properties//property | /attributes//attribute"); i.hasNext();) {
            final Element propertyElement = (Element) i.next();

            // Extract attributes
            final String as = propertyElement.attributeValue("as");
            if (as != null) { // currently schema requires @as
                // Read QName
                final QName typeQName = Dom4jUtils.extractAttributeValueQName(propertyElement, "as");

                if (SUPPORTED_TYPES.get(typeQName) == null)
                    throw new ValidationException("Invalid as attribute: " + typeQName.getQualifiedName(), (LocationData) propertyElement.getData());

                final String processorName = propertyElement.attributeValue("processor-name");
                final String name = propertyElement.attributeValue("name");

                // Value can come from @value attribute or element text
                final String value; {
                    final String valueFromAttribute = propertyElement.attributeValue("value");
                    if (valueFromAttribute == null)
                        value = propertyElement.getText().trim(); // trim as attributes are normalized
                    else
                        value = valueFromAttribute;
                }

                if (processorName != null) {
                    // Processor-specific property
                    final QName processorQName = Dom4jUtils.extractAttributeValueQName(propertyElement, "processor-name");
                    getProcessorPropertySet(processorQName).setProperty(propertyElement, name, typeQName, value);
                } else {
                    // Global property
                    getGlobalPropertySet().setProperty(propertyElement, name, typeQName, value);
                }
            }
        }
    }

    /**
     * Return the global property set for this store.
     *
     * @return PropertySet
     */
    public PropertySet getGlobalPropertySet() {
        return globalPropertySet;
    }

    /**
     * Return the property set for the given processor.
     *
     * @param processorQName processor QName
     * @return PropertySet   PropertySet
     */
    public PropertySet getProcessorPropertySet(final QName processorQName) {
        PropertySet propertySet = processorPropertySets.get(processorQName);
        if (propertySet == null) {
            propertySet = new PropertySet();
            processorPropertySets.put(processorQName, propertySet);
        }
        return propertySet;
    }

    /* All converters */

    private interface Converter {
        public Object convert(final String value, final Element element);
    }

    public static class StringConverter implements Converter {
        public Object convert(final String value, final Element element) {
            return value;
        }
    }

    public static class IntegerConverter implements Converter {
        public Object convert(final String value, final Element element) {
            return new Integer(value);
        }
    }

    public static class BooleanConverter implements Converter {
        public Object convert(final String value, final Element element) {
            return Boolean.valueOf(value);
        }
    }

    public static class DateConverter implements Converter {
        public Object convert(final String value, final Element element) {
            // NOTE: Not sure if we ever use dates in properties as of 2012-05-29
            return new Date(DateUtils.parseISODateOrDateTime(value));
        }
    }

    public static class QNameConverter implements Converter {
        public Object convert(final String value, final Element element) {
            return Dom4jUtils.extractAttributeValueQName(element, "value");
        }
    }

    public static class URIConverter implements Converter {
        public Object convert(final String value, final Element element) {
            try {
                return new URI(value);
            } catch (URISyntaxException e) {
                throw new ValidationException(e, null);
            }
        }
    }

    public static class NCNameConverter implements Converter {
        public Object convert(final String value, final Element element) {
            if (!Name10Checker.getInstance().isValidNCName(value)) {
                throw new ValidationException("Not an NCName: " + value, null);
            }
            return value;
        }
    }

    public static class NMTOKENConverter implements Converter {
        public Object convert(final String value, final Element element) {
            if (!Name10Checker.getInstance().isValidNmtoken(value)) {
                throw new ValidationException("Not an NMTOKEN: " + value, null);
            }
            return value;
        }
    }

    public static class NMTOKENSConverter implements Converter {
        public Object convert(final String value, final Element element) {
            // Create Set from String
            final Set<String> tokens = new HashSet<String>(Arrays.asList(StringUtils.split(value)));
            // Check validity of individual tokens
            for (String token : tokens) {
                if (!Name10Checker.getInstance().isValidNmtoken(token)) {
                    throw new ValidationException("Not an NMTOKENS: " + value, null);
                }
            }
            return tokens;
        }
    }

    public static class NonNegativeIntegerConverter extends IntegerConverter {
        public Object convert(final String value, final Element element) {
            final Integer ret = (Integer) super.convert(value, element);
            if (ret < 0) {
                throw new ValidationException("Not a non-negative integer: " + value, null);
            }
            return ret;
        }
    }
}
