package com.capgemini.reports.bomlineholder.comparison;

import com.capgemini.framework.teamcenter.authent.TcSession;
import com.capgemini.framework.teamcenter.object.types.SnecmaObjectTypes;
import com.capgemini.reports.bomlineholder.BOMLineHolder;
import com.capgemini.reports.bomlineholder.BOMLineHolderHelper;
import com.capgemini.reports.exceptions.InvalidInputException;
import com.capgemini.reports.exceptions.MalformedNomenclatureException;
import com.capgemini.reports.releasestatus.EffectivityBehaviourHelper;
import com.capgemini.reports.snecmaobjects.ISnecmaObject;
import com.capgemini.reports.snecmaobjects.part.AbstractAltOrDetRevision;
import com.capgemini.reports.snecmaobjects.part.AbstractPart;
import com.capgemini.reports.snecmaobjects.part.AlternateRevision;
import com.capgemini.reports.snecmaobjects.part.DETRevision;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.util.*;

/**
 * Browse a {@link BOMLineHolder} tree, and presents methods for dealing with a coherent set of objects
 * having a strong relationship in the Bom, like Generic-RfGeneric-PartAero.<br>
 * The various abstract methods each expose such coherent sets of objects, and are called in the browsing order.
 */
@Slf4j
@Getter
public class DualBomLineBrowser {

    // Sub-components that define the behaviour of this Visitor
    private final EffectivityBehaviourHelper effectivityBehaviour;
    private final boolean doRetrievePartNumberUnderRfGeneric;
    private final boolean doVisitPartBelowPart;
    private final boolean doVisitPartsInPseudoFolder;
    private final Comparator<BOMLineHolder> bomComparator;
    private final Comparator<ISnecmaObject> itemComparator;

    @Setter(value = AccessLevel.PROTECTED)
    private IDualSnecmaLineProcessor preProcessor;
    @Setter(value = AccessLevel.PROTECTED)
    private IDualSnecmaLineProcessor processor;

    /**
     * Constructor.
     *
     * @param effectivityBehaviour             : the behaviour regarding Effectivities
     * @param visitPartsInPseudoFolder         : the behaviour regarding pseudofolders of {@link AlternateRevision} or {@link DETRevision} parts.
     * @param retrievePartNumberUnderRfGeneric boolean Take true if we retrieve the Part number under the RF Generic. Take false else.
     * @param visitPartBelowPart               tells the Visitor weather it should visit a Part that is below another Part
     * @param bomComparator                    {@link Comparator} for {@link BOMLineHolder}s, to know which bomlines are matching accross configurations
     * @param itemComparator                   {@link Comparator} for {@link ISnecmaObject}s, to know which items are matching <i>in pseudo-folders</i> accross configurations
     */
    public DualBomLineBrowser(final EffectivityBehaviourHelper effectivityBehaviour, final boolean visitPartsInPseudoFolder,
                              final boolean retrievePartNumberUnderRfGeneric, final boolean visitPartBelowPart, final Comparator<BOMLineHolder> bomComparator,
                              final Comparator<ISnecmaObject> itemComparator) {
        this.effectivityBehaviour = effectivityBehaviour;
        this.doVisitPartsInPseudoFolder = visitPartsInPseudoFolder;
        this.doRetrievePartNumberUnderRfGeneric = retrievePartNumberUnderRfGeneric;
        this.doVisitPartBelowPart = visitPartBelowPart;
        this.bomComparator = bomComparator;
        this.itemComparator = itemComparator;
        this.processor = new PassiveProcessor();
    }

    private static AbstractPart nextPartOrNull(final Iterator<AbstractPart> iterator) {
        if (!iterator.hasNext()) {
            return null;
        } else {
            return iterator.next();
        }
    }

