/**
 *  Copyright 2005-2015 Red Hat, Inc.
 *
 *  Red Hat licenses this file to you under the Apache License, version
 *  2.0 (the "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License 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 io.fabric8.forge.camel.commands.project;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import io.fabric8.forge.addon.utils.CamelProjectHelper;
import io.fabric8.forge.addon.utils.LineNumberHelper;
import io.fabric8.forge.camel.commands.project.helper.PoorMansLogger;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.util.IntrospectionSupport;
import org.apache.camel.util.ObjectHelper;
import org.jboss.forge.addon.dependencies.Dependency;
import org.jboss.forge.addon.projects.Project;
import org.jboss.forge.addon.projects.ProjectFactory;
import org.jboss.forge.addon.projects.facets.ResourcesFacet;
import org.jboss.forge.addon.projects.facets.WebResourcesFacet;
import org.jboss.forge.addon.resource.FileResource;
import org.jboss.forge.addon.ui.context.UIBuilder;
import org.jboss.forge.addon.ui.context.UIContext;
import org.jboss.forge.addon.ui.context.UIExecutionContext;
import org.jboss.forge.addon.ui.context.UINavigationContext;
import org.jboss.forge.addon.ui.input.InputComponent;
import org.jboss.forge.addon.ui.metadata.UICommandMetadata;
import org.jboss.forge.addon.ui.result.NavigationResult;
import org.jboss.forge.addon.ui.result.Result;
import org.jboss.forge.addon.ui.result.Results;
import org.jboss.forge.addon.ui.util.Categories;
import org.jboss.forge.addon.ui.util.Metadata;
import org.jboss.forge.addon.ui.wizard.UIWizardStep;
import org.jboss.forge.roaster.model.util.Strings;

import static io.fabric8.forge.camel.commands.project.helper.CamelCatalogHelper.getModelJavaType;
import static io.fabric8.forge.camel.commands.project.helper.CamelCatalogHelper.isModelDefaultValue;
import static io.fabric8.forge.camel.commands.project.helper.CamelCatalogHelper.isModelExpressionKind;
import static io.fabric8.forge.camel.commands.project.helper.CamelXmlHelper.dumpModelAsXml;

public abstract class ConfigureEipPropertiesStep extends AbstractCamelProjectCommand implements UIWizardStep {

    private static final PoorMansLogger LOG = new PoorMansLogger(false);

    private final CamelCatalog camelCatalog;

    private final String eipName;
    private final String group;
    private final List<InputComponent> allInputs;
    private final List<InputComponent> inputs;
    private final boolean last;
    private final int index;
    private final int total;

    public ConfigureEipPropertiesStep(ProjectFactory projectFactory,
                                      CamelCatalog camelCatalog,
                                      String eipName, String group,
                                      List<InputComponent> allInputs,
                                      List<InputComponent> inputs,
                                      boolean last, int index, int total) {
        this.projectFactory = projectFactory;
        this.camelCatalog = camelCatalog;
        this.eipName = eipName;
        this.group = group;
        this.allInputs = allInputs;
        this.inputs = inputs;
        this.last = last;
        // we want to 1-based
        this.index = index + 1;
        this.total = total;
    }

    public String getGroup() {
        return group;
    }

    public int getIndex() {
        return index;
    }

    public int getTotal() {
        return total;
    }

    public String getEipName() {
        return eipName;
    }

    @Override
    public UICommandMetadata getMetadata(UIContext context) {
        return Metadata.forCommand(ConfigureEipPropertiesStep.class).name(
                "Camel: EIP options").category(Categories.create(CATEGORY))
                .description(String.format("Configure %s options (%s of %s)", group, index, total));
    }

    @Override
    @SuppressWarnings("unchecked")
    public void initializeUI(UIBuilder builder) throws Exception {
        if (inputs != null) {
            for (InputComponent input : inputs) {
                builder.add(input);
            }
        }
    }

    @Override
    public NavigationResult next(UINavigationContext context) throws Exception {
        return null;
    }

    @Override
    public Result execute(UIExecutionContext context) throws Exception {
        Map<Object, Object> attributeMap = context.getUIContext().getAttributeMap();

        // only execute if we are last
        if (last) {
            String kind = mandatoryAttributeValue(attributeMap, "kind");
            if ("xml".equals(kind)) {
                return executeXml(context, attributeMap);
            } else {
                throw new UnsupportedOperationException("Java not yet supported");
            }
        } else {
            return null;
        }
    }

    protected Result executeXml(UIExecutionContext context, Map<Object, Object> attributeMap) throws Exception {
        String mode = mandatoryAttributeValue(attributeMap, "mode");
        String xml = mandatoryAttributeValue(attributeMap, "xml");
        String pattern = mandatoryAttributeValue(attributeMap, "pattern");

        // line number for the parent node
        String lineNumber = mandatoryAttributeValue(attributeMap, "lineNumber");
        String lineNumberEnd = mandatoryAttributeValue(attributeMap, "lineNumberEnd");

        Project project = getSelectedProject(context);
        ResourcesFacet facet = project.getFacet(ResourcesFacet.class);
        WebResourcesFacet webResourcesFacet = null;
        if (project.hasFacet(WebResourcesFacet.class)) {
            webResourcesFacet = project.getFacet(WebResourcesFacet.class);
        }

        // does the project already have camel?
        Dependency core = CamelProjectHelper.findCamelCoreDependency(project);
        if (core == null) {
            return Results.fail("The project does not include camel-core");
        }

        // collect all the options that was set
        Map<String, Object> options = new HashMap<String, Object>();
        Map<String, String> expressionKeys = new HashMap<String, String>();
        Map<String, String> expressionValues = new HashMap<String, String>();
        for (InputComponent input : allInputs) {
            String key = input.getName();

            // special for expression
            boolean expressionKey = isModelExpressionKind(camelCatalog, eipName, key);
            boolean expressionValue = key.endsWith("_value") || key.endsWith("_extra");

            // only use the value if a value was set (and the value is not the same as the default value)
            if (input.hasValue()) {
                String value = input.getValue().toString();
                if (value != null) {
                    // do not add the value if it match the default value
                    boolean matchDefault = isModelDefaultValue(camelCatalog, eipName, key, value);
                    if (!matchDefault) {
                        if (expressionKey) {
                            expressionKeys.put(key, value);
                        } else if (expressionValue) {
                            expressionValues.put(key, value);
                        } else {
                            options.put(key, value);
                        }
                    }
                }
            } else if (input.isRequired() && input.hasDefaultValue()) {
                // if its required then we need to grab the value
                String value = input.getValue().toString();
                if (value != null) {
                    if (expressionKey) {
                        expressionKeys.put(key, value);
                    } else if (expressionValue) {
                        expressionValues.put(key, value);
                    } else {
                        options.put(key, value);
                    }
                }
            }
        }

        String javaType = getModelJavaType(camelCatalog, eipName);
        if (javaType == null) {
            return Results.fail("Cannot find javaType for " + eipName);
        }

        FileResource file = facet != null ? facet.getResource(xml) : null;
        if (file == null || !file.exists()) {
            file = webResourcesFacet != null ? webResourcesFacet.getWebResource(xml) : null;
        }
        if (file == null || !file.exists()) {
            return Results.fail("Cannot find XML file " + xml);
        }

        String modelXml = null;
        try {
            ClassLoader cl = CamelCatalog.class.getClassLoader();
            Class clazz = cl.loadClass(javaType);
            Object instance = clazz.newInstance();

            // set properties on the instance
            IntrospectionSupport.setProperties(instance, options);

            for (Map.Entry<String, String> entry : expressionKeys.entrySet()) {
                String name = entry.getKey();
                String language = entry.getValue();
                String text = expressionValues.get(name + "_value");
                String extra = expressionValues.get(name + "_extra");

                // build the model
                String lanJavaType = getModelJavaType(camelCatalog, language);
                if (lanJavaType != null) {
                    Class clazz2 = cl.loadClass(lanJavaType);
                    Object instance2 = clazz2.newInstance();

                    // set the value of the expression
                    if (text != null) {
                        IntrospectionSupport.setProperty(instance2, "expression", text);
                    }
                    if (extra != null) {
                        // the extra values are separated using & as uri parameters style
                        Iterator it = ObjectHelper.createIterator(extra, "&");
                        while (it.hasNext()) {
                            String pair = (String) it.next();
                            String key = ObjectHelper.before(pair, "=");
                            String value = ObjectHelper.after(pair, "=");
                            if (key != null && value != null) {
                                // set the extra option
                                IntrospectionSupport.setProperty(instance2, key, value);
                            }
                        }
                    }
                    // add the expression to the parent
                    IntrospectionSupport.setProperty(instance, name, instance2);
                }
            }

            // we should only include the end tag if the node does not have children
            boolean includeEndTag = true;
            String children = optionalAttributeValue(attributeMap, "nodeChildren");
            if (children != null) {
                includeEndTag = "false".equals(children);
            }
            // calculate indent to use
            int indent = calculateIndent(file, lineNumber);
            LOG.info("Calculated indent " + indent);
            // marshal to xml
            modelXml = dumpModelAsXml(instance, cl, includeEndTag, indent);
            LOG.info("Generated XML model:\n" + modelXml + "\n");
        } catch (Exception e) {
            // ignore
        }

        if (modelXml == null) {
            return Results.fail("Cannot create XML model of the node");
        }

        return addOrEditModelXml(file, pattern, modelXml, xml, lineNumber, lineNumberEnd, mode);
    }

    protected int calculateIndent(FileResource file, String lineNumber) throws Exception {
        List<String> lines = LineNumberHelper.readLines(file.getResourceInputStream());

        int idx = Integer.valueOf(lineNumber);

        int spaces = LineNumberHelper.leadingSpaces(lines, idx - 1);
        int spaces1 = LineNumberHelper.leadingSpaces(lines, idx);
        int spaces2 = LineNumberHelper.leadingSpaces(lines, idx + 1);

        int delta = Math.abs(spaces1 - spaces);
        int delta2 = Math.abs(spaces2 - spaces1);

        int answer = delta2 > 0 ? delta2 : delta;
        // the indent must minimum be 2 spaces
        return Math.max(answer, 2);
    }

    protected Result addOrEditModelXml(FileResource file, String pattern, String modelXml, String xml, String lineNumber, String lineNumberEnd, String mode) throws Exception {
        List<String> lines = LineNumberHelper.readLines(file.getResourceInputStream());
        if ("add".equals(mode)) {
            return addModelXml(pattern, lines, lineNumber, lineNumberEnd, modelXml, file, xml);
        } else {
            return editModelXml(pattern, lines, lineNumber, lineNumberEnd, modelXml, file, xml);
        }
    }

    protected abstract Result addModelXml(String pattern, List<String> lines, String lineNumber, String lineNumberEnd, String modelXml, FileResource file, String xml) throws Exception;

    protected abstract Result editModelXml(String pattern, List<String> lines, String lineNumber, String lineNumberEnd, String modelXml, FileResource file, String xml) throws Exception;

    /**
     * Returns the mandatory String value of the given name
     *
     * @throws IllegalArgumentException if the value is not available in the given attribute map
     */
    public static String mandatoryAttributeValue(Map<Object, Object> attributeMap, String name) {
        Object value = attributeMap.get(name);
        if (value != null) {
            String text = value.toString();
            if (!Strings.isBlank(text)) {
                return text;
            }
        }
        throw new IllegalArgumentException("The attribute value '" + name + "' did not get passed on from the previous wizard page");
    }

    /**
     * Returns the optional String value of the given name
     */
    public static String optionalAttributeValue(Map<Object, Object> attributeMap, String name) {
        Object value = attributeMap.get(name);
        if (value != null) {
            String text = value.toString();
            if (!Strings.isBlank(text)) {
                return text;
            }
        }
        return null;
    }
}