package com.capgemini.reports.definition.r07report.data.filter;

import com.capgemini.reports.bomlineholder.flattener.line.SnecmaLine;
import com.capgemini.reports.definition.r07report.data.TransformerR07;
import com.capgemini.reports.definition.r07report.data.extractor.BomFlattenerR07;
import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.*;

/**
 * This class filters two configurations, then extracts the differences.
 * <ul>
 * <li>All lines that are <b>identical</b> on both sides are <b>lost</b></li>
 * <li>All lines that boast the <b>same structure</b> (same receptor, repere, component for example) but have a difference in their content, are stored side by side in a {@link List} of {@link Diff} named {@link ConfigurationFilterR07#propDiff propDiff}.</li>
 * <li>All lines that are <b>exclusive</b> to either configuration are kept in a separate {@link List} of {@link SnecmaLine}s named {@link ConfigurationFilterR07#exclusiveConf1 exclusiveConf1} and {@link ConfigurationFilterR07#exclusiveConf2 exclusiveConf2}.</li>
 * </ul>
 */
@Getter
public class ConfigurationFilterR07 {

    /**
     * Provides capabilities for comparing structure (receptor/repere) between two {@link SnecmaLine}s.
     */
    private static final Comparator<SnecmaLine<String, TransformerR07>> STRUCTURAL_COMPARATOR = new StructuralComparator();
    /**
     * Provides capabilities for comparing full-depth content between two {@link SnecmaLine}s.
     */
    private static final Comparator<SnecmaLine<String, TransformerR07>> CONTENT_COMPARATOR = new ContentComparator();
    /**
     * List of differences detected in properties' contents between two objects of the same structure.
     */
    private final List<Diff> propDiff = new ArrayList<>();
    /**
     * Pairs exclusively present in Conf1.
     */
    private final List<SnecmaLine<String, TransformerR07>> exclusiveConf1 = new ArrayList<>();
    /**
     * Pairs exclusively present in Conf2.
     */
    private final List<SnecmaLine<String, TransformerR07>> exclusiveConf2 = new ArrayList<>();
    /**
     * Number of exact matches in both configurations: same structure, same data content.
     */
    private int cloneNum;

    /**
     * The main method of the class: performs a Filtering on two configurations.<br>
     * Leaves this instance in a state where it holds:
     * <ul>
     * <li>A {@link List} of {@link Diff} between data pairs conf1-conf2</li>
     * <li>Two {@link List}s of exclusive content</li>
     * <li>and the number of clones found in both configurations</li>
     * </ul>
     * <br>
     * Note: the input {@link BomFlattenerR07}'s contents will be cleared in the process!
     *
     * @param conf the configuration holder
     */
    public void filter(final BomFlattenerR07 conf) {
        List<R07Bucket> similarBuckets = filterExclusiveContent(conf.getConfig1(), conf.getConfig2());
        for (R07Bucket bucket : similarBuckets) {
            bucket.filterSimilarContent();
        }
    }