    private static List<OptionalPartPair> getCommonChildren(final List<AbstractPart> conf1, final List<AbstractPart> conf2,
                                                            final Comparator<ISnecmaObject> itemComparator) {
        List<OptionalPartPair> pairs = new ArrayList<>();
        Iterator<AbstractPart> iterator1 = conf1.iterator();
        Iterator<AbstractPart> iterator2 = conf2.iterator();
        AbstractPart child1 = nextPartOrNull(iterator1);
        AbstractPart child2 = nextPartOrNull(iterator2);
        while (child1 != null || child2 != null) {
            if (child1 != null && child2 != null) {
                int cmp = itemComparator.compare(child1, child2);
                if (cmp == 0) {
                    pairs.add(new OptionalPartPair(child1, child2));
                    child1 = nextPartOrNull(iterator1);
                    child2 = nextPartOrNull(iterator2);
                } else if (cmp > 0) {
                    pairs.add(new OptionalPartPair(child1, null));
                    child1 = nextPartOrNull(iterator1);
                } else {
                    pairs.add(new OptionalPartPair(null, child2));
                    child2 = nextPartOrNull(iterator2);
                }
            } else if (child1 != null) {
                pairs.add(new OptionalPartPair(child1, null));
                child1 = nextPartOrNull(iterator1);
            } else {
                pairs.add(new OptionalPartPair(null, child2));
                child2 = nextPartOrNull(iterator2);
            }
        }
        return pairs;
    }

    /**
     * Visit a Top BOMLineHolder and expand all the children to retrieve the properties.
     */
    public void visitTopReceptorBom(final TcSession session, final BOMLineHolder aTopBomLineConf1, final BOMLineHolder aTopBomLineConf2)
            throws MalformedNomenclatureException {
        // Create a Specific first line
        OptionalBomPair topPair = new OptionalBomPair(aTopBomLineConf1, aTopBomLineConf2);
        preProcessor.visitTopBom(topPair);

        // Analyze the type of the SnecmaObject and visit it according to this type
        ISnecmaObject theSnecmaObject1 = aTopBomLineConf1.getSnecmaObject();
        ISnecmaObject theSnecmaObject2 = aTopBomLineConf2.getSnecmaObject();
        if (theSnecmaObject1 != null && theSnecmaObject2 != null) {
            if (theSnecmaObject1.getType().compareTo(theSnecmaObject2.getType()) != 0) {
                throw new MalformedNomenclatureException("Top BomLines have differing types");
            }
            if (theSnecmaObject1.isProduct()) {
                visitProductAsReceptor(session, topPair);
            } else if (theSnecmaObject1.isGeneric()) {
                visitGenericAsReceptor(session, topPair);
            } else if (theSnecmaObject1.isPart()) {
                visitPartAsReceptor(session, topPair);
            } else {
                throw new MalformedNomenclatureException("Unupported bom: topLine " + aTopBomLineConf1.getSnecmaObject().getItemId() +
                        " is not a Generic, a Product, or a Part. It is a " + aTopBomLineConf1.getSnecmaObject().getType());
            }
        } else {
            throw new MalformedNomenclatureException("Empty top BomLine");
        }
    }

