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

import org.jboss.forge.furnace.util.Assert;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.Variables;
import org.jboss.windup.config.condition.EvaluationStrategy;
import org.jboss.windup.config.parameters.FrameContext;
import org.jboss.windup.config.parameters.FrameCreationContext;
import org.jboss.windup.config.parameters.ParameterizedGraphCondition;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.model.FileReferenceModel;
import org.jboss.windup.graph.model.WindupVertexFrame;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.rules.apps.xml.condition.validators.XmlCacheValidator;
import org.jboss.windup.rules.apps.xml.condition.validators.XmlFileDtdValidator;
import org.jboss.windup.rules.apps.xml.condition.validators.XmlFileNameValidator;
import org.jboss.windup.rules.apps.xml.condition.validators.XmlFileXpathValidator;
import org.jboss.windup.rules.apps.xml.model.XmlFileModel;
import org.jboss.windup.rules.apps.xml.model.XmlTypeReferenceModel;
import org.jboss.windup.util.ExecutionStatistics;
import org.jboss.windup.util.exception.WindupException;
import org.ocpsoft.rewrite.config.Condition;
import org.ocpsoft.rewrite.config.ConditionBuilder;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.param.DefaultParameterValueStore;
import org.ocpsoft.rewrite.param.Parameter;
import org.ocpsoft.rewrite.param.ParameterStore;
import org.ocpsoft.rewrite.param.ParameterValueStore;
import org.ocpsoft.rewrite.param.RegexParameterizedPatternParser;
import org.ocpsoft.rewrite.util.Maps;

import javax.xml.xpath.XPathExpression;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Handles matching on {@link XmlFileModel} objects and creating/returning {@link XmlTypeReferenceModel} or {@link XmlFileModel} objects on the matching nodes/whole files.
 */
public class XmlFile extends ParameterizedGraphCondition implements XmlFileDTD, XmlFileIn, XmlFileNamespace, XmlFileResult, XmlFileXpath
{
    public XmlFile() {

    }

    XmlCacheValidator cacheValidator= new XmlCacheValidator();
    XmlFileNameValidator fileNameValidator= new XmlFileNameValidator();
    XmlFileDtdValidator dtdValidator = new XmlFileDtdValidator();
    XmlFileXpathValidator xpathValidator = new XmlFileXpathValidator();


    public void setXpathResultMatch(String xpathResultMatch)
    {
        xpathValidator.setXpathResult(xpathResultMatch);
    }

    private XmlFile(String xpath)
    {
        setXpath(xpath);
    }

    public String getXpathString()
    {
        return xpathValidator.getXpathString();
    }

    /**
     * Create a new {@link XmlFile} {@link Condition}.
     */
    public static XmlFileXpath matchesXpath(String xpath)
    {
        return new XmlFile(xpath);
    }

    public XmlFileDTD andDTDPublicId(String publicIdRegex)
    {
        this.dtdValidator.setPublicId(publicIdRegex);
        return this;
    }

    /**
     * Create a new {@link XmlFile} that matches on the provided DTD namespace.
     */
    public static XmlFileDTD withDTDSystemId(String dtdNamespace)
    {
        XmlFile xmlFile = new XmlFile();
        xmlFile.dtdValidator.setSystemId(dtdNamespace);
        return xmlFile;
    }

    public XmlFileDTD andDTDSystemId(String dtdNamespace)
    {
        dtdValidator.setSystemId(dtdNamespace);
        return this;
    }

    /**
     * Output the results using the provided variable name.
     */
    public ConditionBuilder as(String variable)
    {
        Assert.notNull(variable, "Variable name must not be null.");
        this.setOutputVariablesName(variable);
        return this;
    }

    /**
     * Scan only files that match the given file name.
     */
    public XmlFileIn inFile(String fileName)
    {
        this.fileNameValidator.setFileNameRegex(fileName);
        return this;
    }

