package org.jboss.windup.rules.apps.xml.condition.validators;

import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.graph.model.WindupVertexFrame;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.rules.apps.xml.condition.XmlFile;
import org.jboss.windup.rules.apps.xml.condition.XmlFileEvaluateXPathFunction;
import org.jboss.windup.rules.apps.xml.condition.XmlFileFunctionResolver;
import org.jboss.windup.rules.apps.xml.condition.XmlFileMatchesXPathFunction;
import org.jboss.windup.rules.apps.xml.condition.XmlFileParameterMatchCache;
import org.jboss.windup.rules.apps.xml.condition.XmlFileStartFrameXPathFunction;
import org.jboss.windup.rules.apps.xml.condition.XmlFileXPathTransformer;
import org.jboss.windup.rules.apps.xml.model.NamespaceMetaModel;
import org.jboss.windup.rules.apps.xml.model.XmlFileModel;
import org.jboss.windup.rules.apps.xml.model.XmlTypeReferenceModel;
import org.jboss.windup.rules.apps.xml.service.XmlFileService;
import org.jboss.windup.util.Logging;
import org.jboss.windup.util.xml.LocationAwareContentHandler;
import org.jboss.windup.util.xml.NamespaceMapContext;
import org.jboss.windup.util.xml.XmlUtil;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.param.DefaultParameterStore;
import org.ocpsoft.rewrite.param.Parameter;
import org.ocpsoft.rewrite.param.ParameterStore;
import org.ocpsoft.rewrite.param.RegexParameterizedPatternParser;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionException;
import javax.xml.xpath.XPathFunctionResolver;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * This is a part of XmlFile execution. Validator is checking that xpath/xpathResult attributes matches the queried one.
 */
public class XmlFileXpathValidator implements XmlFileValidator
{
    protected static final String WINDUP_NS_PREFIX = "windup";
    protected static final String WINDUP_NS_URI = "http://windup.jboss.org/windupv2functions";
    private static XPathFactory factory = XPathFactory.newInstance();
    private XPathExpression compiledXPath;
    private final XPath xpathEngine;
    private final XmlFileFunctionResolver xmlFileFunctionResolver;
    private XmlFile.XmlFileEvaluationStrategy evaluationStrategy;
    private String xpathResultMatch;
    private List<WindupVertexFrame> results = new ArrayList<>();
    private Map<String, String> namespaces = new HashMap<>();
    private String xpathString;
    private static final Logger LOG = Logging.get(XmlFileXpathValidator.class);
    private RegexParameterizedPatternParser xpathPattern;

    public XmlFileXpathValidator()
    {

        this.namespaces.put(WINDUP_NS_PREFIX, WINDUP_NS_URI);

        this.xpathEngine = factory.newXPath();
        final XPathFunctionResolver originalResolver = this.xpathEngine.getXPathFunctionResolver();
        xmlFileFunctionResolver = new XmlFileFunctionResolver(originalResolver);
        this.xpathEngine.setXPathFunctionResolver(xmlFileFunctionResolver);
    }

    public void setXpathString(String xpath)
    {
        this.xpathString = xpath;
        this.compiledXPath = null;

        if (xpath != null)
        {
            this.xpathPattern = new RegexParameterizedPatternParser(this.xpathString);
        }
    }

    @Override
    public boolean isValid(GraphRewrite event, EvaluationContext context, XmlFileModel model)
    {
        if(xpathString == null) {
            return true;
        }
        String xpathStringWithParameterFunctions = XmlFileXPathTransformer.transformXPath(this.xpathString);
        LOG.fine("XmlFile compiled: " + this.xpathString + " to " + xpathStringWithParameterFunctions);

        XmlFileService xmlFileService = new XmlFileService(event.getGraphContext());
        Document document = xmlFileService.loadDocumentQuiet(event, context, model);
        if (document != null)
        {
            final ParameterStore store = DefaultParameterStore.getInstance(context);

            final XmlFileParameterMatchCache paramMatchCache = new XmlFileParameterMatchCache();
            this.xmlFileFunctionResolver.registerFunction(WINDUP_NS_URI, "startFrame",
                        new XmlFileStartFrameXPathFunction(paramMatchCache));
            this.xmlFileFunctionResolver
                        .registerFunction(WINDUP_NS_URI, "evaluate", new XmlFileEvaluateXPathFunction(evaluationStrategy));
            this.xmlFileFunctionResolver.registerFunction(WINDUP_NS_URI, "matches", new XmlFileMatchesXPathFunction(context, store,
                        paramMatchCache, event));
            this.xmlFileFunctionResolver.registerFunction(WINDUP_NS_URI, "persist", new XmlFilePersistXPathFunction(event, context, model,
                        evaluationStrategy, store, paramMatchCache, results));

            if (compiledXPath == null)
            {
                NamespaceMapContext nsContext = new NamespaceMapContext(namespaces);
                this.xpathEngine.setNamespaceContext(nsContext);
                try
                {
                    this.compiledXPath = xpathEngine.compile(xpathStringWithParameterFunctions);
                }
                catch (Exception e)
                {
                    String message = e.getMessage();

                    // brutal hack to try to get a reasonable error message (ugly, but it seems to work)
                    if (message == null && e.getCause() != null && e.getCause().getMessage() != null)
                    {
                        message = e.getCause().getMessage();
                    }
                    LOG.severe("Condition: " + this + " failed to run, as the following xpath was uncompilable: " + xpathString
                                + " (compiled contents: " + xpathStringWithParameterFunctions + ") due to: "
                                + message);
                    return false;
                }
            }

            /**
             * This actually does the work.
             */
            XmlUtil.xpathNodeList(document, compiledXPath);
            evaluationStrategy.modelSubmissionRejected();
        }
        return !results.isEmpty();
    }