    /**
     * Detect exclusive items of each conf, stores them appart, and removes them from their respective conf.
     *
     * @return a series of data buckets, each holding two sets of objects from either conf, which belong share the same structure
     */
    private List<R07Bucket> filterExclusiveContent(final List<SnecmaLine<String, TransformerR07>> conf1,
                                                   final List<SnecmaLine<String, TransformerR07>> conf2) {
        List<R07Bucket> similarBuckets = new ArrayList<>();

        // Sort both configurations in place, according to the comparator.
        // Note several elements may be 'equal' regarding to the comparator: they will keep their respective order
        // Sorting several times using increasingly restrictive comparators is not so expensive, because each pass uses the pre-optimised result of the previous pass
        Collections.sort(conf1, STRUCTURAL_COMPARATOR);
        Collections.sort(conf2, STRUCTURAL_COMPARATOR);
        R07Bucket currentBucket = null;
        Iterator<SnecmaLine<String, TransformerR07>> iteratorConf1 = conf1.iterator();
        Iterator<SnecmaLine<String, TransformerR07>> iteratorConf2 = conf2.iterator();
        SnecmaLine<String, TransformerR07> lowestConf1 = iteratorConf1.next();
        SnecmaLine<String, TransformerR07> lowestConf2 = iteratorConf2.next();
        iteratorConf1.remove();
        iteratorConf2.remove();
        boolean keepOn = true;
        // Now iterate from 'lowest' member of either conf to 'highest' member of either conf
        while (keepOn) {
            int compare = STRUCTURAL_COMPARATOR.compare(lowestConf1, lowestConf2);
            if (compare == 0) {
                // Same structure on both stacks!
                if (currentBucket == null) {
                    currentBucket = new R07Bucket(lowestConf1); // Start new bucket
                } else {
                    // Packet already existing, does it have the same value?
                    if (currentBucket.compareTo(lowestConf1) != 0) {
                        similarBuckets.add(currentBucket); // different value detected, let's register the old bucket...
                        currentBucket = new R07Bucket(lowestConf1); // ...and start a new one
                    }
                }

                // The current bucket has the correct ref value, we just need to add the new data pair in it
                currentBucket.add(lowestConf1, lowestConf2);
                if (iteratorConf1.hasNext() && iteratorConf2.hasNext()) {
                    // Prepare next run
                    lowestConf1 = iteratorConf1.next();
                    lowestConf2 = iteratorConf2.next();
                    iteratorConf1.remove();
                    iteratorConf2.remove();
                } else {
                    keepOn = false; // End of the while
                }
            } else {
                // Stream of similar-valued object has stopped. Time to register the bucket content (if any)
                if (currentBucket != null && !currentBucket.isEmpty()) {
                    similarBuckets.add(currentBucket);
                }
                currentBucket = null;
                if (compare < 0) { // Stack 1 is strictly lower than stack 2
                    exclusiveConf1.add(lowestConf1); // Any encountered value is removed from the conf1 and added to the 'exclusive conf 1' list
                    if (iteratorConf1.hasNext()) {
                        // Prepare next run
                        lowestConf1 = iteratorConf1.next();
                        iteratorConf1.remove();
                    } else {
                        keepOn = false; // End of the while loop
                    }
                } else { // Stack 2 is strictly lower than stack 1
                    exclusiveConf2.add(lowestConf2); // Any encountered value is removed from the conf2 and added to the 'exclusive conf 2' list
                    if (iteratorConf2.hasNext()) {
                        // Prepare next run
                        lowestConf2 = iteratorConf2.next();
                        iteratorConf2.remove();
                    } else {
                        keepOn = false; // End of the while loop
                    }
                }
            }
        }
        // Cleanup: active bucket may need to be added to the list if not empty
        if (currentBucket != null && !currentBucket.isEmpty()) {
            similarBuckets.add(currentBucket);
        }
        // Cleanup: Any remaining item between index and the end of its stack is exclusive to that stack
        while (!conf1.isEmpty()) {
            exclusiveConf1.add(conf1.remove(0));
        }
        while (!conf2.isEmpty()) {
            exclusiveConf2.add(conf2.remove(0));
        }
        return similarBuckets;
    }

    /**
     * Holds twoLines of either configuration, which are the same, but have a content difference between them.
     */
    @AllArgsConstructor
    @Getter
    public static class Diff {

        private final SnecmaLine<String, TransformerR07> first;
        private final SnecmaLine<String, TransformerR07> second;

        @Override
        public String toString() {
            return "Diff{" + first.toString() + " != " + second.toString() + "}";
        }
    }

    /**
     * 'Bucket' class to store 'similar' (According to the BOM_COMPARATOR) objects from both configurations.<br>
     * Objects that are 'identical' according to the CONTENT_COMPARATOR can then be identified and removed.
     */
    @AllArgsConstructor
    @Getter
    private class R07Bucket {

        /**
         * Reference line. Defines the Structure signature of this bucket uniquely.
         */
        private final SnecmaLine<String, TransformerR07> ref;
        /**
         * Contains all the SnecmaLine from conf1 that have the same Structure as the REF.
         */
        private final List<SnecmaLine<String, TransformerR07>> first = new ArrayList<>();
        /**
         * Contains all the SnecmaLine from conf2 that have the same Structure as the REF.
         */
        private final List<SnecmaLine<String, TransformerR07>> second = new ArrayList<>();

        /**
         * Compares this bucket's ref to the line.
         *
         * @param line the line
         * @return the result
         */
        public int compareTo(final SnecmaLine<String, TransformerR07> line) {
            return ref.compareTo(line);
        }

