package de.uniba.dsg.bpmnspector.refcheck;

import api.*;
import de.uniba.dsg.bpmnspector.refcheck.utils.ViolationMessageCreator;
import org.jdom2.Element;
import org.jdom2.Namespace;
import org.jdom2.xpath.XPathHelper;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class ReferenceChecker {

    public static final String CONSTRAINT_REF_TYPE = "REF_TYPE";
    public static final String CONSTRAINT_REF_EXISTENCE = "REF_EXISTENCE";

    private final Map<String, BPMNElement> bpmnRefElements;

    private static final org.slf4j.Logger LOGGER = LoggerFactory
            .getLogger(ReferenceChecker.class.getSimpleName());

    public ReferenceChecker(Map<String, BPMNElement> bpmnRefElements) {
        this.bpmnRefElements = bpmnRefElements;
    }

    /**
     * This method validates the current element against the checking reference.
     * Some already existing information will be expected as parameters.
     *
     * @param elements
     *            the elements of the root file (= <code>bpmnFile</code>)
     * @param importedElements
     *            the elements which have been defined in imported files
     * @param validationResult
     *            the validation result for adding found violations
     * @param currentElement
     *            the current element to validate
     * @param line
     *            the line of the reference in the root file for the violation
     *            message
     * @param column
     *            the column of the reference
     * @param checkingReference
     *            the reference to validate against
     * @param referencedId
     *            the referenced ID
     * @param ownPrefix
     *        the namespace prefix of the current element
     * @throws ValidationException thrown if errors occur during creation of the Violation
     */
    public void validateReferenceType(Map<String, Element> elements,
            Map<String, Map<String, Element>> importedElements,
            ValidationResult validationResult, Element currentElement, int line,
            int column, Reference checkingReference, String referencedId, String ownPrefix) throws ValidationException {
        if (checkingReference.isQname()) {
            // Check whether the QName is prefixed
            if (referencedId.contains(":")) { // reference ID is prefixed and
                // therefore probably to find in an imported file
                String[] parts = referencedId.split(":");
                String prefix = parts[0];
                String importedId = parts[1];

                String namespace = "";

                for (Namespace nsp : currentElement.getNamespacesInScope()) {
                    if (nsp.getPrefix().equals(prefix)) {
                        namespace = nsp.getURI();
                        break;
                    }
                }

                Map<String, Element> relevantImportedElements = importedElements
                        .get(namespace);

                // Checking whether the namespace is used for the root and/or the imported file
                // to determine the correct element to be checked
                if (ownPrefix.equals(prefix)) { // namespace is used for the root file
                    // check whether element with ID importedId exists in root or another file
                    if (elements.containsKey(importedId)) { // elem is in root file
                        checkTypeAndAddViolation(validationResult, line, column, currentElement, checkingReference,
                                elements.get(importedId));
                    } else if (relevantImportedElements != null
                            && relevantImportedElements.containsKey(importedId)) { // element is found in the imported file
                        checkTypeAndAddViolation(validationResult, line, column, currentElement, checkingReference,
                                relevantImportedElements.get(importedId));
                    } else {
                        createAndAddExistenceViolation(validationResult, line, column, currentElement, checkingReference);
                    }

                } else if (relevantImportedElements != null) { // NOPMD
                    // namespace is used by an imported file
                    if (relevantImportedElements.containsKey(importedId)) {
                        checkTypeAndAddViolation(validationResult, line, column, currentElement, checkingReference,
                                relevantImportedElements.get(importedId));
                    } else {
                        createAndAddExistenceViolation(validationResult, line, column, currentElement, checkingReference);
                    }
                } else {
                    // invalid namespace: not used in any file case if the namespace is not used for the root file or an
                    // imported file import does not exist or is no BPMN file (as it has to be)
                    createAndAddExistenceViolation(validationResult, line, column, currentElement, checkingReference);
                }

            } else { // no QName prefix - the elem has to be in the root file
                if (elements.containsKey(referencedId)) {
                    checkTypeAndAddViolation(validationResult, line, column, currentElement, checkingReference,
                            elements.get(referencedId));
                } else {
                    createAndAddExistenceViolation(validationResult, line, column, currentElement, checkingReference);
                }
            }

        } else { // reference uses IDREF - ref has to exist in root file
            if (elements.containsKey(referencedId)) {
                checkTypeAndAddViolation(validationResult, line, column, currentElement, checkingReference,
                        elements.get(referencedId));
            } else {
                createAndAddExistenceViolation(validationResult, line, column, currentElement, checkingReference);
            }
        }
    }



    /**
     * This method validates the type of the referenced element against the
     * checking reference and adds a violation if found.
     *
     * @param validationResult
     *            the violation list for adding found violations
     * @param line
     *            the line of the reference in the root file for the violation
     *            message
     * @param column
     *            the column of the reference
     * @param currentElement
     *            the current element to validate
     * @param checkingReference
     *            the reference to validate against
     * @param referencedElement
     *            the referenced element to validate
     * @throws ValidationException thrown if errors occur during creation of the Violation
     */
    private void checkTypeAndAddViolation(ValidationResult validationResult, int line, int column,
                                          Element currentElement, Reference checkingReference,
                                          Element referencedElement) throws ValidationException {
        boolean validType = false;
        List<String> referencedTypes = checkingReference.getTypes();
        // do not check references which are only for existence validation
        if (referencedTypes != null) {
            // find all possible types (with subtypes/children)
            List<String> types = new ArrayList<>(referencedTypes);
            boolean childfound;
            do {
                childfound = false;
                List<String> typesCopy = new ArrayList<>(types);
                for (String type : typesCopy) {
                    if (bpmnRefElements.containsKey(type)) {
                        BPMNElement bpmnElement = bpmnRefElements.get(type);
                        List<String> children = bpmnElement.getChildren();
                        if (children != null) {
                            for (String child : children) {
                                if (!typesCopy.contains(child)) {
                                    types.add(child);
                                    childfound = true;
                                }
                            }
                        }
                    }
                }
            } while (childfound);
            // validate if the referenced element has one of the correct types
            for (String type : types) {
                if (referencedElement.getName().equals(type)) {
                    validType = true;
                    break;
                }
            }

            if (!validType) {
                createAndAddReferenceTypeViolation(validationResult, line, column,
                        currentElement, checkingReference, referencedElement,
                        types);
            }
        }
    }

    /**
     * Creates and adds a found reference type violation to the list of
     * violations.
     *
     * @param validationResult
     *            the validationResult for adding the found violation
     * @param line
     *            the line of the reference in the root file
     * @param column
     *            the column of the reference
     * @param currentElement
     *            the element causing the violation
     * @param checkingReference
     *            the violated reference
     * @param referencedElement
     *            the actually referenced element
     * @param types
     *            a list of allowed reference types
     * @throws ValidationException thrown if the Validation could not be created
     */
    private void createAndAddReferenceTypeViolation(ValidationResult validationResult, int line, int column,
            Element currentElement, Reference checkingReference, Element referencedElement, List<String> types) throws ValidationException {

        String message = ViolationMessageCreator.createTypeViolationMessage(currentElement.getName(), line,
                checkingReference.getName(), referencedElement.getName(), types.toString());
        Violation violation = new Violation(createLocation(line, column, currentElement), message, CONSTRAINT_REF_TYPE);
        validationResult.addViolation(violation);
    }

    /**
     * Adds a found existence violation to the list.
     *
     * @param validationResult
     *            the validation result for adding the found violation
     * @param line
     *            the line of the reference in the root file
     * @param column
     *            the column of the reference in the root file
     * @param currentElement
     *            the element causing the violation
     * @param checkingReference
     *            the violated reference
     * @throws ValidationException thrown if the Validation could not be created
     */
    public void createAndAddExistenceViolation(ValidationResult validationResult,
                                               int line, int column, Element currentElement,
                                               Reference checkingReference) throws ValidationException {
        String message = ViolationMessageCreator
                .createExistenceViolationMessage(currentElement.getName(), checkingReference.getName(), line,
                        ViolationMessageCreator.DEFAULT_MSG, XPathHelper.getAbsolutePath(currentElement));
        Violation violation = new Violation(createLocation(line, column, currentElement), message, CONSTRAINT_REF_EXISTENCE);
        validationResult.addViolation(violation);
    }

    private Location createLocation(int line, int column, Element currentElement) throws ValidationException {
        try {
            URI baseUri = currentElement.getXMLBaseURI();
            Path locationPath = Paths.get(baseUri);
            return new Location(locationPath,
                    new LocationCoordinate(line, column));
        } catch (URISyntaxException e) {
            throw new ValidationException("Base URI of current element " + currentElement.getName()
                    + " could not be restored.", e);
        }
    }
}
