/*
 * Created on 27/apr/2010
 *
 * Copyright 2010 by Andrea Vacondio (andrea.vacondio@gmail.com).
 * 
 * This file is part of the Sejda source code
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.sejda.core.context;

import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.sejda.common.XMLUtils.nullSafeGetBooleanAttribute;
import static org.sejda.common.XMLUtils.nullSafeGetStringAttribute;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.io.IOUtils;
import org.sejda.common.XMLUtils;
import org.sejda.core.Sejda;
import org.sejda.core.notification.strategy.AsyncNotificationStrategy;
import org.sejda.core.notification.strategy.NotificationStrategy;
import org.sejda.core.notification.strategy.SyncNotificationStrategy;
import org.sejda.model.exception.ConfigurationException;
import org.sejda.model.parameter.base.TaskParameters;
import org.sejda.model.task.Task;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Retrieves the configuration from the input xml stream
 * 
 * @author Andrea Vacondio
 * 
 */

final class XmlConfigurationStrategy implements ConfigurationStrategy {

    private static final String ROOT_NODE = "/sejda";
    private static final String VALIDATION_ATTRIBUTENAME = "validation";
    private static final String IGNORE_XML_CONFIG_VALIDATION_ATTRIBUTENAME = "ignore_xml_config";
    private static final String NOTIFICATION_XPATH = "/notification";
    private static final String NOTIFICATION_ASYNC_ATTRIBUTENAME = "async";
    private static final String TASKS_XPATH = "/tasks/task";
    private static final String TASK_PARAM_ATTRIBUTENAME = "parameters";
    private static final String TASK_VALUE_ATTRIBUTENAME = "task";
    private static final String DEFAULT_SEJDA_CONFIG = "sejda.xsd";

    private XPathFactory xpathFactory = XPathFactory.newInstance();
    private Class<? extends NotificationStrategy> notificationStrategy;
    @SuppressWarnings("rawtypes")
    private Map<Class<? extends TaskParameters>, Class<? extends Task>> tasks;
    private boolean validation = false;
    private boolean ignoreXmlConfig = true;

    /**
     * Creates an instance initialized with the given input stream. The stream is not closed.
     * 
     * @param input
     *            stream to the input xml configuration file
     * @throws ConfigurationException
     *             in case of error parsing the input stream
     */
    private XmlConfigurationStrategy(InputStream input) throws ConfigurationException {
        initializeFromInputStream(input);
    }

    private void initializeFromInputStream(InputStream input) throws ConfigurationException {
        try {
            DocumentBuilderFactory factory = XMLUtils.getDocumentBuilderFactory();
            initializeSchemaValidation(factory);

            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setErrorHandler(XMLUtils.ERROR_HANDLER);
            Document document = builder.parse(input);

            notificationStrategy = getNotificationStrategy(document);
            tasks = getTasksMap(document);
            validation = getValidation(document);
            ignoreXmlConfig = getIgnoreXmlConfig(document);
        } catch (IOException | SAXException e) {
            throw new ConfigurationException(e);
        } catch (ParserConfigurationException | XPathExpressionException e) {
            throw new ConfigurationException("Unable to create DocumentBuilder.", e);
        }
    }

    private void initializeSchemaValidation(DocumentBuilderFactory factory) throws SAXException {
        if (Boolean.getBoolean(Sejda.PERFORM_SCHEMA_VALIDATION_PROPERTY_NAME)) {
            SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

            factory.setSchema(schemaFactory.newSchema(new Source[] { new StreamSource(
                    Thread.currentThread().getContextClassLoader().getResourceAsStream(DEFAULT_SEJDA_CONFIG)) }));

            factory.setNamespaceAware(true);
        }
    }

    @Override
    public Class<? extends NotificationStrategy> getNotificationStrategy() {
        return notificationStrategy;
    }

    @Override
    @SuppressWarnings("rawtypes")
    public Map<Class<? extends TaskParameters>, Class<? extends Task>> getTasksMap() {
        return tasks;
    }

    @Override
    public boolean isValidation() {
        return validation;
    }