    /**
     * Only return results that match the given regex.
     */
    public XmlFileResult resultMatches(String regex)
    {
        this.xpathValidator.setXpathResult(regex);
        return this;
    }

    public XPathExpression getXPathExpression()
    {
        return this.xpathValidator.getXpathExpression();
    }

    public RegexParameterizedPatternParser getInFilePattern()
    {
        return this.fileNameValidator.getFileNamePattern();
    }

    public String getPublicId()
    {
        return this.dtdValidator.getPublicId();
    }

    /**
     * Specify the name of the variables to base this query on.
     *
     * @param fromVariable
     * @return
     */
    public static XmlFileFrom from(String fromVariable)
    {
        return new XmlFileFrom(fromVariable);
    }

    @Override
    public void setParameterStore(ParameterStore store)
    {
        this.xpathValidator.setParameterStore(store);
    }

    @Override
    public Set<String> getRequiredParameterNames()
    {
        Set<String> result = new HashSet<>();
        result.addAll(xpathValidator.getRequiredParamaterNames());
        result.addAll(fileNameValidator.getRequiredParamaterNames());
        return result;
    }

    @Override
    protected String getVarname()
    {
        return getOutputVariablesName();
    }

    @Override
    @SuppressWarnings("unchecked")
    protected boolean evaluateAndPopulateValueStores(final GraphRewrite event, final EvaluationContext context,
                final FrameCreationContext frameCreationContext)
    {
        return evaluate(event, context, new XmlFileEvaluationStrategy()
        {
            private LinkedHashMap<String, List<WindupVertexFrame>> variables;

            @Override
            public void modelMatched()
            {
                this.variables = new LinkedHashMap<>();
                frameCreationContext.beginNew((Map) this.variables);
            }

            @Override
            @SuppressWarnings("rawtypes")
            public void modelSubmitted(WindupVertexFrame model)
            {
                Maps.addListValue(this.variables, getVarname(), model);
            }

            @Override
            public boolean submitValue(Parameter<?> parameter, String value)
            {
                ParameterValueStore valueStore = DefaultParameterValueStore.getInstance(context);
                return valueStore.submit(event, context, parameter, value);
            }

            @Override
            public void modelSubmissionRejected()
            {
                if (variables != null)
                {
                    this.variables = null;
                    frameCreationContext.rollback();
                }
            }
        });
    }

    public interface XmlFileEvaluationStrategy extends EvaluationStrategy
    {
        boolean submitValue(Parameter<?> parameter, String value);
    }

    @Override
    protected boolean evaluateWithValueStore(final GraphRewrite event, final EvaluationContext context, final FrameContext frameContext)
    {
        boolean result = evaluate(event, context, new XmlFileEvaluationStrategy()
        {
            @Override
            public void modelMatched()
            {
            }

            @Override
            public void modelSubmitted(WindupVertexFrame model)
            {
            }

            @Override
            public boolean submitValue(Parameter<?> parameter, String value)
            {
                ParameterValueStore valueStore = DefaultParameterValueStore.getInstance(context);
                String existingValue = valueStore.retrieve(parameter);
                if (existingValue == null)
                {
                    return valueStore.submit(event, context, parameter, value);
                }
                else
                {
                    return valueStore.isValid(event, context, parameter, value);
                }
            }

            @Override
            public void modelSubmissionRejected()
            {
            }
        });

        if (!result)
            frameContext.reject();

        return result;
    }

    private boolean evaluate(final GraphRewrite event, final EvaluationContext context, final XmlFileEvaluationStrategy evaluationStrategy)
    {
        try
        {
            ExecutionStatistics.get().begin("XmlFile.evaluate");
            initValidators(event,context,evaluationStrategy);
            final List<WindupVertexFrame> finalResults = new ArrayList<>();
            final GraphContext graphContext = event.getGraphContext();
            Iterable<? extends WindupVertexFrame> startVertices = getStartingVertices(event,graphContext);
            for (WindupVertexFrame iterated : startVertices)
            {
                XmlFileModel xml = getXmlFileModelFromVertex(iterated);
                if(xmlFilePassRestrictions(event,context,xml)) {
                    registerAndSubmitResultsFor(xml, finalResults,evaluationStrategy);
                }
            }
            setResults(event, getOutputVariablesName(), finalResults);

            return !finalResults.isEmpty();
        }
        finally
        {
            ExecutionStatistics.get().end("XmlFile.evaluate");
        }
    }