        /**
         * Bucket empty?
         *
         * @return <code>true</code> if it is empty
         */
        public boolean isEmpty() {
            return first.isEmpty() && second.isEmpty();
        }

        /**
         * Adds a pair to the bucket. Added objects MUST be 'similar' to the bucket's content.
         *
         * @param conf1 the object from conf1
         * @param conf2 the object from conf2
         */
        public void add(final SnecmaLine<String, TransformerR07> conf1, final SnecmaLine<String, TransformerR07> conf2) {
            first.add(conf1);
            second.add(conf2);
        }

        /**
         * This method:
         * <ol>
         * <li>Removes 'identical' (same content) pairs found in both configurations.</li>
         * <li>Bundles 'similar' (same structure) but not 'identical' objects from either confs together</li>
         * <li>Remaining 'surplus' objects from a conf (that therefore cannot be appaired to an object from the other conf) are sent to an 'exclusive' list.</li>
         * </ol>
         */
        void filterSimilarContent() {
            removeIdenticalPairs();
            // All exact pairs from this bucket have been found and matched together (they will not show up in the report
            bundleSimilarPairs();
            // Any remaining item in the larger list of the two will be shown as an exclusive member of its conf
            exclusiveConf1.addAll(first);
            exclusiveConf2.addAll(second);
        }

        /**
         * Finds and removes all identical pairs from this bucket (they will not show up in the report).
         */
        private void removeIdenticalPairs() {
            Iterator<SnecmaLine<String, TransformerR07>> conf1Iterator = first.iterator();
            while (conf1Iterator.hasNext()) {
                SnecmaLine<String, TransformerR07> line1 = conf1Iterator.next();
                Iterator<SnecmaLine<String, TransformerR07>> conf2Iterator = second.iterator();
                while (conf2Iterator.hasNext()) {
                    SnecmaLine<String, TransformerR07> line2 = conf2Iterator.next();
                    if (CONTENT_COMPARATOR.compare(line1, line2) == 0) { // Exact same content detected, from two lines in the same bucket so same structure)
                        cloneNum++;
                        // This pair will never make it to the report
                        conf1Iterator.remove();
                        conf2Iterator.remove();
                        break;
                    }
                }
            }
        }

        /**
         * Bundles together any two line from either configuration.<br>
         * At this point these lines all have a difference otherwise they would have been removed by the previous step {@link R07Bucket#removeIdenticalPairs()}.<br>
         * In priority, lines from either configuration that have the same Part number will be paired. This is not required, but is nice.
         */
        private void bundleSimilarPairs() {
            // First pair lines that have the same Part ID. If several can be found, the 1st one is chosen (arbitrary)
            Iterator<SnecmaLine<String, TransformerR07>> conf1Iterator = first.iterator();
            while (conf1Iterator.hasNext()) {
                SnecmaLine<String, TransformerR07> line1 = conf1Iterator.next();
                Iterator<SnecmaLine<String, TransformerR07>> conf2Iterator = second.iterator();
                while (conf2Iterator.hasNext()) {
                    SnecmaLine<String, TransformerR07> line2 = conf2Iterator.next();
                    if (line1.getIDComponent().compareTo(line2.getIDComponent()) == 0) {
                        // This pair will be shown as a content diff between the two configurations
                        propDiff.add(new Diff(line1, line2));
                        conf1Iterator.remove();
                        conf2Iterator.remove();
                        break; // Find another couple
                    }
                }
            }

            // Any remaining item in the bucket will be paired arbitrarily until one conf is emptied
            conf1Iterator = first.iterator();
            Iterator<SnecmaLine<String, TransformerR07>> conf2Iterator = second.iterator();
            while (conf1Iterator.hasNext() && conf2Iterator.hasNext()) {
                SnecmaLine<String, TransformerR07> line1 = conf1Iterator.next();
                SnecmaLine<String, TransformerR07> line2 = conf2Iterator.next();
                // This pair will be shown as a content diff between the two configurations
                propDiff.add(new Diff(line1, line2));
                conf1Iterator.remove();
                conf2Iterator.remove();
            }
        }

        @Override
        public String toString() {
            return "Pack<" + first.toString() + " vs " + second.toString() + ">";
        }
    }
}