    private void visitPartAsReceptor(final TcSession session, final OptionalBomPair aPart) {
        List<OptionalBomPair> commonChildren = getCommonChildren(aPart, bomComparator);
        // Pre - processing
        if (preProcessor != null) {

            for (OptionalBomPair child : commonChildren) {
                if (doVisitPartBelowPart && child.isPart()) {
                    preProcessor.visitPartPart(aPart, child); // Non-recursive
                }
                if (child.isRFPartOrPartAssemb()) {
                    preProcessRfPart(session, aPart, child);
                }
            }
            if (SnecmaObjectTypes.isAltOrDet(aPart.getType()) && doVisitPartsInPseudoFolder) { // Specific treatment of Alt or Det Receptor
                List<AbstractPart> pseudoFolder1;
                if (aPart.firstExists()) {
                    pseudoFolder1 = ((AbstractAltOrDetRevision) aPart.getFirst().getSnecmaObject()).getPseudoFolder();
                } else {
                    pseudoFolder1 = new ArrayList<>();
                }
                List<AbstractPart> pseudoFolder2;
                if (aPart.secondExists()) {
                    pseudoFolder2 = ((AbstractAltOrDetRevision) aPart.getSecond().getSnecmaObject()).getPseudoFolder();
                } else {
                    pseudoFolder2 = new ArrayList<>();
                }
                for (OptionalPartPair child : getCommonChildren(pseudoFolder1, pseudoFolder2, itemComparator)) {
                    // Visit the pseudo-folder
                    preProcessor.visitPartsInPseudoFolder(aPart, child);
                }
            }
        }


        // There may be other children, like some Parts, RF Parts
        for (OptionalBomPair child : commonChildren) {
            if (doVisitPartBelowPart && child.isPart()) {
                processor.visitPartPart(aPart, child);
                visitPartAsReceptor(session, child); // Recursive
            }
            if (child.isRFPartOrPartAssemb()) {
                visitRfPart(session, aPart, child);
            }
        }
        if (SnecmaObjectTypes.isAltOrDet(aPart.getType()) && doVisitPartsInPseudoFolder) { // Specific treatment of Alt or Det Receptor
            List<AbstractPart> pseudoFolder1;
            if (aPart.firstExists()) {
                pseudoFolder1 = ((AbstractAltOrDetRevision) aPart.getFirst().getSnecmaObject()).getPseudoFolder();
            } else {
                pseudoFolder1 = new ArrayList<>();
            }
            List<AbstractPart> pseudoFolder2;
            if (aPart.secondExists()) {
                pseudoFolder2 = ((AbstractAltOrDetRevision) aPart.getSecond().getSnecmaObject()).getPseudoFolder();
            } else {
                pseudoFolder2 = new ArrayList<>();
            }
            for (OptionalPartPair child : getCommonChildren(pseudoFolder1, pseudoFolder2, itemComparator)) {
                // Visit the pseudo-folder
                processor.visitPartsInPseudoFolder(aPart, child);
            }
        }
    }

    private void preProcessRfPart(final TcSession session, final OptionalBomPair aPart, final OptionalBomPair rfPart) {
        boolean partFound = false;
        for (OptionalBomPair child : getCommonChildren(rfPart, bomComparator)) {
            if (child.isPart()) {
                preProcessor.visitPartAsComponent(session, aPart, rfPart, child);
                partFound = true;
            }
        }
        if (!partFound) {
            preProcessor.visitReceptorWithoutComponent(session, aPart, rfPart);
        }
    }

    private void visitRfPart(final TcSession session, final OptionalBomPair aPart, final OptionalBomPair rfPart) {
        boolean partFound = false;
        for (OptionalBomPair child : getCommonChildren(rfPart, bomComparator)) {
            if (child.isPart()) {
                processor.visitPartAsComponent(session, aPart, rfPart, child);
                visitPartAsReceptor(session, child);
                partFound = true;
            }
        }
        if (!partFound) {
            processor.visitReceptorWithoutComponent(session, aPart, rfPart);
        }
    }

    private void visitProductAsReceptor(final TcSession session, final OptionalBomPair productReceptors) throws MalformedNomenclatureException {
        // Pre - processing : fetch all PRoduct - Gen - RF Gen
        List<OptionalBomPair> commonChildren = getCommonChildren(productReceptors, bomComparator);
        if (preProcessor != null) {
            for (OptionalBomPair child : commonChildren) {
                if (child.isGeneric()) {
                    OptionalBomPair rfGenericComponent = extractRfGeneric(child);
                    preProcessor.visitProductGeneric(session, productReceptors, child, rfGenericComponent);
                }
            }
        }

        // Recursive processing
        for (OptionalBomPair child : commonChildren) {
            if (child.isGeneric()) {
                // According to the MDD, there is one and only one RF Generic below any Generic.
                OptionalBomPair rfGenericComponent = extractRfGeneric(child);
                processor.visitProductGeneric(session, productReceptors, child, rfGenericComponent);

                // Now the component becomes receptor
                visitGenericAsReceptor(session, child);
            }
        }
    }

