package eu.dnetlib.validator2.validation.guideline;

import eu.dnetlib.validator2.engine.Helper;
import eu.dnetlib.validator2.engine.Predicates;
import eu.dnetlib.validator2.engine.Rule;
import org.w3c.dom.Document;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class Builders {

    @Deprecated
    public static ElementSpecBuilder forElement(String elementName) {
        return new ElementSpecBuilder(elementName);
    }

    public static ElementSpecBuilder forOptionalElement(String elementName) {
        return new ElementSpecBuilder(elementName).optional();
    }

    public static ElementSpecBuilder forOptionalRepeatableElement(String elementName) {
        return new ElementSpecBuilder(elementName).optionalRepeatable();
    }

    public static ElementSpecBuilder forRecommendedElement(String elementName) {
        return new ElementSpecBuilder(elementName).recommended();
    }

    public static ElementSpecBuilder forRecommendedRepeatableElement(String elementName) {
        return new ElementSpecBuilder(elementName).recommendedRepeatable();
    }

    public static ElementSpecBuilder forMandatoryElement(String elementName, Cardinality cardinality) {
        return new ElementSpecBuilder(elementName).mandatory(cardinality);
    }

    public static <T> ElementSpecBuilder forMandatoryIfApplicableElement(String elementName,
                                                                         Cardinality cardinality,
                                                                         Rule<Document> applicabilityRule) {
        return new ElementSpecBuilder(elementName).mandatoryIfApplicable(cardinality, applicabilityRule);
    }

    @Deprecated
    public static ElementSpecBuilder forElement(String elementName,
                                                RequirementLevel requirementLevel,
                                                Cardinality cardinality) {
        return Helper.buildElement(elementName, requirementLevel, cardinality);
    }

    @Deprecated
    public static ElementSpecBuilder forElement(String elementName,
                                                RequirementLevel requirementLevel,
                                                Cardinality cardinality,
                                                String... allowedValues) {
        return Helper.buildElement(elementName, requirementLevel, cardinality).allowedValues(allowedValues);
    }

    @Deprecated
    public static ElementSpecBuilder forElement(String elementName,
                                                RequirementLevel requirementLevel,
                                                Cardinality cardinality,
                                                Predicate<String> allowedValuesPredicate) {
        return Helper.buildElement(elementName, requirementLevel, cardinality).allowedValues(allowedValuesPredicate);
    }

    static final Predicate<String> ALLOW_ALL_VALUES = (String value) -> true;

    private static class DefaultNodeSpec implements NodeSpec {

        private final String name;
        private final RequirementLevel requirementLevel;
        private final Cardinality cardinality;
        private final Predicate<String> allowedValuesPredicate;
        private final Rule<Document> applicabilityRule;

        DefaultNodeSpec(String name,
                        RequirementLevel requirementLevel,
                        Cardinality cardinality,
                        Predicate<String> allowedValuesPredicate,
                        Rule<Document> applicabilityRule) {
            this.name = name;
            this.requirementLevel = requirementLevel;
            this.cardinality = cardinality;
            this.allowedValuesPredicate = allowedValuesPredicate;
            this.applicabilityRule = applicabilityRule;
        }

        @Override
        public String nodeName() {
            return name;
        }

        @Override
        public RequirementLevel requirementLevel() {
            return requirementLevel;
        }

        @Override
        public Cardinality cardinality() {
            return cardinality;
        }

        @Override
        public Predicate<String> allowedValuesPredicate() {
            return allowedValuesPredicate;
        }

        @Override
        public Rule<Document> applicabilityRule() {
            return applicabilityRule;
        }
    }

    private static class DefaultAttributeSpec extends DefaultNodeSpec implements AttributeSpec {

        DefaultAttributeSpec(String name,
                             RequirementLevel requirementLevel,
                             Predicate<String> allowedValuesPredicate,
                             Rule<Document> applicabilityRule) {
            super(name, requirementLevel, Cardinality.ONE, allowedValuesPredicate, applicabilityRule);
        }
    }

    private static class DefaultElementSpec extends DefaultNodeSpec implements ElementSpec {

        private final Set<String> parents;
        private final Set<AttributeSpec> attributeSpecs;
        private final Set<ElementSpec> subElementSpecs;
        private final String valuePrefix;
        private final ElementPosition elementPosition;

        DefaultElementSpec(Set<String> parents,
                           String name,
                           RequirementLevel requirementLevel,
                           Cardinality cardinality,
                           Predicate<String> allowedValuesPredicate,
                           Rule<Document> applicabilityRule,
                           ElementPosition elementPosition,
                           String valuePrefix,
                           Set<AttributeSpec> attributeSpecs,
                           Set<ElementSpec> subElementSpecs) {
            super(name, requirementLevel, cardinality, allowedValuesPredicate, applicabilityRule);
            this.parents = parents;
            this.elementPosition = elementPosition;
            this.valuePrefix = valuePrefix;
            this.attributeSpecs = attributeSpecs;
            this.subElementSpecs = subElementSpecs;
        }

        @Override
        public Set<String> parents() {
            return parents;
        }

        @Override
        public Set<ElementSpec> subElementSpecs() {
            return subElementSpecs;
        }

        @Override
        public Set<AttributeSpec> attributeSpecs() {
            return attributeSpecs;
        }

        @Override
        public String valuePrefix() {
            return valuePrefix;
        }

        @Override
        public ElementPosition position() {
            return elementPosition;
        }
    }

    public static class ElementSpecBuilder {

        private final String elementName;
        private String[] parents;
        private RequirementLevel elementRequirementLevel;
        private Cardinality elementCardinality;
        private ElementPosition elementPosition;
        private String valuePrefix;
        private Predicate<String> allowedValuesPredicate = ALLOW_ALL_VALUES;
        private Set<AttributeSpec> attributeSpecs = new LinkedHashSet<>();
        private Set<ElementSpecBuilder> subElementSpecs = new LinkedHashSet<>();
        private Rule<Document> applicabilityRule;

        private ElementSpecBuilder(String elementName) {
            this.elementName = elementName;
        }

        private ElementSpecBuilder with(RequirementLevel requirementLevel, Cardinality cardinality) {
            elementRequirementLevel = requirementLevel;
            elementCardinality = cardinality;
            return this;
        }

        public ElementSpecBuilder inContext(String... parentElementNames) {
            this.parents = parentElementNames;
            return this;
        }

        private ElementSpecBuilder optional() {
            return with(RequirementLevel.OPTIONAL, Cardinality.ONE);
        }

        private ElementSpecBuilder optionalRepeatable() {
            return with(RequirementLevel.OPTIONAL, Cardinality.ONE_TO_N);
        }

        private ElementSpecBuilder recommended() {
            return with(RequirementLevel.RECOMMENDED, Cardinality.ONE);
        }

        private ElementSpecBuilder recommendedRepeatable() {
            return with(RequirementLevel.RECOMMENDED, Cardinality.ONE_TO_N);
        }

        private ElementSpecBuilder mandatory(Cardinality cardinality) {
            return with(RequirementLevel.MANDATORY, cardinality);
        }

        private ElementSpecBuilder mandatoryIfApplicable(Cardinality cardinality, Rule<Document> applicabilityRule) {
            this.applicabilityRule = applicabilityRule;
            return with(RequirementLevel.MANDATORY_IF_APPLICABLE, cardinality);
        }

        public ElementSpecBuilder allowedValues(String... allowedValues) {
            allowedValuesPredicate = allowedValuesPredicateFor("Element", allowedValues);
            return this;
        }

        public ElementSpecBuilder allowedValues(Predicate<String> allowedValuesPredicate) {
            this.allowedValuesPredicate = allowedValuesPredicate;
            return this;
        }

        public ElementSpecBuilder atPosition(ElementPosition elementOccurrence) {
            this.elementPosition = elementOccurrence;
            return this;
        }

        public ElementSpecBuilder valueMustStartWith(String prefix) {
            String canonical = Helper.canonicalize(prefix);
            if (canonical.isEmpty()) {
                throw new IllegalArgumentException("Prefix cannot be empty");
            }
            this.valuePrefix = canonical;
            return this;
        }

        public ElementSpecBuilder withOptionalAttribute(String attributeName) {
            return withAttribute(attributeName, RequirementLevel.OPTIONAL, ALLOW_ALL_VALUES);
        }

        public ElementSpecBuilder withOptionalAttribute(String attributeName, String... allowedValues) {
            return withAttribute(attributeName, RequirementLevel.OPTIONAL, allowedValues);
        }

        public ElementSpecBuilder withOptionalAttribute(String attributeName, Predicate<String> allowedValuesPredicate) {
            return withAttribute(attributeName, RequirementLevel.OPTIONAL, allowedValuesPredicate);
        }

        public ElementSpecBuilder withRecommendedAttribute(String attributeName) {
            return withAttribute(attributeName, RequirementLevel.RECOMMENDED, ALLOW_ALL_VALUES);
        }

        public ElementSpecBuilder withRecommendedAttribute(String attributeName, String... allowedValues) {
            return withAttribute(attributeName, RequirementLevel.RECOMMENDED, allowedValues);
        }

        public ElementSpecBuilder withRecommendedAttribute(String attributeName, Predicate<String> allowedValuesPredicate) {
            return withAttribute(attributeName, RequirementLevel.RECOMMENDED, allowedValuesPredicate);
        }

        public ElementSpecBuilder withMandatoryAttribute(String attributeName) {
            return withAttribute(attributeName, RequirementLevel.MANDATORY, ALLOW_ALL_VALUES);
        }

        public ElementSpecBuilder withMandatoryAttribute(String attributeName, String... allowedValues) {
            return withAttribute(attributeName, RequirementLevel.MANDATORY, allowedValues);
        }

        public ElementSpecBuilder withMandatoryAttribute(String attributeName, Predicate<String> allowedValuesPredicate) {
            return withAttribute(attributeName, RequirementLevel.MANDATORY, allowedValuesPredicate);
        }

        public ElementSpecBuilder withMandatoryIfApplicableAttribute(String attributeName,
                                                                     Rule<Document> applicabilityRule) {
            return withAttribute(attributeName, RequirementLevel.MANDATORY_IF_APPLICABLE, ALLOW_ALL_VALUES, applicabilityRule);
        }

        public ElementSpecBuilder withMandatoryIfApplicableAttribute(String attributeName,
                                                                     Rule<Document> applicabilityRule,
                                                                     String... allowedValues) {
            return withAttribute(attributeName, RequirementLevel.MANDATORY_IF_APPLICABLE, allowedValuesPredicateFor("Attribute", allowedValues), applicabilityRule);
        }

        public ElementSpecBuilder withMandatoryIfApplicableAttribute(String attributeName,
                                                                     Rule<Document> applicabilityRule,
                                                                     Predicate<String> allowedValuesPredicate) {
            return withAttribute(attributeName, RequirementLevel.MANDATORY_IF_APPLICABLE, allowedValuesPredicate, applicabilityRule);
        }

        public ElementSpecBuilder withSubElement(ElementSpecBuilder subElementSpecBuilder) {
            subElementSpecs.add(subElementSpecBuilder);
            return this;
        }

        public ElementSpec build() {

            String canonical = Helper.ensureNonEmpty(elementName,
                    () -> new IllegalStateException("Element name cannot be empty"));

            validate("Element", elementName, elementRequirementLevel, elementCardinality, allowedValuesPredicate, applicabilityRule);

            Set<String> setOfParents = parents == null ?
                    Collections.emptySet() :
                    Stream.of(parents).filter( p -> !Helper.isEmpty(p)).collect(Collectors.toSet());

            if (elementPosition == null) {
                elementPosition = ElementPosition.ALL;
            }

            return new DefaultElementSpec(
                    setOfParents,
                    canonical,
                    elementRequirementLevel,
                    elementCardinality,
                    allowedValuesPredicate,
                    applicabilityRule,
                    elementPosition,
                    valuePrefix,
                    attributeSpecs,
                    subElementSpecs.stream().map(ElementSpecBuilder::build).collect(Collectors.toCollection(LinkedHashSet::new))
            );
        }

        private ElementSpecBuilder withAttribute(String attrName,
                                                 RequirementLevel requirementLevel) {
            return withAttribute(attrName, requirementLevel, ALLOW_ALL_VALUES, null);
        }

        private ElementSpecBuilder withAttribute(String attrName,
                                                 RequirementLevel requirementLevel,
                                                 String... allowedValues) {
            return withAttribute(attrName, requirementLevel, allowedValuesPredicateFor("Attribute", allowedValues), null);
        }

        private ElementSpecBuilder withAttribute(String attrName,
                                                 RequirementLevel requirementLevel,
                                                 Predicate<String> allowedValuesPredicate) {
            return withAttribute(attrName, requirementLevel, allowedValuesPredicate, null);
        }

        private ElementSpecBuilder withAttribute(String attrName,
                                                 RequirementLevel requirementLevel,
                                                 Predicate<String> allowedValuesPredicate,
                                                 Rule<Document> applicabilityRule) {
            String canonical = Helper.ensureNonEmpty(attrName,
                                                     () -> new IllegalStateException("Attribute name cannot be empty"));
            validate("Attribute", attrName, requirementLevel, allowedValuesPredicate, applicabilityRule);
            attributeSpecs.add(new DefaultAttributeSpec(canonical, requirementLevel, allowedValuesPredicate, applicabilityRule));
            return this;
        }
    }

    private static Predicate<String> allowedValuesPredicateFor(String type, String... allowedValues) {
        if (allowedValues == null || allowedValues.length == 0) {
            throw new IllegalArgumentException(type + " allowed values cannot be empty");
        }
        //TODO: Confirm that ignoring case is the right thing to do.
        Predicates.SetOfCaseInsensitiveAllowedValues setOfAllowedValues =
                new Predicates.SetOfCaseInsensitiveAllowedValues(allowedValues);
        if (setOfAllowedValues.isEmpty()) {
            throw new IllegalArgumentException(type + " allowed values cannot be empty");
        }
        return setOfAllowedValues;
    }

    private static void validate(String type,
                                 String name,
                                 RequirementLevel requirementLevel,
                                 Predicate<String> allowedValuesPredicate,
                                 Rule<Document> applicabilityRule) {
        if (requirementLevel == null) {
            throw new IllegalStateException(type + ":" + name + " requirement level cannot be empty");
        }

        if (allowedValuesPredicate == null) {
            throw new IllegalStateException(type + ":" + name + " allowed values predicate cannot be empty");
        }

        validate(type, name, requirementLevel, applicabilityRule);
    }

    private static void validate(String type,
                                 String name,
                                 RequirementLevel requirementLevel,
                                 Cardinality cardinality,
                                 Predicate<String> allowedValuesPredicate,
                                 Rule<Document> applicabilityRule) {
        validate(type, name, requirementLevel, allowedValuesPredicate, applicabilityRule);

        if (cardinality == null) {
            throw new IllegalStateException(type + ":" + name + " cardinality cannot be empty");
        }
    }

    private static void validate(String type, String name, RequirementLevel requirementLevel, Rule<Document> applicabilityRule) {
        if (requirementLevel == RequirementLevel.MANDATORY_IF_APPLICABLE &&
                applicabilityRule == null) {
            throw new IllegalStateException(type + ": " + name + " is invalid: required applicability rule is missing");
        }
        if (requirementLevel != RequirementLevel.MANDATORY_IF_APPLICABLE &&
                applicabilityRule != null) {
            throw new IllegalStateException(type + ": " + name + " is invalid: an applicability rule is present");
        }

    }


}
