package de.uniba.dsg.bpmnspector.schematron.preprocessing;

import de.uniba.dsg.bpmnspector.common.importer.BPMNProcess;
import de.uniba.dsg.bpmnspector.common.util.ConstantHelper;
import de.uniba.dsg.bpmnspector.refcheck.utils.JDOMUtils;
import org.jdom2.Attribute;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.filter.Filters;
import org.jdom2.xpath.XPathExpression;
import org.jdom2.xpath.XPathFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * Does the preprocessing step for creating only one document contenting
 * everything and a namespace table
 *
 * @author Philipp Neugebauer
 * @author Matthias Geiger
 * @version 1.0
 *
 */
public class PreProcessor {

    private static final Logger LOGGER;

    private static final String ALL_QNAME_ATTRIBUTES =  "//bpmn:*/@sourceRef | //bpmn:*/@targetRef | "
            + "//bpmn:*/@calledElement | //bpmn:*/@processRef | "
            + "//bpmn:*/@dataStoreRef | //bpmn:*/@categoryValueRef | //bpmn:*/@messageRef | "
            + "//bpmn:*/@correlationKeyRef | //bpmn:*/@correlationPropertyRef | //bpmn:*/@structureRef |"
            + "//bpmn:*/@evaluatesToTypeRef | //bpmn:*/@itemRef | //bpmn:*/@type | "
            + "//bpmn:*/@definitionalCollaborationRef | //bpmn:*/@parameterRef | //bpmn:*/@operationRef |"
            + "//bpmn:*/@calledElement | //bpmn:*/@itemSubjectRef | //bpmn:*/@dataStoreRef | "
            + "//bpmn:*/@attachedToRef | //bpmn:*/@activityRef | //bpmn:*/@errorRef | //bpmn:*/@escalationRef | "
            + "//bpmn:*/@source | //bpmn:*/@target | //bpmn:*/@signalRef | //bpmn:*/@partitionElementRef";

    private static final String ALL_IDREF_ATTRIBUTES = "//bpmn:*/@sourceRef | //bpmn:*/@targetRef | //bpmn:*/@default |"
            + "//bpmn:*/@inputDataRef | //bpmn:*/@outputDataRef | //bpmn:*/@dataObjectRef";

    private static final String ALL_QNAME_ELEMENTS = "//bpmn:eventDefinitionRef | "
            + "//bpmn:incoming | //bpmn:outgoing | //bpmn:dataInputRefs | "
            + "//bpmn:dataOutputRefs | //bpmn:correlationPropertyRef | //bpmn:categoryValueRef |"
            + "//bpmn:inMessageRef | //bpmn:outMessageRef | //bpmn:errorRef | //bpmn:choreographyRef |"
            + "//bpmn:interfaceRef | //bpmn:endPointRef | //bpmn:participantRef | //bpmn:innerParticipantRef |"
            + "//bpmn:outerParticipantRef | //bpmn:supports | //bpmn:resourceRef | //bpmn:supportedInterfaceRefs |"
            + "//bpmn:loopDataInputRef | //bpmn:loopDataOutputRef | //bpmn:oneBehaviorEventRef |"
            + "//bpmn:noneBehaviorEventRef";

    private static final String ALL_IDREF_ELEMENTS = "//bpmn:dataInputRefs | "
            + "//bpmn:optionalInputRefs | //bpmn:whileExecutingInputRefs | //bpmn:outputSetRefs | "
                    + "//bpmn:dataOutputRefs | //bpmn:optionalOutputRefs | //bpmn:whileExecutingOutputRefs | "
                    + "//bpmn:inputSetRefs | //bpmn:sourceRef | //bpmn:targetRef | //bpmn:flowNodeRef";

    private final XPathFactory xPathFactory = XPathFactory.instance();

    static {
        LOGGER = LoggerFactory.getLogger(PreProcessor.class.getSimpleName());
    }

