/*
 * Copyright 2013-2017 EMC Corporation. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
package com.emc.ecs.sync.config;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.util.*;

public final class XmlGenerator {
    private static final ConversionService conversionService = new DefaultConversionService();

    public static String generateXml(boolean addComments, boolean advancedOptions, String sourcePrefix, String targetPrefix, String... filterNames)
            throws InstantiationException, IllegalAccessException, ParserConfigurationException, TransformerException {
        // find config wrappers for given plugins
        ConfigWrapper<?> sourceWrapper = ConfigUtil.storageConfigWrapperFor(sourcePrefix);
        ConfigWrapper<?> targetWrapper = ConfigUtil.storageConfigWrapperFor(targetPrefix);
        List<ConfigWrapper<?>> filterWrappers = new ArrayList<>();
        if (filterNames != null) {
            for (String filterName : filterNames) {
                filterWrappers.add(ConfigUtil.filterConfigWrapperFor(filterName));
            }
        }

        DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
        builderFactory.setNamespaceAware(true);
        DocumentBuilder builder = builderFactory.newDocumentBuilder();

        Document document = builder.newDocument();
        Element rootElement = document.createElement("syncConfig");
        rootElement.setAttribute("xmlns", ConfigUtil.XML_NAMESPACE);
        document.appendChild(rootElement);

        // sync options
        rootElement.appendChild(createDefaultElement(document, ConfigUtil.wrapperFor(SyncOptions.class), "options", addComments, advancedOptions));
        rootElement.appendChild(document.createTextNode("\n\n    ")); // blank line

        // source
        Element sourceElement = document.createElement("source");
        rootElement.appendChild(sourceElement);
        if (addComments) {
            sourceElement.appendChild(document.createComment(" " + sourceWrapper.getDocumentation() + " "));
            sourceElement.appendChild(document.createTextNode("\n        ")); // blank line
        }
        sourceElement.appendChild(createDefaultElement(document, sourceWrapper, null, addComments, advancedOptions));
        rootElement.appendChild(document.createTextNode("\n\n    ")); // blank line

        // filters
        if (filterNames != null && filterNames.length > 0) {
            Element filtersElement = document.createElement("filters");
            rootElement.appendChild(filtersElement);
            for (ConfigWrapper<?> filterWrapper : filterWrappers) {
                if (addComments) {
                    filtersElement.appendChild(document.createComment(" " + filterWrapper.getDocumentation() + " "));
                    filtersElement.appendChild(document.createTextNode("\n        ")); // blank line
                }
                filtersElement.appendChild(createDefaultElement(document, filterWrapper, null, addComments, advancedOptions));
            }
            rootElement.appendChild(document.createTextNode("\n\n    ")); // blank line
        }

        // target
        Element targetElement = document.createElement("target");
        rootElement.appendChild(targetElement);
        if (addComments) {
            targetElement.appendChild(document.createComment(" " + targetWrapper.getDocumentation() + " "));
            targetElement.appendChild(document.createTextNode("\n        ")); // blank line
        }
        targetElement.appendChild(createDefaultElement(document, targetWrapper, null, addComments, advancedOptions));

        // write XML
        StringWriter writer = new StringWriter();
        TransformerFactory factory = TransformerFactory.newInstance();
        factory.setAttribute("indent-number", 4);
        Transformer transformer = factory.newTransformer();
        transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
        transformer.transform(new DOMSource(document), new StreamResult(writer));

        return writer.toString();
    }

    private static <C> Element createDefaultElement(Document document, ConfigWrapper<C> configWrapper, String name, boolean addComments, boolean advancedOptions)
            throws IllegalAccessException, InstantiationException {

        // create main element
        if (name == null) name = initialLowerCase(configWrapper.getTargetClass().getSimpleName());
        Element mainElement = document.createElement(name);

        // create object instance for defaults
        C object = configWrapper.getTargetClass().newInstance();
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);

        List<ConfigPropertyWrapper> propertyWrappers = new ArrayList<>();
        for (String property : configWrapper.propertyNames()) {
            propertyWrappers.add(configWrapper.getPropertyWrapper(property));
        }
        Collections.sort(propertyWrappers, new Comparator<ConfigPropertyWrapper>() {
            @Override
            public int compare(ConfigPropertyWrapper o1, ConfigPropertyWrapper o2) {
                return o1.getOrderIndex() - o2.getOrderIndex();
            }
        });

        for (ConfigPropertyWrapper propertyWrapper : propertyWrappers) {
            if (propertyWrapper.isAdvanced() && !advancedOptions) continue;

            Object defaultValue = beanWrapper.getPropertyValue(propertyWrapper.getName());

            // create XMl comment[s]
            if (addComments) {
                Comment comment = document.createComment(" " + propertyWrapper.getDescription() + " ");
                mainElement.appendChild(comment);
                String specString = propertyWrapper.getDescriptor().getPropertyType().getSimpleName();
                if (propertyWrapper.isRequired()) specString += " - Required";
                if (propertyWrapper.getDescriptor().getPropertyType().isArray())
                    specString += " - Repeat for multiple values";
                if (propertyWrapper.getValueList() != null && propertyWrapper.getValueList().length > 0)
                    specString += " - Values: " + Arrays.toString(propertyWrapper.getValueList());
                else if (propertyWrapper.getDescriptor().getPropertyType().isEnum())
                    specString += " - Values: " + Arrays.toString(propertyWrapper.getDescriptor().getPropertyType().getEnumConstants());
                if (defaultValue != null) specString += " - Default: " + defaultValue;
                comment = document.createComment(" " + specString + " ");
                mainElement.appendChild(comment);
            }

            // create XMl element
            Element propElement = document.createElement(propertyWrapper.getName());

            // set default value
            String defaultValueStr = propertyWrapper.getValueHint();
            if (defaultValue != null) defaultValueStr = conversionService.convert(defaultValue, String.class);
            if (defaultValueStr == null || defaultValueStr.length() == 0) defaultValueStr = propertyWrapper.getName();
            propElement.appendChild(document.createTextNode(defaultValueStr));

            // add to parent element
            mainElement.appendChild(propElement);
        }

        return mainElement;
    }

    private static String initialLowerCase(String value) {
        if (value.length() == 0) return value;
        String result = "" + Character.toLowerCase(value.charAt(0));
        if (value.length() > 1) return result + value.substring(1);
        return result;
    }

    private XmlGenerator() {
    }
}