    @Override
    public boolean isIgnoreXmlConfiguration() {
        return ignoreXmlConfig;
    }

    @SuppressWarnings("rawtypes")
    private Map<Class<? extends TaskParameters>, Class<? extends Task>> getTasksMap(Document document)
            throws ConfigurationException, XPathExpressionException {
        Map<Class<? extends TaskParameters>, Class<? extends Task>> retMap = new HashMap<>();
        NodeList nodes = (NodeList) xpathFactory.newXPath().evaluate(ROOT_NODE + TASKS_XPATH, document,
                XPathConstants.NODESET);
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            Class<? extends TaskParameters> paramClass = getClassFromNode(node, TASK_PARAM_ATTRIBUTENAME,
                    TaskParameters.class);
            Class<? extends Task> taksClass = getClassFromNode(node, TASK_VALUE_ATTRIBUTENAME, Task.class);
            retMap.put(paramClass, taksClass);

        }
        return retMap;
    }

    /**
     * Retrieves the value of the input xpath in the given node, creates a Class object and performs a check to ensure that the input assignableInterface is assignable by the
     * created Class object.
     * 
     * @param <T>
     * 
     * @param node
     * @param attributeName
     * @param assignableInterface
     * @return the retrieved class.
     * @throws ConfigurationException
     */
    private <T> Class<? extends T> getClassFromNode(Node node, String attributeName, Class<T> assignableInterface)
            throws ConfigurationException {
        String attributeValue = nullSafeGetStringAttribute(node, attributeName);
        if (isNotBlank(attributeValue)) {
            Class<?> clazz;
            try {
                clazz = Class.forName(attributeValue.trim());
            } catch (ClassNotFoundException e) {
                throw new ConfigurationException(String.format("Unable to find the configured %s", attributeValue), e);
            }
            if (assignableInterface.isAssignableFrom(clazz)) {
                return clazz.asSubclass(assignableInterface);
            }
            throw new ConfigurationException(
                    String.format("The configured %s is not a subtype of %s", clazz, assignableInterface));
        }
        throw new ConfigurationException(String.format("Missing %s configuration parameter.", attributeName));
    }

    /**
     * Given a document, search for the notification strategy configuration and returns the configured strategy or the default one if nothing is configured.
     * 
     * @param document
     * @return the class extending {@link NotificationStrategy} configured.
     * @throws XPathExpressionException
     */
    private Class<? extends NotificationStrategy> getNotificationStrategy(Document document)
            throws XPathExpressionException {
        Node node = (Node) xpathFactory.newXPath().evaluate(ROOT_NODE + NOTIFICATION_XPATH, document,
                XPathConstants.NODE);
        if (nullSafeGetBooleanAttribute(node, NOTIFICATION_ASYNC_ATTRIBUTENAME)) {
            return AsyncNotificationStrategy.class;
        }
        return SyncNotificationStrategy.class;
    }

    private boolean getValidation(Document document) throws XPathExpressionException {
        Node node = (Node) xpathFactory.newXPath().evaluate(ROOT_NODE, document, XPathConstants.NODE);
        return nullSafeGetBooleanAttribute(node, VALIDATION_ATTRIBUTENAME);
    }

    private boolean getIgnoreXmlConfig(Document document) throws XPathExpressionException {
        Node node = (Node) xpathFactory.newXPath().evaluate(ROOT_NODE, document, XPathConstants.NODE);
        return nullSafeGetBooleanAttribute(node, IGNORE_XML_CONFIG_VALIDATION_ATTRIBUTENAME, true);
    }

    /**
     * static factory method.
     * 
     * @param provider
     *            provider for the configuration stream.
     * @return the new instance.
     * @throws ConfigurationException
     */
    static XmlConfigurationStrategy newInstance(ConfigurationStreamProvider provider) throws ConfigurationException {
        InputStream stream = null;
        try {
            stream = provider.getConfigurationStream();
            return new XmlConfigurationStrategy(stream);
        } finally {
            IOUtils.closeQuietly(stream);
        }
    }
}