    private void visitGenericAsReceptor(final TcSession session, final OptionalBomPair genericReceptor) throws MalformedNomenclatureException {
        // According to the MDD, there is one and only one RF Generic below any Generic.
        OptionalBomPair rfGeneric = extractRfGeneric(genericReceptor);
        List<OptionalBomPair> commonChildren = getCommonChildren(genericReceptor, bomComparator);

        if (preProcessor != null) {
            // Pre-processing
            if (doRetrievePartNumberUnderRfGeneric) {
                // Go ahead and extract the generic / Rf Generic / Part
                visitRfGeneric(genericReceptor, rfGeneric, preProcessor);
            }
            // There may be other children, like some RF parts
            for (OptionalBomPair child : commonChildren) {
                if (child.isRFPartOrPartAssemb()) {
                    visitRfPartAsComponentNoRec(session, genericReceptor, rfGeneric, child);
                } else if (child.isGeneric()) {
                    visitGenericAsComponentNoRec(session, genericReceptor, rfGeneric, child);
                }
            }
        }

        // Recursive treatment now
        if (doRetrievePartNumberUnderRfGeneric) {
            // Go ahead and extract the generic / Rf Generic / Part
            visitRfGeneric(genericReceptor, rfGeneric, processor);
        }
        // There may be other children, like some RF parts
        for (OptionalBomPair child : commonChildren) {
            if (child.isRFPartOrPartAssemb()) {
                visitRfPartAsComponentRec(session, genericReceptor, rfGeneric, child);
            } else if (child.isGeneric()) {
                processor.visitQuad(session, genericReceptor, rfGeneric, child, extractRfGeneric(child));
                visitGenericAsReceptor(session, child);
            }
        }
    }

    private void visitGenericAsComponentNoRec(final TcSession session, final OptionalBomPair genericReceptor, final OptionalBomPair rfGenericReceptor,
                                              final OptionalBomPair genericComponent) throws MalformedNomenclatureException {
        // According to the MDD, there is one and only one RF Generic below any Generic.
        OptionalBomPair rfGenericComponent = extractRfGeneric(genericComponent);
        preProcessor.visitQuad(session, genericReceptor, rfGenericReceptor, genericComponent, rfGenericComponent);
    }

    private void visitRfPartAsComponentNoRec(final TcSession session, final OptionalBomPair receptor, final OptionalBomPair rfReceptor, final OptionalBomPair rfPart) {
        boolean partFound = false;
        for (OptionalBomPair child : getCommonChildren(rfPart, bomComparator)) {
            if (child.isPart()) {
                preProcessor.visitQuad(session, receptor, rfReceptor, child, rfPart);
                partFound = true;
            }
        }
        if (!partFound) {
            preProcessor.visitReceptorWithoutComponent(session, receptor, rfPart);
        }
    }

    private void visitRfPartAsComponentRec(final TcSession session, final OptionalBomPair receptor, final OptionalBomPair rfReceptor, final OptionalBomPair rfPart) {
        // Recursivity
        boolean partFound = false;
        for (OptionalBomPair child : getCommonChildren(rfPart, bomComparator)) {
            if (child.isPart()) {
                processor.visitQuad(session, receptor, rfReceptor, child, rfPart);
                visitPartAsReceptor(session, child);
                partFound = true;
            }
        }
        if (!partFound) {
            processor.visitReceptorWithoutComponent(session, receptor, rfPart);
        }
    }

    // RF Generics have a single PartAero below them. Also below , RF Admin(s) carry data about this RfGeneric
    private void visitRfGeneric(final OptionalBomPair genericReceptor, final OptionalBomPair rfGeneric, final IDualSnecmaLineProcessor proc)
            throws MalformedNomenclatureException {
        for (OptionalBomPair child : getCommonChildren(rfGeneric, bomComparator)) {
            if (child.isPart()) {
                proc.visitPartNumber(genericReceptor, rfGeneric, child);
                return;
            }
        }
        throw new MalformedNomenclatureException("No part aero found below RfGeneric");
    }

    private OptionalBomPair extractRfGeneric(final OptionalBomPair aGeneric) throws MalformedNomenclatureException {
        BOMLineHolder rfGeneric1;
        try {
            rfGeneric1 = BOMLineHolderHelper.findRfGenericBL(aGeneric.first);
        } catch (InvalidInputException e) {
            log.trace("null Generic in conf 1 ", e); // This is OK, it is exclusive to conf2
            rfGeneric1 = null;
        }
        BOMLineHolder rfGeneric2;
        try {
            rfGeneric2 = BOMLineHolderHelper.findRfGenericBL(aGeneric.second);
        } catch (InvalidInputException e) {
            log.trace("null Generic in conf 2 ", e); // This is OK, it is exclusive to conf1
            rfGeneric2 = null;
        }
        return new OptionalBomPair(rfGeneric1, rfGeneric2);
    }

