/**
 * 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.validator.internal;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.wsdl.Binding;
import javax.wsdl.BindingOperation;
import javax.wsdl.Definition;
import javax.wsdl.Fault;
import javax.wsdl.Import;
import javax.wsdl.Operation;
import javax.wsdl.Part;
import javax.wsdl.Port;
import javax.wsdl.PortType;
import javax.wsdl.Service;
import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.xpath.XPathConstants;

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

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.common.i18n.Message;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.common.util.StringUtils;
import org.apache.cxf.common.xmlschema.SchemaCollection;
import org.apache.cxf.helpers.CastUtils;
import org.apache.cxf.helpers.XPathUtils;
import org.apache.cxf.service.model.SchemaInfo;
import org.apache.cxf.service.model.ServiceSchemaInfo;
import org.apache.cxf.tools.common.ToolException;
import org.apache.cxf.tools.validator.internal.model.FailureLocation;
import org.apache.cxf.tools.validator.internal.model.XBinding;
import org.apache.cxf.tools.validator.internal.model.XDef;
import org.apache.cxf.tools.validator.internal.model.XFault;
import org.apache.cxf.tools.validator.internal.model.XInput;
import org.apache.cxf.tools.validator.internal.model.XMessage;
import org.apache.cxf.tools.validator.internal.model.XNode;
import org.apache.cxf.tools.validator.internal.model.XOperation;
import org.apache.cxf.tools.validator.internal.model.XOutput;
import org.apache.cxf.tools.validator.internal.model.XPort;
import org.apache.cxf.tools.validator.internal.model.XPortType;
import org.apache.cxf.tools.validator.internal.model.XService;
import org.apache.cxf.wsdl.WSDLConstants;
import org.apache.cxf.wsdl.WSDLManager;
import org.apache.cxf.wsdl11.SchemaUtil;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaType;

public class WSDLRefValidator extends AbstractDefinitionValidator {
    protected static final Logger LOG = LogUtils.getL7dLogger(WSDLRefValidator.class);
    protected List<XNode> vNodes = new ArrayList<>();

    private Set<QName> portTypeRefNames = new HashSet<>();
    private Set<QName> messageRefNames = new HashSet<>();
    private Map<QName, Service> services = new HashMap<>();

    private ValidationResult vResults = new ValidationResult();

    private Definition definition;
    private Document baseDoc;

    private List<Definition> importedDefinitions;
    private SchemaCollection schemaCollection = new SchemaCollection();

    private boolean suppressWarnings;

    public WSDLRefValidator(Definition wsdl, Document doc) {
        this(wsdl, doc, BusFactory.getThreadDefaultBus());
    }

    public WSDLRefValidator(Definition wsdl, Document doc, Bus bus) {
        this.definition = wsdl;
        baseDoc = doc;
        importedDefinitions = new ArrayList<>();
        parseImports(wsdl);
        processSchemas(bus);
    }
    private void getSchemas(Bus bus) {
        Map<String, Element> schemaList = new HashMap<>();
        SchemaUtil schemaUtil = new SchemaUtil(bus, schemaList);
        List<SchemaInfo> si = new ArrayList<>();
        schemaUtil.getSchemas(definition, schemaCollection, si);
        ServiceSchemaInfo ssi = new ServiceSchemaInfo();
        ssi.setSchemaCollection(schemaCollection);
        ssi.setSchemaInfoList(si);
        ssi.setSchemaElementList(schemaList);
        bus.getExtension(WSDLManager.class).putSchemasForDefinition(definition, ssi);
    }
    private void processSchemas(Bus bus) {
        try {
            ServiceSchemaInfo info = bus.getExtension(WSDLManager.class)
                .getSchemasForDefinition(definition);
            if (info == null) {
                getSchemas(bus);
            } else {
                schemaCollection = info.getSchemaCollection();
            }
            checkTargetNamespace(this.definition.getTargetNamespace());
        } catch (Exception ex) {
            throw new ToolException(ex);
        }
    }
    private Collection<Import> getImports(final Definition wsdlDef) {
        Collection<Import> importList = new ArrayList<>();
        Map<?, ?> imports = wsdlDef.getImports();
        for (Map.Entry<?, ?> entry : imports.entrySet()) {
            List<Import> lst = CastUtils.cast((List<?>)entry.getValue());
            importList.addAll(lst);
        }

        return importList;
    }
    private void parseImports(Definition def) {
        for (Import impt : getImports(def)) {
            if (!importedDefinitions.contains(impt.getDefinition())) {
                importedDefinitions.add(impt.getDefinition());
                parseImports(impt.getDefinition());
            }
        }
    }


    private void checkTargetNamespace(String path) {
        // no check as any namespace URI is already a valid target namespace string.
    }

    public void setSuppressWarnings(boolean s) {
        this.suppressWarnings = s;
    }

    public ValidationResult getValidationResults() {
        return this.vResults;
    }

    private Document getWSDLDocument(final String wsdl) throws URISyntaxException {
        return new Stax2DOM().getDocument(wsdl);
    }

    private Document getWSDLDocument() throws Exception {
        if (baseDoc != null) {
            return baseDoc;
        }
        return getWSDLDocument(this.definition.getDocumentBaseURI());
    }

    private List<Document> getWSDLDocuments() {
        List<Document> docs = new ArrayList<>();
        try {
            docs.add(getWSDLDocument());

            if (null != importedDefinitions) {
                for (Definition d : importedDefinitions) {
                    docs.add(getWSDLDocument(d.getDocumentBaseURI()));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            // ignore
        }

        return docs;
    }

    private boolean isExist(List<Document> docs, XNode vNode) {
        for (Document doc : docs) {
            if (vNode.matches(doc)) {
                return true;
            }
        }
        return false;
    }

    private FailureLocation getFailureLocation(List<Document> docs, XNode fNode) {
        if (fNode == null) {
            return null;
        }

        XPathUtils xpather = new XPathUtils(fNode.getNSMap());
        for (Document doc : docs) {
            Node node = (Node) xpather.getValue(fNode.toString(), doc, XPathConstants.NODE);
            if (null != node) {
                try {
                    return new FailureLocation((Location)node.getUserData("location"),
                                               doc.getDocumentURI());
                } catch (Exception ex) {
                    //ignore, probably not DOM level 3
                }
            }
        }
        return null;
    }

    public boolean isValid() {
        try {
            loadServices();

            collectValidationPoints();

            List<Document> wsdlDocs = getWSDLDocuments();
            for (XNode vNode : vNodes) {
                if (!isExist(wsdlDocs, vNode)) {
                    //System.out.println("Fail: " + vNode.getXPath());
                    FailureLocation loc = getFailureLocation(wsdlDocs, vNode.getFailurePoint());

                    vResults.addError(new Message("FAILED_AT_POINT",
                                                  LOG,
                                                  loc.getLocation().getLineNumber(),
                                                  loc.getLocation().getColumnNumber(),
                                                  loc.getDocumentURI(),
                                                  vNode.getPlainText()));
                }
            }
        } catch (Exception e) {
            this.vResults.addError(e.getMessage());
            return false;
        }
        return vResults.isSuccessful();
    }

    private void addServices(final Definition wsdlDef) {
        Iterator<QName> sNames = CastUtils.cast(wsdlDef.getServices().keySet().iterator());
        while (sNames.hasNext()) {
            QName sName = sNames.next();
            services.put(sName, definition.getService(sName));
        }
    }

    private void loadServices() {
        addServices(this.definition);
        if (importedDefinitions != null) {
            for (Definition d : importedDefinitions) {
                addServices(d);
            }
        }
    }

    private Map<QName, XNode> getBindings(Service service) {
        Map<QName, XNode> bindings = new HashMap<>();

        if (service.getPorts().values().isEmpty()) {
            throw new ToolException("Service " + service.getQName() + " does not contain any usable ports");
        }
        Collection<Port> ports = CastUtils.cast(service.getPorts().values());
        for (Port port : ports) {
            Binding binding = port.getBinding();
            bindings.put(binding.getQName(), getXNode(service, port));
            if (WSDLConstants.NS_WSDL11.equals(binding.getQName().getNamespaceURI())) {
                throw new ToolException("Binding "
                                        + binding.getQName().getLocalPart()
                                        + " namespace set improperly.");
            }
        }

        return bindings;
    }

    private Map<QName, Operation> getOperations(PortType portType) {
        Map<QName, Operation> operations = new HashMap<>();
        Collection<Operation> pops = CastUtils.cast(portType.getOperations());
        for (Operation op : pops) {
            operations.put(new QName(portType.getQName().getNamespaceURI(), op.getName()), op);
        }
        return operations;
    }

    private XNode getXNode(Service service, Port port) {
        XNode vService = getXNode(service);

        XPort pNode = new XPort();
        pNode.setName(port.getName());
        pNode.setParentNode(vService);
        return pNode;
    }

    private XNode getXNode(Service service) {
        XDef xdef = new XDef();
        xdef.setTargetNamespace(service.getQName().getNamespaceURI());

        XService sNode = new XService();
        sNode.setName(service.getQName().getLocalPart());
        sNode.setParentNode(xdef);
        return sNode;
    }

    private XNode getXNode(Binding binding) {
        XDef xdef = new XDef();
        xdef.setTargetNamespace(binding.getQName().getNamespaceURI());

        XBinding bNode = new XBinding();
        bNode.setName(binding.getQName().getLocalPart());
        bNode.setParentNode(xdef);
        return bNode;
    }

    private XNode getXNode(PortType portType) {
        XDef xdef = new XDef();
        xdef.setTargetNamespace(portType.getQName().getNamespaceURI());

        XPortType pNode = new XPortType();
        pNode.setName(portType.getQName().getLocalPart());
        pNode.setParentNode(xdef);
        return pNode;
    }

    private XNode getOperationXNode(XNode pNode, String opName) {
        XOperation node = new XOperation();
        node.setName(opName);
        node.setParentNode(pNode);
        return node;
    }

    private XNode getInputXNode(XNode opVNode, String name) {
        XInput oNode = new XInput();
        oNode.setName(name);
        oNode.setParentNode(opVNode);

        if (name != null && name.equals(opVNode.getAttributeValue() + "Request")) {
            oNode.setDefaultAttributeValue(true);
        }
        return oNode;
    }

    private XNode getOutputXNode(XNode opVNode, String name) {
        XOutput oNode = new XOutput();
        oNode.setName(name);
        oNode.setParentNode(opVNode);
        if (name != null && name.equals(opVNode.getAttributeValue() + "Response")) {
            oNode.setDefaultAttributeValue(true);
        }
        return oNode;
    }

    private XNode getFaultXNode(XNode opVNode, String name) {
        XFault oNode = new XFault();
        oNode.setName(name);
        oNode.setParentNode(opVNode);
        return oNode;
    }

    private XNode getXNode(javax.wsdl.Message msg) {
        XDef xdef = new XDef();
        xdef.setTargetNamespace(msg.getQName().getNamespaceURI());

        XMessage mNode = new XMessage();
        mNode.setName(msg.getQName().getLocalPart());
        mNode.setParentNode(xdef);
        return mNode;
    }

    private void addWarning(String warningMsg) {
        if (suppressWarnings) {
            return;
        }
        vResults.addWarning(warningMsg);
    }

    private void collectValidationPoints() throws Exception {
        if (services.isEmpty()) {
            LOG.log(Level.WARNING, "WSDL document "
                    + this.definition.getDocumentBaseURI() + " does not define any services");
            //addWarning("WSDL document does not define any services");
            Collection<QName> ports = CastUtils.cast(this.definition.getAllPortTypes().keySet());
            portTypeRefNames.addAll(ports);
        } else {
            collectValidationPointsForBindings();
        }

        collectValidationPointsForPortTypes();
        collectValidationPointsForMessages();
    }

    private void collectValidationPointsForBindings() throws Exception {
        Map<QName, XNode> vBindingNodes = new HashMap<>();
        for (Service service : services.values()) {
            vBindingNodes.putAll(getBindings(service));
        }

        for (Map.Entry<QName, XNode> entry : vBindingNodes.entrySet()) {
            QName bName = entry.getKey();
            Binding binding = this.definition.getBinding(bName);
            if (binding == null) {
                LOG.log(Level.SEVERE, bName.toString()
                        + " is not correct, please check that the correct namespace is being used");
                throw new Exception(bName.toString()
                        + " is not correct, please check that the correct namespace is being used");
            }
            XNode vBindingNode = getXNode(binding);
            vBindingNode.setFailurePoint(entry.getValue());
            vNodes.add(vBindingNode);

            if (binding.getPortType() == null) {
                continue;
            }
            portTypeRefNames.add(binding.getPortType().getQName());

            XNode vPortTypeNode = getXNode(binding.getPortType());
            vPortTypeNode.setFailurePoint(vBindingNode);
            vNodes.add(vPortTypeNode);
            Collection<BindingOperation> bops = CastUtils.cast(binding.getBindingOperations());
            for (BindingOperation bop : bops) {
                XNode vOpNode = getOperationXNode(vPortTypeNode, bop.getName());
                XNode vBopNode = getOperationXNode(vBindingNode, bop.getName());
                vOpNode.setFailurePoint(vBopNode);
                vNodes.add(vOpNode);
                if (bop.getBindingInput() != null) {
                    String inName = bop.getBindingInput().getName();
                    if (!StringUtils.isEmpty(inName)) {
                        XNode vInputNode = getInputXNode(vOpNode, inName);
                        vInputNode.setFailurePoint(getInputXNode(vBopNode, inName));
                        vNodes.add(vInputNode);
                    }
                }
                if (bop.getBindingOutput() != null) {
                    String outName = bop.getBindingOutput().getName();
                    if (!StringUtils.isEmpty(outName)) {
                        XNode vOutputNode = getOutputXNode(vOpNode, outName);
                        vOutputNode.setFailurePoint(getOutputXNode(vBopNode, outName));
                        vNodes.add(vOutputNode);
                    }
                }
                for (Iterator<?> iter1 = bop.getBindingFaults().keySet().iterator(); iter1.hasNext();) {
                    String faultName = (String) iter1.next();
                    XNode vFaultNode = getFaultXNode(vOpNode, faultName);
                    vFaultNode.setFailurePoint(getFaultXNode(vBopNode, faultName));
                    vNodes.add(vFaultNode);
                }
            }
        }
    }

    private javax.wsdl.Message getMessage(QName msgName) {
        javax.wsdl.Message message = this.definition.getMessage(msgName);
        if (message == null) {
            for (Definition d : importedDefinitions) {
                message = d.getMessage(msgName);
                if (message != null) {
                    break;
                }
            }
        }
        return message;
    }

    private void collectValidationPointsForMessages() {
        for (QName msgName : messageRefNames) {
            javax.wsdl.Message message = getMessage(msgName);
            for (Iterator<?> iter = message.getParts().values().iterator(); iter.hasNext();) {
                Part part = (Part) iter.next();
                QName elementName = part.getElementName();
                QName typeName = part.getTypeName();

                if (elementName == null && typeName == null) {
                    vResults.addError(new Message("PART_NO_TYPES", LOG));
                    continue;
                }

                if (elementName != null && typeName != null) {
                    vResults.addError(new Message("PART_NOT_UNIQUE", LOG));
                    continue;
                }

                if (elementName != null && typeName == null) {
                    boolean valid = validatePartType(elementName.getNamespaceURI(),
                                                     elementName.getLocalPart(), true);
                    if (!valid) {
                        vResults.addError(new Message("TYPE_REF_NOT_FOUND", LOG, message.getQName(),
                                                      part.getName(), elementName));
                    }

                }
                if (typeName != null && elementName == null) {
                    boolean valid = validatePartType(typeName.getNamespaceURI(),
                                                     typeName.getLocalPart(),
                                                     false);
                    if (!valid) {
                        vResults.addError(new Message("TYPE_REF_NOT_FOUND", LOG, message.getQName(),
                                                      part.getName(), typeName));
                    }
                }
            }
        }
    }

    private PortType getPortType(QName ptName) {
        PortType portType = this.definition.getPortType(ptName);
        if (portType == null) {
            for (Definition d : importedDefinitions) {
                portType = d.getPortType(ptName);
                if (portType != null) {
                    break;
                }
            }
        }
        return portType;
    }

    private void collectValidationPointsForPortTypes() {
        for (QName ptName : portTypeRefNames) {
            PortType portType = getPortType(ptName);
            if (portType == null) {
                vResults.addError(new Message("NO_PORTTYPE", LOG, ptName));
                continue;
            }

            XNode vPortTypeNode = getXNode(portType);
            for (Operation operation : getOperations(portType).values()) {
                XNode vOperationNode = getOperationXNode(vPortTypeNode, operation.getName());
                if (operation.getInput() == null) {
                    vResults.addError(new Message("WRONG_MEP", LOG, operation.getName(),
                                                  portType.getQName()));
                    continue;
                }
                javax.wsdl.Message inMsg = operation.getInput().getMessage();
                if (inMsg == null) {
                    addWarning("Operation " + operation.getName() + " in PortType: "
                               + portType.getQName() + " has no input message");
                } else {
                    XNode vInMsgNode = getXNode(inMsg);
                    vInMsgNode.setFailurePoint(getInputXNode(vOperationNode, operation.getInput().getName()));
                    vNodes.add(vInMsgNode);
                    messageRefNames.add(inMsg.getQName());
                }

                if (operation.getOutput() != null) {
                    javax.wsdl.Message outMsg = operation.getOutput().getMessage();

                    if (outMsg == null) {
                        addWarning("Operation " + operation.getName() + " in PortType: "
                                   + portType.getQName() + " has no output message");
                    } else {
                        XNode vOutMsgNode = getXNode(outMsg);
                        vOutMsgNode.setFailurePoint(getOutputXNode(vOperationNode,
                                                                   operation.getOutput().getName()));
                        vNodes.add(vOutMsgNode);
                        messageRefNames.add(outMsg.getQName());
                    }
                }
                for (Iterator<?> iter = operation.getFaults().values().iterator(); iter.hasNext();) {
                    Fault fault = (Fault) iter.next();
                    javax.wsdl.Message faultMsg = fault.getMessage();
                    XNode vFaultMsgNode = getXNode(faultMsg);
                    vFaultMsgNode.setFailurePoint(getFaultXNode(vOperationNode, fault.getName()));
                    vNodes.add(vFaultMsgNode);
                    messageRefNames.add(faultMsg.getQName());
                }
            }
        }
    }

    private boolean validatePartType(String namespace, String name, boolean isElement) {

        boolean partvalid = false;

        if (namespace.equals(WSDLConstants.NS_SCHEMA_XSD)) {
            if (isElement) {
                XmlSchemaElement schemaEle =
                    schemaCollection.getElementByQName(new QName(WSDLConstants.NS_SCHEMA_XSD, name));
                partvalid = schemaEle != null;
            } else {
                if ("anyType".equals(name)) {
                    return true;
                }
                XmlSchemaType schemaType =
                    schemaCollection.getTypeByQName(new QName(WSDLConstants.NS_SCHEMA_XSD, name));

                partvalid = schemaType != null;
            }

        } else {
            if (isElement) {
                if (schemaCollection.getElementByQName(new QName(namespace, name)) != null) {
                    partvalid = true;
                }
            } else {
                if (schemaCollection.getTypeByQName(new QName(namespace, name)) != null) {
                    partvalid = true;
                }
            }
        }
        return partvalid;
    }

    public String getErrorMessage() {
        return vResults.toString();
    }

    public Definition getDefinition() {
        return this.definition;
    }
}
