/**
 *  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.camel.tooling.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import de.pdark.decentxml.Attribute;
import de.pdark.decentxml.Comment;
import de.pdark.decentxml.Document;
import de.pdark.decentxml.Element;
import de.pdark.decentxml.Namespace;
import de.pdark.decentxml.Node;
import de.pdark.decentxml.NodeFilter;
import de.pdark.decentxml.NodeWithChildren;
import de.pdark.decentxml.Parent;
import de.pdark.decentxml.Text;
import de.pdark.decentxml.Token;
import de.pdark.decentxml.XMLWriter;
import org.apache.camel.spring.CamelEndpointFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;
import org.xml.sax.SAXException;

public class CamelNamespaces {

    public static Logger LOG = LoggerFactory.getLogger(CamelNamespaces.class);

    // All these statics are the sign, that Scala's "object" is not that excellent after all...

    public static final String springNS = "http://camel.apache.org/schema/spring";
    public static final String blueprintNS = "http://camel.apache.org/schema/blueprint";

    public static final String[] camelNamespaces = new String[] { springNS, blueprintNS };

    public static final Namespace springNamespace = new Namespace("", "http://www.springframework.org/schema/beans");
    public static final Namespace droolsNamespace = new Namespace("drools", "http://drools.org/schema/drools-spring");

    private static Schema _schema;

    private static Set<String> elementsWithDescription;

    public static Set<String> elementsWithDescription() {
        if (elementsWithDescription == null) {
            String file = "camelDescriptionElements.txt";
            String text = "";
            URL url = findResource(file, null);
            if (url != null) {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                try {
                    FileCopyUtils.copy(url.openStream(), baos);
                    text = new String(baos.toByteArray(), "UTF-8");
                } catch (IOException e) {
                    throw new RuntimeException(e.getMessage(), e);
                }
            } else {
                LOG.warn("Could not find file {} on the class path", file);
            }
            text += " camelContext";
            String[] arr = text.split(" ");

            elementsWithDescription = new HashSet<String>(Arrays.asList(arr));
        }
        return elementsWithDescription;
    }

    public static List<Node> nodesByNamespace(Document doc, final String namespaceUri, final String localName) {
        NodeFilter<Node> filter = new NodeFilter<Node>() {
            @Override
            public boolean matches(Node n) {
                if (n instanceof Element) {
                    Namespace ns = ((Element) n).getNamespace();
                    // TODO this doesn't work with empty prefixes!
                    if (!((Element) n).getName().equals(localName)) {
                        return false;
                    } else {
                        String uri = getNamespaceURI(n);
                        return ns != null;// && namespaceUri.equals(uri); // (!)
                    }
                }
                return false;
            }
        };
        return findNodes(doc, filter);
    }

    public static List<Node> findNodes(NodeWithChildren node, NodeFilter<Node> filter) {
        List<Node> answer = node.getNodes(filter);
        List<Node> children = node.getNodes();
        for (Node child: children) {
            if (child instanceof Element) {
                List<Node> childMatched = findNodes((Element) child, filter);
                answer.addAll(childMatched);
            }
        }
        return answer;
    }

    public static String nodeWithNamespacesToText(Node parseNode, Element namespacesNode) throws IOException {
        // we need to add any namespace prefixes defined in the root directory
        Node copy = parseNode.copy();
        if (copy instanceof Element && parseNode instanceof Element) {
            moveCommentsIntoDescriptionElements((Element) copy, (Element) parseNode);
            addParentNamespaces((Element) copy, namespacesNode.getParent());
        }
        return xmlToText(copy);
    }

    public static void moveCommentsIntoDescriptionElements(Element e, Element root) {
        // lets iterate through finding all comments which are then added to a description node
        int idx = 0;
        List<Node> nodes = e.getNodes();
        for (Node node: nodes.toArray(new Node[nodes.size()])) {
            if (node instanceof Comment) {
                Comment c = (Comment) node;
                Token token = c.getToken();
                if (token != null) {
                    String text = token.getText().trim().replace("<!--", "").replace("-->", "").trim();
                    Element descr = findOrCreateDescriptionOnNextElement(e, idx, root);
                    if (descr == null) {
                        // lets move the comment node to before the root element...
                        LOG.warn("No description node found");
                        e.removeNode(c);
                        Parent grandParent = root.getParent();
                        if (grandParent != null) {
                            grandParent.addNode(grandParent.nodeIndexOf(root), c);
                        } else {
                            LOG.warn("Cannot save the comment '{}' as there's no parent in the DOM", text);
                        }
                    } else {
                        if (descr.getNodes().size() > 0) {
                            text = "\n" + text;
                        }
                        descr.addNode(new Text(text));
                    }
                }
            } else if (node instanceof Element) {
                moveCommentsIntoDescriptionElements((Element) node, root);
            }
            idx++;
        }
    }

    protected static Element findOrCreateDescriptionOnNextElement(Element element, int commentIndex, Parent root) {
        // lets find the next peer element node and if it can contain a description lets use that
        List<Node> nodes = element.getNodes();
        Node[] array = nodes.toArray(new Node[nodes.size()]);
        for (int i=commentIndex + 1; i<array.length; i++) {
            if (array[i] instanceof Element) {
                if (elementsWithDescription().contains(element.getName())) {
                    return findOrCreateDescrptionElement((Element) array[i], root);
                }
            }
        }

        return findOrCreateDescrptionElement(element, root);
    }

    protected static Element findOrCreateDescrptionElement(Element element, Parent root) {
        for (Node node : element.getNodes()) {
            if (node instanceof Element) {
                if ("description".equals(((Element) node).getName())) {
                    return (Element) node;
                }
            }
        }

        Element parent = element.getParentElement();
        if (element == root || parent == null || parent == root) {
            return null;
        } else if (elementsWithDescription.contains(element.getName())) {
            // lets check for a namespace prefix
            String ebn = element.getBeginName();
            int idx = ebn.indexOf(":");
            String name = idx > 0 ? ebn.substring(0, idx + 1) + "description" : "description";
            Element description = new Element(name, element.getNamespace());

            element.addNode(0, description);
            return description;
        } else {
            return findOrCreateDescrptionElement(parent, root);
        }
    }

    public static String xmlToText(Node node) throws IOException {
        StringWriter buffer = new StringWriter();
        XMLWriter writer = new XMLWriter(buffer);
        node.toXML(writer);
        writer.close();

        return buffer.toString();
    }

    public static String getNamespaceURI(Node node) {
        if (node instanceof Element) {
            Namespace ns = ((Element) node).getNamespace();
            if (ns != null) {
                String uri = ns.getURI();
                if (uri == null || uri.length() == 0) {
                    String uriAttr = ns.getPrefix().equals("") ? ((Element) node).getAttributeValue("xmlns") : null;
                    if (uriAttr != null) {
                        return uriAttr;
                    } else {
                        return getNamespaceURI(((Element) node).getParent());
                    }
                } else {
                    return uri;
                }
            }
        }
        return null;
    }

    public static void addParentNamespaces(Element element, Parent parent) {
        if (parent instanceof Element) {
            for (Attribute attr : ((Element) parent).getAttributes()) {
                String name = attr.getName();
                if (name.startsWith("xmlns") && element.getAttributeValue(name) == null) {
                    element.setAttribute(name, attr.getValue());
                }
            }
            addParentNamespaces(element, ((Element) parent).getParent());
        }
    }

    public static Document getOwnerDocument(Node node) {
        if (node instanceof Element) {
            return ((Element) node).getDocument();
        } else if (node instanceof Document) {
            return (Document) node;
        }
        return null;
    }

    public static void replaceChild(Parent parent, Node newChild, Node oldNode) {
        int idx = parent.nodeIndexOf(oldNode);
        if (idx < 0) {
            parent.addNode(newChild);
        } else {
            parent.removeNode(idx);
            parent.addNode(idx, newChild);
        }
    }

    public static URL findResource(String name, Iterable<ClassLoader> classLoaders) {
        if (classLoaders == null) {
            classLoaders = Arrays.asList(
                Thread.currentThread().getContextClassLoader(),
                CamelNamespaces.class.getClassLoader()
            );
        }

        for (ClassLoader cl : classLoaders) {
            URL resource = tryLoadClass(cl, name);
            if (resource != null) {
                return resource;
            }
        }
        return null;
    }

    private static URL tryLoadClass(ClassLoader classLoader, String name) {
        try {
           return classLoader.getResource(name);
        } catch (Exception e) {
            return null;
        }
    }

    public void loadSchemasWith(final SchemaFinder finder) throws IOException, SAXException {
        loadSchemas(new SchemaFinder() {
            @Override
            public URL findSchema(XsdDetails xsd) {
                return finder.findSchema(xsd);
            }
        });
    }

    private static void loadSchemas(SchemaFinder loader) throws SAXException, IOException {
        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);

        XsdDetails[] xsds = new XsdDetails[] {
            new XsdDetails("camel-spring.xsd", "http://camel.apache.org/schema/spring/camel-spring.xsd", CamelEndpointFactoryBean.class),
            new XsdDetails("camel-blueprint.xsd", "http://camel.apache.org/schema/blueprint/camel-blueprint.xsd", org.apache.camel.blueprint.CamelEndpointFactoryBean.class)
        };

        List<Source> sources = new ArrayList<Source>(xsds.length);

        for (XsdDetails xsdd : xsds) {
            URL url = loader.findSchema(xsdd);
            if (url != null) {
                sources.add(new StreamSource(url.openStream(), xsdd.getUri()));
            } else {
                System.out.println("Warning could not find local resource " + xsdd.getPath() + " on classpath");
                sources.add(new StreamSource(xsdd.getUri()));
            }
        }

        _schema = factory.newSchema(sources.toArray(new Source[sources.size()]));
    }

    public static Schema camelSchemas() throws IOException, SAXException {
        if (_schema == null) {
            loadSchemas(new SchemaFinder() {
                @Override
                public URL findSchema(XsdDetails details) {
                    return details.getClassLoader().getResource(details.getPath());
                }
            });
        }
        return _schema;
    }

}