    /**
     * Sort the children of a BomLineHolder.
     *
     * @param bomHolder  IBOMTree
     * @param comparator the comparator to be used for sorting. The comparator must be resilient to null. Any two elements deemed equal according to this comparator will be kept in the same order as they are found.
     */
    private List<BOMLineHolder> sortBomChildren(final BOMLineHolder bomHolder, final Comparator<BOMLineHolder> comparator) {
        if (bomHolder == null) {
            return Collections.emptyList();
        }
        List<BOMLineHolder> copy = new ArrayList<>(bomHolder.getBomChilds());
        removeIllConditioned(copy); // Remove all null elements for safety
        Collections.sort(copy, comparator);
        return copy;
    }

    private void removeIllConditioned(final List<BOMLineHolder> list) {
        Iterator<BOMLineHolder> iterator = list.iterator();
        while (iterator.hasNext()) {
            BOMLineHolder blh = iterator.next();
            if (blh == null || blh.getSnecmaObject() == null) {
                iterator.remove();
            }
        }
    }

    /**
     * Returns a List of comparison pairs from both configurations. All RF that have the wrong effectivity are ignored in the ouput.<br>
     * A resulting child pair can be:
     * <ul>
     * <li>one-sided: item is found only in one of the two configurations, no object from the other conf is equal according to the provided {@link Comparator} or maybe the other item is a RF with wrong effectivity (date etc.)</li>
     * <li>Two-sided: an item from either configuration is found, and the provided {@link Comparator} finds them equal.</li>
     * </ul>
     *
     * @param pair           pair the original pair of objects to be compared.
     * @param dualComparator the comparator to be used for sorting. The comparator must be resilient to null. Any two elements deemed equal according to this comparator will be kept in the same order as they are found.
     */
    private List<OptionalBomPair> getCommonChildren(final OptionalBomPair pair, final Comparator<BOMLineHolder> dualComparator) {
        List<OptionalBomPair> pairs = new ArrayList<>();
        Iterator<BOMLineHolder> iterator1 = sortBomChildren(pair.getFirst(), dualComparator).iterator(); // Sort both boms according to the comparator's sense of order
        Iterator<BOMLineHolder> iterator2 = sortBomChildren(pair.getSecond(), dualComparator).iterator(); // Sort both boms according to the comparator's sense of order
        BOMLineHolder child1 = nextOrNull(iterator1);
        BOMLineHolder child2 = nextOrNull(iterator2);
        while (child1 != null || child2 != null) {
            if (child1 != null && child2 != null) {
                // Both lists are not empty...
                int cmp = dualComparator.compare(child1, child2);
                if (cmp == 0) {
                    pairs.add(new OptionalBomPair(child1, child2));
                    child1 = nextOrNull(iterator1);
                    child2 = nextOrNull(iterator2);
                } else if (cmp < 0) {
                    pairs.add(new OptionalBomPair(child1, null));
                    child1 = nextOrNull(iterator1);
                } else {
                    pairs.add(new OptionalBomPair(null, child2));
                    child2 = nextOrNull(iterator2);
                }
            } else if (child1 != null) {
                pairs.add(new OptionalBomPair(child1, null));
                child1 = nextOrNull(iterator1);
            } else {
                pairs.add(new OptionalBomPair(null, child2));
                child2 = nextOrNull(iterator2);
            }
        }
        return pairs;
    }

    private BOMLineHolder nextOrNull(final Iterator<BOMLineHolder> iterator) {
        if (!iterator.hasNext()) {
            return null; // No more elements in this iterator
        }
        BOMLineHolder next = iterator.next();
        if (next.getSnecmaObject().isRF() && !effectivityBehaviour.shouldVisit(next)) {
            next = nextOrNull(iterator); // Next element is a RF, wich should not be visited, so we'll jump it
        }
        return next;
    }

    private static class PassiveProcessor implements IDualSnecmaLineProcessor {

        @Override
        public void visitTopBom(final OptionalBomPair aTopBom) {
            // Nothing to do
        }

