/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2011 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library 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 any later version.
 *
 * This library 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */
package org.ow2.jonas.tools.maven.shade.transformers;

import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.util.IOUtil;
import org.ow2.jonas.tools.maven.shade.transformers.xml.Input;
import org.ow2.jonas.tools.maven.shade.transformers.xml.Target;
import org.ow2.util.maven.plugin.mergeproperties.core.MergeProperties;
import org.ow2.util.maven.plugin.mergeproperties.core.Preferences;
import org.ow2.util.maven.plugin.mergeproperties.core.Resource;
import org.ow2.util.maven.plugin.mergeproperties.core.Strategies;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;
import java.util.jar.JarOutputStream;

/**
 * This class generate Build.xml ant script
 * @author Jeremy Cazaux
 */
public class BuildResourceTransformer extends AbstractJOnASResourceTransformer {

    /**
     * build.properties master file
     */
    private Resource buildPropertiesMasterResource;

    /**
     * build.properties properties files
     */
    private List<Resource> buildPropertiesResources;

    /**
     * Build.properties template files
     */
    private List<Resource> buildPropertiesTemplateResources;

    /**
     * Output build.properties file
     */
    private String outputBuildPropertiesFile;

    /**
     * build.xml master File
     */
    private Resource buildXmlMasterResource;

    /**
     * build.xml template files
     */
    private List<Resource> buildXmlTemplateResources;

    /**
     * Output build.xml file
     */
    private String outputBuildXmlFile;


    /**
     * Strategies
     */
    private Strategies strategies;

    /**
     * Preferences
     */
    private Preferences preferences;

    /**
     * List of <key,value> for each buildProperties files
     */
    private HashMap properties;

    /**
     * The build.xml master file
     */
    public static final String BUILD_XML_MASTER_FILE = "build-master.xml";

    /**
     * The build.properties master file
     */
    public static final String BUILD_PROPERTIES_MASTER_FILE = "build-master.properties.template";

    /**
     * An antacall service marker is a marker which need to be replaced by antcalls to all JOnAS services
     * A JOnAS service is a target with a target name prefix by the string "set_"
     */
    private final String ANTCALL_SERVICES_MARKER = "antcall_services";

    /**
     * An antcall implementation is a marker which need to be replaced by antcalls to all services which implemented
     * the service where is defined this antcall
     */
    private final String ANTCALL_IMPLEMENTATION_MARKER = "antcall_implementation";

    /**
     * A input marker is a marker which need to be replaced by inputs tags.
     */
    private static final String INPUT_MARKER = "input";

    /**
     * A condition marker is a marker which need to be replaced by conditions tags
     */
    private static final String CONDITION_MARKER = "condition";

    /**
     * Build.xml pattern
     */
    public static final String BUILD_XML_PATTERN = "build-.*[.]xml";

    /**
     * Build.properties pattern
     */
    public static final String BUILD_PROPERTIES_PATTERN = "build-.*[.]properties";

    /**
     * Build.properties.template pattern
     */
    public static final String BUILD_PROPERTIES_TEMPLATE_PATTERN = "build-.*[.]properties.template";

    /**
     * Default constructor
     */
    public BuildResourceTransformer() {
        super();
        this.properties = new HashMap<String, String>();
        this.buildPropertiesTemplateResources = new ArrayList<Resource>();
        this.buildXmlTemplateResources = new ArrayList<Resource>();
        this.buildPropertiesResources = new ArrayList<Resource>();
    }

    /**
     *
     * @param resource The resource
     * @return true if the resource is a build.xml resource
     */
    private boolean isBuildXMLResource(String resource) {
        return matchPattern(resource, BUILD_XML_PATTERN);
    }

    /**
     * @param resource The resource
     * @return true if the resource is a build.properties.template resource
     */
    private boolean isBuildPropertiesTemplateResource(String resource) {
        return matchPattern(resource, BUILD_PROPERTIES_TEMPLATE_PATTERN);
    }

     /**
     * @param resource The resource
     * @return true if the resource is a build.properties resource
     */
    private boolean isBuildPropertiesResource(String resource) {
        return matchPattern(resource, BUILD_PROPERTIES_PATTERN);
    }

