/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF 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 org.apache.cxf.tools.common.toolspec;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.XMLConstants;
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 javax.xml.transform.stream.StreamSource;
import javax.xml.xpath.XPathConstants;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.apache.cxf.common.i18n.Message;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.helpers.XPathUtils;
import org.apache.cxf.tools.common.ToolException;
import org.apache.cxf.tools.common.dom.ExtendedDocumentBuilder;

public class ToolSpec {

    private static final Logger LOG = LogUtils.getL7dLogger(ToolSpec.class);

    private final ExtendedDocumentBuilder builder = new ExtendedDocumentBuilder();
    private Document doc;
    private Tool handler;

    public ToolSpec() {
    }

    public ToolSpec(InputStream in) throws ToolException {
        this(in, true);
    }

    public ToolSpec(InputStream in, boolean validate) throws ToolException {
        if (in == null) {
            throw new NullPointerException("Cannot create a ToolSpec object from a null stream");
        }
        try {
            builder.setValidating(validate);
            this.doc = builder.parse(in);
        } catch (Exception ex) {
            Message message = new Message("FAIL_TO_PARSING_TOOLSPCE_STREAM", LOG);
            throw new ToolException(message, ex);
        }
    }

    public ToolSpec(Document d) {
        if (d == null) {
            throw new NullPointerException("Cannot create a ToolSpec object from "
                                           + "a null org.w3c.dom.Document");
        }
        this.doc = d;
    }

    public ExtendedDocumentBuilder getDocumentBuilder() {
        return builder;
    }

    public boolean isValidInputStream(String id) {
        Element streams = getStreams();

        if (streams == null) {
            return false;
        }

        List<Element> elemList = DOMUtils.findAllElementsByTagNameNS(streams,
                                                                     Tool.TOOL_SPEC_PUBLIC_ID,
                                                                     "instream");
        for (Element elem : elemList) {
            if (elem.getAttribute("id").equals(id)) {
                return true;
            }
        }
        return false;
    }

    public Element getElementById(String id) {
        Element ele = doc.getElementById(id);
        if (ele != null) {
            return ele;
        }

        XPathUtils xpather = new XPathUtils(new HashMap<String, String>());
        NodeList nl = (NodeList) xpather.getValue("//*[@id='" + id + "']",
                                                  doc,
                                                  XPathConstants.NODESET);
        if (nl != null && nl.getLength() > 0) {
            return (Element)nl.item(0);
        }
        return null;
    }

    public boolean hasHandler() {
        return doc.getDocumentElement().hasAttribute("handler");
    }

    public Tool getHandler() throws ToolException {
        if (!hasHandler()) {
            return null;
        }

        if (handler == null) {
            String handlerClz = doc.getDocumentElement().getAttribute("handler");

            try {
                handler = (Tool)Class.forName(handlerClz).getDeclaredConstructor().newInstance();
            } catch (Exception ex) {
                Message message = new Message("FAIL_TO_INSTANTIATE_HANDLER", LOG, handlerClz);
                throw new ToolException(message, ex);
            }
        }
        return handler;
    }

    public Tool getHandler(ClassLoader loader) throws ToolException {
        if (!hasHandler()) {
            return null;
        }

        if (handler == null) {
            String handlerClz = doc.getDocumentElement().getAttribute("handler");

            try {
                handler = (Tool)Class.forName(handlerClz, true, loader).getDeclaredConstructor().newInstance();
            } catch (Exception ex) {
                Message message = new Message("FAIL_TO_INSTANTIATE_HANDLER", LOG, handlerClz);
                throw new ToolException(message, ex);
            }
        }
        return handler;
    }

    public Element getStreams() {
        List<Element> elemList = DOMUtils.findAllElementsByTagNameNS(doc.getDocumentElement(),
                                                                     Tool.TOOL_SPEC_PUBLIC_ID,
                                                                     "streams");
        if (!elemList.isEmpty()) {
            return elemList.get(0);
        }
        return null;
    }

    public List<String> getInstreamIds() {
        List<String> res = new ArrayList<>();
        Element streams = getStreams();

        if (streams != null) {
            List<Element> elemList = DOMUtils.findAllElementsByTagNameNS(streams,
                                                                         Tool.TOOL_SPEC_PUBLIC_ID,
                                                                         "instream");
            for (Element elem : elemList) {
                res.add(elem.getAttribute("id"));
            }
        }
        return Collections.unmodifiableList(res);
    }