        @Override
        public void visitReceptorWithoutComponent(TcSession session, final OptionalBomPair part, final OptionalBomPair rfPart) {
            // Nothing to do
        }

        @Override
        public void visitQuad(TcSession session, final OptionalBomPair receptor, final OptionalBomPair rfReceptor, final OptionalBomPair component,
                              final OptionalBomPair rfComponent) {
            // Nothing to do
        }

        @Override
        public void visitProductGeneric(TcSession session, final OptionalBomPair productReceptor, final OptionalBomPair genericComponent,
                                        final OptionalBomPair rfGenericComponent) {
            // Nothing to do
        }

        @Override
        public void visitPartsInPseudoFolder(final OptionalBomPair aPart, final OptionalPartPair child) {
            // Nothing to do
        }

        @Override
        public void visitPartPart(final OptionalBomPair fatherPart, final OptionalBomPair childPart) {
            // Nothing to do
        }

        @Override
        public void visitPartNumber(final OptionalBomPair genericReceptor, final OptionalBomPair rfGeneric, final OptionalBomPair child) {
            // Nothing to do
        }

        @Override
        public void visitPartAsComponent(TcSession session, final OptionalBomPair part, final OptionalBomPair rfPart, final OptionalBomPair child) {
            // Nothing to do
        }
    }

    /**
     * Can hold a pair of snecma {@link ISnecmaObject} from either conf, which are deemed similar to each other. Either one may be null, or none.
     */
    @Getter
    @AllArgsConstructor
    public static class OptionalPartPair {

        private final AbstractPart first;
        private final AbstractPart second;

        /**
         * True if the object of the 1st conf is undefined.
         *
         * @return True if the bomLineHolder of the 1st conf is undefined
         */
        public boolean firstExists() {
            return first != null;
        }

        /**
         * True if the object of the 2nd conf is undefined.
         *
         * @return True if the bomLineHolder of the 2nd conf is undefined
         */
        public boolean secondExists() {
            return second != null;
        }
    }

    /**
     * Can hold a pair of {@link BOMLineHolder}s from either conf, which are deemed similar to each other. Either one may be null, or none.
     */
    @Getter
    @AllArgsConstructor
    public static class OptionalBomPair {
        private final BOMLineHolder first;
        private final BOMLineHolder second;

        /**
         * Returns the common type of both held objects.
         *
         * @return a String containing the object type.
         */
        public String getType() {
            return first == null ? second.getSnecmaObject().getType() : first.getSnecmaObject().getType();
        }

        /**
         * Return true if it is a RF Generic.
         *
         * @return the boolean
         */
        public boolean isRFGeneric() {
            return first == null ? second.getSnecmaObject().isRFGeneric() : first.getSnecmaObject().isRFGeneric();
        }

        /**
         * Return true if it is a RF Part or a RF PArt Assemb.
         *
         * @return the boolean
         */
        public boolean isRFPartOrPartAssemb() {
            return first == null ? second.getSnecmaObject().isRFPartOrPartAssemb() : first.getSnecmaObject().isRFPartOrPartAssemb();
        }

        /**
         * Return true if it is a Part.
         *
         * @return the boolean
         */
        public boolean isPart() {
            return first == null ? second.getSnecmaObject().isPart() : first.getSnecmaObject().isPart();
        }

        /**
         * Return true if it is a Generic.
         *
         * @return the boolean
         */
        public boolean isGeneric() {
            return first == null ? second.getSnecmaObject().isGeneric() : first.getSnecmaObject().isGeneric();
        }

        /**
         * True if the bomLineHolder of the 1st conf is undefined.
         *
         * @return True if the bomLineHolder of the 1st conf is undefined
         */
        public boolean firstExists() {
            return first != null;
        }

        /**
         * True if the bomLineHolder of the 2nd conf is undefined.
         *
         * @return True if the bomLineHolder of the 2nd conf is undefined
         */
        public boolean secondExists() {
            return second != null;
        }

        @Override
        public String toString() {
            return "Pair: " + (firstExists() ? first.toString() : "") + " <-> " + (secondExists() ? second.toString() : "");
        }
    }

}