    private void initValidators(GraphRewrite event, EvaluationContext context, XmlFileEvaluationStrategy evaluationStrategy)
    {
        xpathValidator.setEvaluationStrategy(evaluationStrategy);
        cacheValidator.clear();
    }

    private Iterable<? extends WindupVertexFrame> getStartingVertices(GraphRewrite event,GraphContext graphContext) {
        GraphService<XmlFileModel> xmlResourceService = new GraphService<>(graphContext, XmlFileModel.class);
        Iterable<? extends WindupVertexFrame> allXmls;
        if (getInputVariablesName() == null || getInputVariablesName().isEmpty())
        {
            allXmls = xmlResourceService.findAll();
        }
        else
        {
            allXmls = Variables.instance(event).findVariable(getInputVariablesName());
        }
        return allXmls;
    }

    private void registerAndSubmitResultsFor(XmlFileModel xml, List<WindupVertexFrame> results, EvaluationStrategy evaluationStrategy)
    {
        final List<WindupVertexFrame> xpathResults = xpathValidator.getAndClearResultLocations();
        if(xpathResults.isEmpty()) {
            evaluationStrategy.modelMatched();
            evaluationStrategy.modelSubmitted(xml);
            results.add(xml);
        } else {
            //these were already submitted by XpathValidator
            results.addAll(xpathResults);
        }
    }

    private XmlFileModel getXmlFileModelFromVertex(WindupVertexFrame vertexFrame) {
        final XmlFileModel xml;
        if (vertexFrame instanceof FileReferenceModel)
        {
            xml = (XmlFileModel) ((FileReferenceModel) vertexFrame).getFile();
        }
        else if (vertexFrame instanceof XmlFileModel)
        {
            xml = (XmlFileModel) vertexFrame;
        }
        else
        {
            throw new WindupException("XmlFile was called on the wrong graph type ( " + vertexFrame.toPrettyString()
                        + ")");
        }
        return xml;
    }

    private boolean xmlFilePassRestrictions(final GraphRewrite event,final EvaluationContext context,XmlFileModel xml) {
        boolean validation = cacheValidator.isValid(event,context,xml);
        validation = validation && fileNameValidator.isValid(event,context,xml);
        validation = validation && dtdValidator.isValid(event,context,xml);
        validation = validation && xpathValidator.isValid(event,context,xml);

        return validation;
    }

    public XmlFileNamespace namespace(String prefix, String url)
    {
        this.xpathValidator.addNamespace(prefix, url);
        return this;
    }

    public void setXpath(String xpath)
    {
        this.xpathValidator.setXpathString(xpath);

    }

    public String toString()
    {
        StringBuilder builder = new StringBuilder();
        builder.append("XmlFile");
        if (getInputVariablesName() != null)
        {
            builder.append(".inputVariable(" + getInputVariablesName() + ")");
        }
        if (xpathValidator.getXpathString() != null)
        {
            builder.append(".matches(" + xpathValidator.getXpathString() + ")");
        }
        if (fileNameValidator.getFileNamePattern() != null)
        {
            builder.append(".inFile(" + fileNameValidator.getFileNamePattern().toString() + ")");
        }
        if (dtdValidator.getPublicId() != null)
        {
            builder.append(".withDTDPublicId(" + dtdValidator.getPublicId() + ")");
        }
        builder.append(".as(" + getOutputVariablesName() + ")");
        return builder.toString();
    }



}