    public List<String> getOutstreamIds() {
        List<String> res = new ArrayList<>();
        Element streams = getStreams();

        if (streams != null) {
            List<Element> elemList = DOMUtils.findAllElementsByTagNameNS(streams,
                                                                         Tool.TOOL_SPEC_PUBLIC_ID,
                                                                         "outstream");

            for (Element elem : elemList) {
                res.add(elem.getAttribute("id"));
            }
        }
        return Collections.unmodifiableList(res);
    }

    public Element getUsage() {
        return DOMUtils.findAllElementsByTagNameNS(doc.getDocumentElement(),
                                            Tool.TOOL_SPEC_PUBLIC_ID,
                                            "usage").get(0);
    }

    public void transform(InputStream stylesheet, OutputStream out) throws TransformerException {
        TransformerFactory factory = TransformerFactory.newInstance();
        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, Boolean.TRUE);
        Transformer trans = factory.newTransformer(new StreamSource(stylesheet));
        trans.transform(new DOMSource(doc), new StreamResult(out));
    }

    public Element getPipeline() {

        List<Element> elemList = DOMUtils.findAllElementsByTagNameNS(doc.getDocumentElement(),
                                            Tool.TOOL_SPEC_PUBLIC_ID,
                                            "pipeline");
        if (!elemList.isEmpty()) {
            return elemList.get(0);
        }
        return null;
    }

    public List<Element> getUsageForms() {
        return DOMUtils.findAllElementsByTagNameNS(getUsage(), Tool.TOOL_SPEC_PUBLIC_ID, "form");
    }

    /**
     * Arguments can have streamref attributes which associate them with a
     * stream. Tools usually request streams and rely on them being ready. If an
     * argument is given a streamref, then the container constructs a stream
     * from the argument value. This would usually be a simple FileInputStream
     * or FileOutputStream. The mechanics of this are left for the container to
     * sort out, but that is the reason why this getter method exists.
     */
    public String getStreamRefName(String streamId) {
        if (getUsage() != null) {
            List<Element> elemList = DOMUtils.findAllElementsByTagNameNS(getUsage(),
                                                                        Tool.TOOL_SPEC_PUBLIC_ID,
                                                                        "associatedArgument");
            for (Element elem : elemList) {
                if (elem.getAttribute("streamref").equals(streamId)) {
                    return ((Element)elem.getParentNode()).getAttribute("id");
                }
            }

            elemList = DOMUtils.findAllElementsByTagNameNS(getUsage(),
                                                           Tool.TOOL_SPEC_PUBLIC_ID,
                                                           "argument");
            for (Element elem : elemList) {
                if (elem.getAttribute("streamref").equals(streamId)) {
                    return elem.getAttribute("id");
                }
            }
        }
        return null;
    }

    public String getParameterDefault(String name) {
        Element el = getElementById(name);
        if (LOG.isLoggable(Level.FINE)) {
            LOG.fine("Element with id " + name + " is " + el);
        }
        if (el != null) {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("local name is " + el.getLocalName());
            }
            if ("argument".equals(el.getLocalName())) {
                if (el.hasAttribute("default")) {
                    return el.getAttribute("default");
                }
            } else if ("option".equals(el.getLocalName())) {
                List<Element> elemList =
                    DOMUtils.findAllElementsByTagNameNS(el,
                                                        "http://cxf.apache.org/Xpipe/ToolSpecification",
                                                        "associatedArgument");
                if (!elemList.isEmpty()) {
                    Element assArg = elemList.get(0);
                    if (assArg.hasAttribute("default")) {
                        return assArg.getAttribute("default");
                    }
                }
            }
        }
        return null;
    }

    public String getAnnotation() {
        String result = null;
        Element element = doc.getDocumentElement();

        Node node = element.getFirstChild();
        while (node != null) {
            if ((node.getNodeType() == Node.ELEMENT_NODE)
                && ("annotation".equals(node.getNodeName()))) {
                result = node.getNodeValue();
                break;
            }
            node = node.getNextSibling();
        }
        return result;
    }

}