    public List<WindupVertexFrame> getAndClearResultLocations()
    {
        List<WindupVertexFrame> output = results;
        results = new ArrayList<WindupVertexFrame>();
        return output;
    }

    public void setXpathResult(String xpathResult)
    {
        this.xpathResultMatch = xpathResult;
    }

    public String getXpathString()
    {
        return xpathString;
    }

    public XPathExpression getXpathExpression()
    {
        return compiledXPath;
    }

    public void setParameterStore(ParameterStore store)
    {
        if (this.xpathPattern != null)
        {
            this.xpathPattern.setParameterStore(store);
        }
    }

    public Collection<? extends String> getRequiredParamaterNames()
    {
        if (xpathPattern != null)
        {
            return xpathPattern.getRequiredParameterNames();
        }
        else
        {
            return Collections.emptyList();
        }

    }

    public void addNamespace(String prefix, String url)
    {
        this.namespaces.put(prefix, url);
    }

    public void setEvaluationStrategy(XmlFile.XmlFileEvaluationStrategy evaluationStrategy)
    {
        this.evaluationStrategy = evaluationStrategy;
    }

    final class XmlFilePersistXPathFunction implements XPathFunction
    {
        private final GraphRewrite event;
        private final EvaluationContext context;
        private final XmlFileModel xml;
        private final XmlFile.XmlFileEvaluationStrategy evaluationStrategy;
        private final ParameterStore store;
        private final XmlFileParameterMatchCache paramMatchCache;
        private final List<WindupVertexFrame> resultLocations;

        XmlFilePersistXPathFunction(GraphRewrite event, EvaluationContext context, XmlFileModel xml,
                    XmlFile.XmlFileEvaluationStrategy evaluationStrategy,
                    ParameterStore store,
                    XmlFileParameterMatchCache paramMatchCache, List<WindupVertexFrame> resultLocations)
        {
            this.event = event;
            this.context = context;
            this.xml = xml;
            this.evaluationStrategy = evaluationStrategy;
            this.store = store;
            this.paramMatchCache = paramMatchCache;
            this.resultLocations = resultLocations;
        }

        @Override
        public Object evaluate(@SuppressWarnings("rawtypes") List args) throws XPathFunctionException
        {
            int frameIdx = ((Double) args.get(0)).intValue();
            NodeList arg1 = (NodeList) args.get(1);
            String nodeText = XmlUtil.nodeListToString(arg1);
            LOG.fine("persist(" + frameIdx + ", " + nodeText + ")");

            for (int i = 0; i < arg1.getLength(); i++)
            {
                Node node = arg1.item(i);
                if (xpathResultMatch != null)
                {
                    if (!node.toString().matches(xpathResultMatch))
                    {
                        continue;
                    }
                }
                // Everything passed for this Node. Start creating XmlTypeReferenceModel for it.
                int lineNumber = (int) node.getUserData(
                            LocationAwareContentHandler.LINE_NUMBER_KEY_NAME);
                int columnNumber = (int) node.getUserData(
                            LocationAwareContentHandler.COLUMN_NUMBER_KEY_NAME);

                GraphService<XmlTypeReferenceModel> fileLocationService = new GraphService<>(
                            event.getGraphContext(),
                            XmlTypeReferenceModel.class);
                XmlTypeReferenceModel fileLocation = fileLocationService.create();
                String sourceSnippit = XmlUtil.nodeToString(node);
                fileLocation.setSourceSnippit(sourceSnippit);
                fileLocation.setLineNumber(lineNumber);
                fileLocation.setColumnNumber(columnNumber);
                fileLocation.setLength(node.toString().length());
                fileLocation.setFile(xml);
                fileLocation.setXpath(xpathString);
                GraphService<NamespaceMetaModel> metaModelService = new GraphService<>(
                            event.getGraphContext(),
                            NamespaceMetaModel.class);
                for (Map.Entry<String, String> namespace : namespaces.entrySet())
                {
                    NamespaceMetaModel metaModel = metaModelService.create();
                    metaModel.setSchemaLocation(namespace.getKey());
                    metaModel.setSchemaLocation(namespace.getValue());
                    metaModel.addXmlResource(xml);
                    fileLocation.addNamespace(metaModel);
                }
                resultLocations.add(fileLocation);

                evaluationStrategy.modelSubmissionRejected();
                evaluationStrategy.modelMatched();

                for (Map.Entry<String, String> entry : paramMatchCache.getVariables().entrySet())
                {
                    Parameter<?> param = store.get(entry.getKey());
                    String value = entry.getValue();
                    if (!evaluationStrategy.submitValue(param, value))
                    {
                        evaluationStrategy.modelSubmissionRejected();
                        return false;
                    }
                }
                evaluationStrategy.modelSubmitted(fileLocation);
                evaluationStrategy.modelMatched();
            }

            return true;
        }
    }
}