    /**
     * return true if the resource is an ant script fragment or an ant properties file
     */
    public boolean canTransformResource(String resource) {
        return isBuildPropertiesResource(resource) || isBuildPropertiesTemplateResource(resource) ||
               isBuildXMLResource(resource);
    }

    /**
     *
     * @param resource  The ressource
     * @param is The InputStream associated to the resource
     * @param relocators The list of relocators
     * @throws IOException
     */
    public void processResource(String resource, InputStream is, List relocators) throws IOException {

        Resource res = new Resource(new ByteArrayInputStream(IOUtil.toByteArray(is)));
        res.setName(resource);

        //master resources
        if (isMasterBuildProperties(res)) {
            this.buildPropertiesMasterResource = res;
        } else if (isMasterBuildXml(res)) {
            this.buildXmlMasterResource = res;

        //template resources
        } else if (isBuildXMLResource(resource)) {
            this.buildXmlTemplateResources.add(res);
        } else if (isBuildPropertiesResource(resource)) {
            updateProperties(res);
            this.buildPropertiesResources.add(res);
        } else if (isBuildPropertiesTemplateResource(resource)) {
            this.buildPropertiesTemplateResources.add(res);
        }
    }

     /**
     * @return true if there is transformed ressource. Otherwise false.
     */
    public boolean hasTransformedResource() {
        return (this.buildPropertiesMasterResource != null || this.buildXmlMasterResource != null);
    }

    /**
     * This method generates Build.xml Ant script
     * @param os
     * @throws IOException
     */
    public void modifyOutputStream(JarOutputStream os) {

        //merge build.xml
       try {
            this.mergeAntTasks();
        } catch (MojoFailureException e) {
            this.logger.info("." + e);
        }

        //merge build.properties
        try {
            MergeProperties mergeProperties = new MergeProperties();
            mergeProperties.setMasterResource(this.buildPropertiesMasterResource);
            mergeProperties.setTemplateResources(this.buildPropertiesTemplateResources);
            mergeProperties.setPropertiesResources(this.buildPropertiesResources);
            mergeProperties.setStrategies(this.strategies);
            mergeProperties.setPreferences(this.preferences);
            mergeProperties.setOutputFile(this.outputBuildPropertiesFile);
            mergeProperties.execute();
        } catch (Exception e) {
            this.logger.error(".", e);
        }
    }

    /**
     * Update properties List
     * @param resource A BuildProperties file
     */
    public void updateProperties(final Resource resource) {
        BufferedReader br = resource.getBufferedReader();

        try {
            StringTokenizer stringTokenizer;
            String line;
            while ((line = br.readLine()) != null) {
                if (line.contains("=")) {
                    stringTokenizer = new StringTokenizer(line, "=");
                } else {
                    stringTokenizer = new StringTokenizer(line, " ");
                }
                if (stringTokenizer.hasMoreElements()) {
                    String key = stringTokenizer.nextElement().toString();
                    if (stringTokenizer.hasMoreElements()) {
                        String value = stringTokenizer.nextElement().toString();
                        if (this.properties.containsKey(key)) {
                            this.properties.put(key, this.properties.get(key) + "," + value);
                        } else {
                            this.properties.put(key, value);
                        }
                    }
                }
            }
        } catch (IOException e) {
            this.logger.info(".", e);
        }
    }

    /**
     *
     * @param resource
     * @return true if the resource is the build.properties master file. Otherwise false.
     */
    private boolean isMasterBuildProperties(Resource resource) {
        return resource.getName().equals(BUILD_PROPERTIES_MASTER_FILE);
    }

    /**
     *
     * @param resource
     * @return true if the resource is the build.xml master file. Otherwise false.
     */
    private boolean isMasterBuildXml(Resource resource) {
        return resource.getName().equals(BUILD_XML_MASTER_FILE);
    }

    /**
     *
     * @param resource
     * @return ture if the resource is a master file. Otherwise false.
     */
    private boolean isMaster(Resource resource) {
        return isMasterBuildProperties(resource) || isMasterBuildXml(resource);
    }