    /**
     *
     * Performs the pre-processing step for creating one document including the
     * content of all imported BPMN processes of the document
     *
     * @param process
     *          the BPMNProcess which should be used as starting point
     * @return  a cloned org.jdom2.Document representation of the BPMN process
     *          including all imported nodes
     */
    public Document preProcess(BPMNProcess process) {
        LOGGER.info("Starting to preprocess file: " + process.getBaseURI());

        Document cloneOfDoc = process.getProcessAsDoc().clone();

        // Preprocessing can be skipped if no files are imported and there is no Prefix used for the targetNamespace
        if(process.getChildren().isEmpty() && !JDOMUtils.getUsedPrefixForTargetNamespace(cloneOfDoc).isPresent()) {
            LOGGER.info("Skipping preprocessing.");
            return cloneOfDoc;
        }

        // get all nodes which potentially refer to other files
        List<Attribute> refAttributes = createXPathExpressionForAllQNameAttributes().evaluate(cloneOfDoc);
        List<Element> refElements = createXPathExpressionForAllQNameElements().evaluate(cloneOfDoc);

        // for each attribute: rename IDs which can be used globally
        refAttributes.stream()
                .filter(attribute -> attribute.getValue().contains(":"))
                .forEach(attribute -> renameGlobalIds(process, attribute));

        // for each element: rename IDs which can be used globally
        refElements.stream().filter(element -> element.getText().contains(":"))
                .forEach(element -> renameGlobalIds(process, element));

        // for each import: perform further processing
        for(BPMNProcess imported : process.getChildren()){
            // rename IDs in imported file
            renameIds(imported);

            LOGGER.debug("Checking imported importedProcess for further imports.");
            Document result = preProcess(imported);

            LOGGER.debug("integration of document will be done now");

            addNodesToDocument(result, cloneOfDoc);
        }

        LOGGER.info("Preprocessing of file {} completed.", process.getBaseURI());

        return cloneOfDoc;
    }

    /**
     * helper method to rename all global ids in the bpmn files to be able to
     * merge them into one file and to detect later the errors in the right
     * files. the colon is replaced by an underscore because ids in the one
     * merged file can't contain colons
     *
     * @param process
     *            the document which should be validated
     * @param attribute
     *            the attribute with the ID to be renamed
     */
    private void renameGlobalIds(BPMNProcess process, Attribute attribute) {
        String prefix = attribute.getValue().substring(0,
                attribute.getValue().indexOf(":"));
        String newPrefix;
        // special treatment if a prefix is used for the local targetNamespace
        if(prefix.equals(JDOMUtils.getUsedPrefixForTargetNamespace(process.getProcessAsDoc()).orElse(""))) {
            // replaces usage of 'tns:ID' with 'ID'
            newPrefix = "";
        } else {
            newPrefix = getNewPrefixByOldPrefix(process, prefix)+"_";
        }
        String newId = attribute.getValue().replace(prefix + ":", newPrefix);
        LOGGER.debug("new prefix '{}' for ID '{}' - new ID: {}", newPrefix, attribute.getValue(), newId);
        attribute.setValue(newId);
    }

    /**
     * helper method to rename all global ids in the bpmn files to be able to
     * merge them into one file and to detect later the errors in the right
     * files. the colon is replaced by an underscore because ids in the one
     * merged file can't contain colons
     *
     * @param process
     *            the document which should be validated
     * @param element
     *            the element with the ID to be renamed
     */
    private void renameGlobalIds(BPMNProcess process, Element element) {
        String prefix = element.getText().substring(0,
                element.getText().indexOf(":"));
        String newPrefix;
        // special treatment if a prefix is used for the local targetNamespace
        if(prefix.equals(JDOMUtils.getUsedPrefixForTargetNamespace(process.getProcessAsDoc()).orElse(""))) {
            // replaces usage of 'tns:ID' with 'ID'
            newPrefix = "";
        } else {
            newPrefix = getNewPrefixByOldPrefix(process, prefix)+"_";
        }
        String newId = element.getText().replace(prefix + ":", newPrefix);
        LOGGER.debug("new prefix '{}' for ID '{}' - new ID: {}", newPrefix, element.getText(), newId);
        element.setText(newId);

    }