    /**
     * Update children and parent of target
     */
    private void updateTarget(List<Target> targets) {

        for (Target target1: targets) {

            for (Target target2: targets){

                if (target1.getName().startsWith(target2.getName() + "_") && !target1.equals(target2) &&
                        !target1.isMasterTarget() && !target2.isMasterTarget()) {
                    target2.addChild(target1);
                    target1.setParent(target2);
                }
            }
        }
    }

    /**
     * @param targets
     * @param name the name of the target
     * @return the target identified by the name
     */
    private Target getTarget(List<Target> targets, final String name) {
        int i = 0;

        //search for a target
        while (i < targets.size() && !targets.get(i).getName().equals(name)) {
            i++;
        }

        //return the target if found, null otherwise
        if (i < targets.size() && targets.get(i).getName().equals(name)) {
            return targets.get(i);
        } else {
            return null;
        }
    }

    /**
     * Generate ant tasks
     * @throws org.apache.maven.plugin.MojoExecutionException
     * @throws org.apache.maven.plugin.MojoFailureException
     */
    public void mergeAntTasks() throws MojoFailureException {

        BufferedReader br = null;
        InputStreamReader inputStreamReader = null;

        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db = null;
        try {
            db = dbf.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            this.logger.error("." + e);
        }

        List<Resource> resources = new ArrayList<Resource>();
        resources.add(this.buildXmlMasterResource);
        resources.addAll(this.buildXmlTemplateResources);

        List<Input> inputs = new ArrayList<Input>();
        List<Element> conditions = new ArrayList<Element>();
        List<Target> targets = new ArrayList<Target>();

        Document masterDom = null;
        List<Document> templateDomList = new ArrayList<Document>();

        String headerComment = null;

        //searching input, condition and target Elements.
        for (Resource resource: resources) {

            Document dom = null;
            try {
                dom = db.parse(resource.getInputStream());
            } catch (SAXException e) {
                this.logger.info(".", e);
            } catch (IOException e) {
                this.logger.info(".", e);
            }

            if (isMaster(resource)) {
                masterDom = dom;
                //remove header comment node of the master
                headerComment = removeHeaderCommentFromDocument(masterDom);
            } else {
                templateDomList.add(dom);
            }

            Element rootElement = dom.getDocumentElement();

            if (!isMaster(resource)) {
                NodeList inputNodes = rootElement.getElementsByTagName("input");
                for (int i = 0; i < inputNodes.getLength(); i++) {
                    Input input = new Input();
                    input.setElement((Element) inputNodes.item(i));
                    inputs.add(input);
                }
            }

            NodeList conditionNodes = rootElement.getElementsByTagName("condition");
            for (int i = 0; i < conditionNodes.getLength(); i++) {
                conditions.add((Element) conditionNodes.item(i));
            }

            //add template targets with a target name prefix by the string "set_" (a JOnAS service)
            NodeList targetNodes = rootElement.getElementsByTagName("target");
            for (int i = 0; i < targetNodes.getLength(); i++) {
                Target target = new Target();
                target.setElement((Element) targetNodes.item(i));
                target.setMasterTarget(this.buildXmlMasterResource.equals(resource));
                if (target.getElement().getAttribute("name").startsWith("set_") ||
                        this.buildXmlMasterResource.equals(resource)) {
                    targets.add(target);
                }
            }
        }

        //update children of targets contained in templates file
        this.updateTarget(targets);

        //sort the list of templates target
        //Targets of the master template file have the highest priority
        //Others target are sort by target name in ascending order
        Collections.sort(targets);

        //update condition tags of each input tag element
        for (Input input: inputs) {
            input.update(conditions, this.properties);
        }

        //sort the list of input
        //Inputs are sorted by their defaultValue value
        Collections.sort(inputs);

        List<Document> documents = new ArrayList<Document>();
        documents.add(masterDom);
        documents.addAll(templateDomList);


        //updating markers...
        for (Document document: documents) {

            NodeList markers = document.getElementsByTagName("marker");

            List<Element> markerList = new ArrayList<Element>();
            for (int k = 0; k < markers.getLength(); k++) {
                markerList.add((Element) markers.item(k));
            }

            for (Element marker: markerList) {

                String markerType = marker.getAttribute("type");

                Node parent = marker.getParentNode();
                Node nextNode = marker.getNextSibling();

                if (markerType.equals(INPUT_MARKER)) {

                    if (inputs.size() == 0) {
                        parent.removeChild(marker);
                    } else {
                        for (int y = 0; y < inputs.size(); y++) {

                            Element inputElement = inputs.get(y).getElement();
                            Node adoptNode = document.importNode(inputElement, true);

                            if (y == 0) {
                                parent.replaceChild(adoptNode, marker);
                            } else {
                                parent.insertBefore(adoptNode, nextNode);
                            }
                        }
                    }

                } else if (markerType.equals(CONDITION_MARKER)) {

                    if (conditions.size() == 0) {
                        parent.removeChild(marker);
                    } else {
                        for (int y = 0; y < conditions.size(); y++) {

                            Element conditionElement = conditions.get(y);
                            Node adoptNode = document.importNode(conditionElement, true);

                            if (y == 0) {
                                parent.replaceChild(adoptNode, marker);
                            } else {
                                parent.insertBefore(adoptNode, nextNode);
                            }
                        }
                    }

                } else if (markerType.equals(ANTCALL_IMPLEMENTATION_MARKER)) {

                    Node node = parent;
                    while (node != null && !(node.getNodeName().equals("target"))){
                        node = node.getParentNode();
                    }

                    if (node != null) {
                        String targetName = ((Element) node).getAttribute("name");
                        Target target = getTarget(targets, targetName);
                        List<Target> children = target.getChildren();

                        for (int i = 0; i < children.size(); i++) {

                            Target child = children.get(i);
                            Element element = document.createElement("antcall");
                            element.setAttribute("target", child.getName());

                            if (i == 0) {
                                parent.replaceChild(element, marker);
                            } else {
                                parent.insertBefore(element, nextNode);
                            }
                        }
                    }

                } else if (markerType.equals(ANTCALL_SERVICES_MARKER)) {
                    int ind = 0;

                    for (int i = 0; i < targets.size(); i++) {
                        Target target = targets.get(i);

                        if (target.getParent() == null && !target.isMasterTarget()) {
                            Element element = document.createElement("antcall");
                            element.setAttribute("target", target.getName());

                            if (ind == 0) {
                                parent.replaceChild(element, marker);
                            } else {
                                parent.insertBefore(element, nextNode);
                            }

                            ind++;
                        }
                     }
                } else {
                    this.logger.error("Error: marker type unknow");
                }
            }
        }

        //adding template Element to the master Document...
        Element rootElement = masterDom.getDocumentElement();
        for (Document document: templateDomList) {
            NodeList nodeList = document.getElementsByTagName("target");
            for (int i = 0; i< nodeList.getLength(); i++) {
                Element target = (Element) nodeList.item(i);
                rootElement.appendChild(masterDom.importNode(target, true));
            }
        }

        //Create parent directories if !exists
        File file = new File(this.outputBuildXmlFile);
        File parentFile = file.getParentFile();
        if (parentFile != null && !parentFile.exists()) {
            parentFile.mkdirs();
        }

        //writing the output file
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            this.logger.error("Cannot create a FileOutputStream from the file " + file.getAbsolutePath(), e);
        }
        serializeXML(masterDom, fileOutputStream, 65, true, false, headerComment);
    }

    /**
     *
     * @param strategies Strategies to set
     */
    public void setStrategies(Strategies strategies) {
        this.strategies = strategies;
    }

    /**
     *
     * @param preferences Preferences to set
     */
    public void setPreferences(Preferences preferences) {
        this.preferences = preferences;
    }


    /**
     *
     * @param outputBuildXmlFile Output build.xml file to set
     */
    public void setOutputBuildXmlFile(String outputBuildXmlFile) {
        this.outputBuildXmlFile = outputBuildXmlFile;
    }

    /**
     *
     * @param outputBuildPropertiesFile  Output build.properties file to set
     */
    public void setOutputBuildPropertiesFile(String outputBuildPropertiesFile) {
        this.outputBuildPropertiesFile = outputBuildPropertiesFile;
    }
}