    private String getNewPrefixByOldPrefix(BPMNProcess process,
            String oldPrefix) {
        Namespace namespace = process.getProcessAsDoc().getRootElement().getNamespace(
                oldPrefix);
        String nspURI = "";
        if(namespace!=null) {
            nspURI = namespace.getURI();
        }
        String newPrefix = "";
        for (BPMNProcess imported : process.getChildren()) {
            if (nspURI.equals(imported.getNamespace())) {
                newPrefix = imported.getGeneratedPrefix();
            }
        }
        return newPrefix;
    }


    /**
     * adds to all nodes new and unique prefixes in the given document for the
     * validation process and violation searching
     *
     * @param importedProcess the process in which the ids should be changed
     */
    private void renameIds(BPMNProcess importedProcess) {

        List<Attribute> attributes = createXPathExpressionForAllIDsAndReferenceAttributes().evaluate(importedProcess.getProcessAsDoc());
        List<Element> elements = createXPathExpressionForAllReferenceElements().evaluate(importedProcess.getProcessAsDoc());

        attributes.stream().filter(x -> !x.getValue().contains(":"))
                .forEach(x -> {
                    LOGGER.debug("Updating attribute ID '{}' new value: {}", x.getValue(), importedProcess.getGeneratedPrefix()+"_"+x.getValue());
                    x.setValue(importedProcess.getGeneratedPrefix()+"_"+x.getValue());
                });
        elements.stream().filter(x-> !x.getText().contains(":"))
                .forEach(x -> {
                    LOGGER.debug("Updating element {} ID '{}' new value: {}", x.getName(), x.getText(), importedProcess.getGeneratedPrefix()+"_"+x.getText());
                    x.setText(importedProcess.getGeneratedPrefix()+"_"+x.getText());
                });
    }

    /**
     * adds the children of importDefinitionsNode to the definitionsNode of the
     * given headFileDocument
     *
     * @param importedProcess
     *            the definitionsNode of the document, which should be added to
     *            the headFileDocument
     * @param headDocument
     *            the document, where the nodes should be added
     *
     */
    private void addNodesToDocument(Document importedProcess,
            Document headDocument) {
        Element definitionsHead = headDocument.getRootElement();

        Element definitionsImported = importedProcess.getRootElement();

        for(Element element : definitionsImported.getChildren()) {
            Element clone = element.clone();
            definitionsHead.addContent(clone);
        }
    }

    /**
     * Creates an XPathExpression filtering all Attributes which are used to reference another BPMN element
     */
    private XPathExpression<Attribute> createXPathExpressionForAllQNameAttributes() {

        return xPathFactory.compile(ALL_QNAME_ATTRIBUTES, Filters.attribute(), null,
                ConstantHelper.BPMN_NAMESPACE);
    }

    /**
     * Creates an XPathExpression filtering all Elements which are used to reference another BPMN element
     */
    private XPathExpression<Element> createXPathExpressionForAllQNameElements() {

        return xPathFactory.compile(ALL_QNAME_ELEMENTS, Filters.element(), null,
                ConstantHelper.BPMN_NAMESPACE);
    }
    

    /**
     * Creates an XPathExpression filtering all Attributes which define and use IDs
     * (regardless of the used type (ID, IDREF or QName))
     */
    private XPathExpression<Attribute> createXPathExpressionForAllIDsAndReferenceAttributes() {

        String expStr = "//bpmn:*/@id | " + ALL_IDREF_ATTRIBUTES +" | "+ ALL_QNAME_ATTRIBUTES;

        return xPathFactory.compile(expStr, Filters.attribute(), null,
                ConstantHelper.BPMN_NAMESPACE);
    }

    /**
     * Creates an XPathExpression filtering all Elements which use IDs
     * (regardless of the used type (ID, IDREF or QName))
     */
    private XPathExpression<Element> createXPathExpressionForAllReferenceElements() {

        String expStr = ALL_IDREF_ELEMENTS + " | " + ALL_QNAME_ELEMENTS;

        return xPathFactory.compile(expStr, Filters.element(), null,
                ConstantHelper.BPMN_NAMESPACE);
    }


}
