/* FichothequeLib_Tools - Copyright (c) 2013-2025 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.fichotheque.tools.junction;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.fichotheque.Fichotheque;
import net.fichotheque.Subset;
import net.fichotheque.SubsetItem;
import net.fichotheque.SubsetKey;
import net.fichotheque.junction.JunctionKey;
import net.fichotheque.include.IncludeKey;
import net.fichotheque.utils.JunctionUtils;
import net.fichotheque.junction.Tie;
import net.fichotheque.junction.JunctionChange;
import net.fichotheque.junction.Junction;
import net.fichotheque.junction.Junctions;
import net.fichotheque.junction.JunctionChanges;


/**
 *
 * @author Vincent Calame
 */
public class JunctionChangeEngine {

    private final static short CLEAR_EXISTING = 1;
    private final static short APPEND_AND_WEIGHT_REPLACE = 2;
    private final static short APPEND_AND_WEIGHT_MAX = 3;
    private final static short APPEND_AND_WEIGHT_NOCHANGE = 4;
    private final short changeType;
    private final SubsetItem mainSubsetItem;
    private final Map<String, NewJunction> junctionMap = new LinkedHashMap<String, NewJunction>();
    private final Map<SubsetKey, SubsetInfo> subsetMap = new HashMap<SubsetKey, SubsetInfo>();
    private final boolean withIncludeKeyFilter;

    private JunctionChangeEngine(short changeType, SubsetItem mainSubsetItem) {
        this.changeType = changeType;
        this.mainSubsetItem = mainSubsetItem;
        this.withIncludeKeyFilter = false;
    }

    private JunctionChangeEngine(short changeType, SubsetItem mainSubsetItem, Collection<IncludeKey> scope) {
        this.changeType = changeType;
        this.mainSubsetItem = mainSubsetItem;
        this.withIncludeKeyFilter = true;
        Fichotheque fichotheque = mainSubsetItem.getFichotheque();
        for (IncludeKey includeKey : scope) {
            Subset subset = fichotheque.getSubset(includeKey.getSubsetKey());
            if (subset != null) {
                initSubset(subset, includeKey.getMode(), includeKey.getWeightFilter());
            }
        }
    }

    private JunctionChangeEngine() {
        this.changeType = CLEAR_EXISTING;
        this.mainSubsetItem = null;
        this.withIncludeKeyFilter = false;
    }

    private void initSubset(Subset subset, String mode, int weightFilter) {
        SubsetInfo subsetInfo = subsetMap.get(subset.getSubsetKey());
        if (subsetInfo == null) {
            subsetInfo = new SubsetInfo(subset);
            subsetMap.put(subset.getSubsetKey(), subsetInfo);
        }
        subsetInfo.add(mode, weightFilter);

    }

    public void addJunctions(Junctions junctions, Predicate<SubsetItem> predicate) {
        for (Junctions.Entry entry : junctions.getEntryList()) {
            SubsetItem otherSubsetItem = entry.getSubsetItem();
            if (predicate.test(otherSubsetItem)) {
                for (Tie tie : entry.getJunction().getTieList()) {
                    try {
                        addTie(otherSubsetItem, tie.getMode(), tie.getWeight());
                    } catch (IllegalArgumentException iae) {
                    }
                }
            }
        }
    }

    public void addTie(TieBuffer tieBuffer) {
        addTie(tieBuffer.getSubsetItem(), tieBuffer.getMode(), tieBuffer.getWeight());
    }

    public void addTie(SubsetItem otherSubsetItem, String mode, int weight) {
        if (isMainSubsetItem(otherSubsetItem)) {
            return;
        }
        SubsetInfo subsetInfo = getOrCreateSubsetInfo(otherSubsetItem);
        ModeInfo modeInfo = getOrCreateModeInfo(subsetInfo, mode);
        if (!modeInfo.containsWeight(weight)) {
            throw new IllegalArgumentException("Weight not declared : " + weight);
        }
        boolean mainInFirst;
        if (mainSubsetItem != null) {
            mainInFirst = (JunctionKey.getOrder(mainSubsetItem, otherSubsetItem) == 1);
        } else {
            mainInFirst = true;
        }
        NewTie newTie = new NewTie(otherSubsetItem, mode, weight, mainInFirst);
        boolean done = getOrCreateNewJunction(otherSubsetItem).adNewTie(newTie);
        if (done) {
            subsetInfo.addNewTie(newTie);
        }
    }

    public void removeTie(SubsetItem otherSubsetItem, String mode) {
        if (isMainSubsetItem(otherSubsetItem)) {
            return;
        }
        SubsetInfo subsetInfo = getOrCreateSubsetInfo(otherSubsetItem);
        getOrCreateModeInfo(subsetInfo, mode);
        getOrCreateNewJunction(otherSubsetItem).addRemovedMode(mode);
    }

    public JunctionChanges toJunctionChanges() {
        if (mainSubsetItem == null) {
            checkNew();
        } else {
            checkExisting();
        }
        JunctionChangesBuilder builder = new JunctionChangesBuilder();
        for (NewJunction newJunction : junctionMap.values()) {
            if (newJunction.isRemoved()) {
                builder.addRemoved(newJunction.otherSubsetItem);
            } else if (!newJunction.isCancelled()) {
                JunctionChange junctionChange = newJunction.toJunctionChange();
                builder.addEntry(newJunction.otherSubsetItem, junctionChange);
            }
        }
        return builder.toJunctionChanges();
    }

    public static JunctionChangeEngine newEngine() {
        return new JunctionChangeEngine();
    }

    public static JunctionChangeEngine clearExistingEngine(SubsetItem mainSubsetItem) {
        return new JunctionChangeEngine(CLEAR_EXISTING, mainSubsetItem);
    }

    public static JunctionChangeEngine clearExistingEngine(SubsetItem mainSubsetItem, Collection<IncludeKey> scope) {
        return new JunctionChangeEngine(CLEAR_EXISTING, mainSubsetItem, scope);
    }

    public static JunctionChangeEngine appendEngine(SubsetItem mainSubsetItem) {
        return new JunctionChangeEngine(APPEND_AND_WEIGHT_NOCHANGE, mainSubsetItem);
    }

    public static JunctionChangeEngine appendOrWeightReplaceEngine(SubsetItem mainSubsetItem) {
        return new JunctionChangeEngine(APPEND_AND_WEIGHT_REPLACE, mainSubsetItem);
    }

    public static JunctionChangeEngine appendOrWeightMaxEngine(SubsetItem mainSubsetItem) {
        return new JunctionChangeEngine(APPEND_AND_WEIGHT_MAX, mainSubsetItem);
    }

    private boolean isMainSubsetItem(SubsetItem otherSubsetItem) {
        if ((mainSubsetItem != null) && (mainSubsetItem.equals(otherSubsetItem))) {
            return true;
        } else {
            return false;
        }
    }

    private SubsetInfo getOrCreateSubsetInfo(SubsetItem otherSubsetItem) {
        SubsetInfo subsetInfo = subsetMap.get(otherSubsetItem.getSubsetKey());
        if (subsetInfo == null) {
            if (withIncludeKeyFilter) {
                throw new IllegalArgumentException("Subset not declared : " + otherSubsetItem.getSubsetKey());
            } else {
                subsetInfo = new SubsetInfo(otherSubsetItem.getSubset());
                subsetMap.put(otherSubsetItem.getSubsetKey(), subsetInfo);
            }
        }
        return subsetInfo;
    }

    private ModeInfo getOrCreateModeInfo(SubsetInfo subsetInfo, String mode) {
        ModeInfo modeInfo = subsetInfo.getModeInfo(mode);
        if (modeInfo == null) {
            if (withIncludeKeyFilter) {
                throw new IllegalArgumentException("Mode not declared : " + mode);
            } else {
                modeInfo = subsetInfo.add(mode, -1);
            }
        }
        return modeInfo;
    }

    private NewJunction getOrCreateNewJunction(SubsetItem otherSubsetItem) {
        String stringKey = toStringKey(otherSubsetItem);
        NewJunction newJunction = junctionMap.get(stringKey);
        if (newJunction == null) {
            newJunction = new NewJunction(otherSubsetItem);
            junctionMap.put(stringKey, newJunction);
        }
        return newJunction;
    }

    private void checkNew() {
        for (Map.Entry<SubsetKey, SubsetInfo> entry : subsetMap.entrySet()) {
            SubsetInfo subsetInfo = entry.getValue();
            PositionMaxima positionMaxima = new PositionMaxima();
            for (NewTie newTie : subsetInfo.newTieList) {
                if (!newTie.isCanceled()) {
                    if (newTie.isMainPositionUndefined()) {
                        newTie.setMainPosition(positionMaxima.getNewPosition(newTie.getMode()));
                    }
                }
            }
        }
    }

    private void checkExisting() {
        Fichotheque fichotheque = mainSubsetItem.getFichotheque();
        for (Map.Entry<SubsetKey, SubsetInfo> entry : subsetMap.entrySet()) {
            SubsetInfo subsetInfo = entry.getValue();
            Junctions currentJunctions = fichotheque.getJunctions(mainSubsetItem, subsetInfo.subset);
            PositionMaxima positionMaxima = new PositionMaxima();
            if (changeType != CLEAR_EXISTING) {
                for (Junctions.Entry currentEntry : currentJunctions.getEntryList()) {
                    Junction junction = currentEntry.getJunction();
                    positionMaxima.checkJunction(junction, (junction.getJunctionKey().getOrder(mainSubsetItem) != 1));
                }
            }
            for (Junctions.Entry currentEntry : currentJunctions.getEntryList()) {
                SubsetItem otherSubsetItem = currentEntry.getSubsetItem();
                Junction currentJunction = currentEntry.getJunction();
                String key = toStringKey(otherSubsetItem);
                NewJunction newJunction = junctionMap.get(key);
                if (newJunction == null) {
                    if (changeType == CLEAR_EXISTING) {
                        newJunction = initRemovedNewJunction(subsetInfo, otherSubsetItem, currentJunction);
                        junctionMap.put(key, newJunction);
                    }
                } else {
                    for (Tie currentTie : currentJunction.getTieList()) {
                        if (subsetInfo.containsMode(currentTie.getMode())) {
                            newJunction.checkCurrentTie(currentTie, changeType);
                        }
                    }
                }
            }
            for (NewTie newTie : subsetInfo.newTieList) {
                if (!newTie.isCanceled()) {
                    if (newTie.isMainPositionUndefined()) {
                        PositionMaxima otherPositionMaxima = subsetInfo.getMainPositionMaxima(newTie.getOtherSubsetItem(), mainSubsetItem.getSubset());
                        newTie.setMainPosition(otherPositionMaxima.getNewPosition(newTie.getMode()));
                    }
                    if (newTie.isOtherPositionUndefined()) {
                        newTie.setOtherPosition(positionMaxima.getNewPosition(newTie.getMode()));
                    }
                }
            }
        }
    }

    private NewJunction initRemovedNewJunction(SubsetInfo subsetInfo, SubsetItem otherSubsetItem, Junction junction) {
        List<Tie> tieList = junction.getTieList();
        int tieCount = tieList.size();
        int removeCount = 0;
        NewJunction newJunction = new NewJunction(otherSubsetItem);
        for (Tie tie : tieList) {
            String mode = tie.getMode();
            ModeInfo modeInfo = subsetInfo.getModeInfo(mode);
            if (modeInfo != null) {
                int weight = tie.getWeight();
                if (modeInfo.containsWeight(weight)) {
                    newJunction.addRemovedMode(mode);
                    removeCount++;
                }
            }
        }
        if (removeCount == tieCount) {
            newJunction.setRemoveAll(true);
        }
        return newJunction;
    }


    private static class SubsetInfo {

        private final Fichotheque fichotheque;
        private final Subset subset;
        private final Map<String, ModeInfo> modeInfoMap = new HashMap<String, ModeInfo>();
        private List<NewTie> newTieList = new ArrayList<NewTie>();
        private final Map<Integer, PositionMaxima> maximaMap = new HashMap<Integer, PositionMaxima>();

        private SubsetInfo(Subset subset) {
            this.subset = subset;
            this.fichotheque = subset.getFichotheque();
        }

        private ModeInfo add(String mode, int weightFilter) {
            ModeInfo modeInfo = modeInfoMap.get(mode);
            if (modeInfo == null) {
                modeInfo = new ModeInfo(mode);
                modeInfoMap.put(mode, modeInfo);
            }
            modeInfo.addWeightFilter(weightFilter);
            return modeInfo;
        }

        private boolean containsMode(String mode) {
            return (modeInfoMap.get(mode) != null);
        }

        private ModeInfo getModeInfo(String mode) {
            return modeInfoMap.get(mode);
        }

        private void addNewTie(NewTie newTie) {
            newTieList.add(newTie);
        }

        private PositionMaxima getMainPositionMaxima(SubsetItem otherSubsetItem, Subset mainSubset) {
            PositionMaxima positionMaxima = maximaMap.get(otherSubsetItem.getId());
            if (positionMaxima == null) {
                Junctions mainSubsetJunctions = fichotheque.getJunctions(otherSubsetItem, mainSubset);
                positionMaxima = new PositionMaxima();
                for (Junctions.Entry entry : mainSubsetJunctions.getEntryList()) {
                    Junction junction = entry.getJunction();
                    positionMaxima.checkJunction(junction, (junction.getJunctionKey().getOrder(otherSubsetItem) != 1));
                }
                maximaMap.put(otherSubsetItem.getId(), positionMaxima);
            }
            return positionMaxima;
        }

    }


    private static class ModeInfo {

        private final String mode;
        private boolean allWeight = false;
        private Set<Integer> weightSet;

        private ModeInfo(String mode) {
            this.mode = mode;
        }

        private void addWeightFilter(int weightFilter) {
            if (allWeight) {
                return;
            }
            if (weightFilter == -1) {
                allWeight = true;
            } else {
                if (weightSet == null) {
                    weightSet = new HashSet<Integer>();
                }
                weightSet.add(weightFilter);
            }
        }

        private boolean containsWeight(int weight) {
            if (allWeight) {
                return true;
            }
            if (weightSet == null) {
                return false;
            }
            return weightSet.contains(weight);
        }

    }


    private static class NewJunction {

        private final SubsetItem otherSubsetItem;
        private final Set<String> removedModeSet = new HashSet<String>();
        private final Map<String, NewTie> newTieMap = new HashMap<String, NewTie>();
        private boolean removeAll = false;

        private NewJunction(SubsetItem otherSubsetItem) {
            this.otherSubsetItem = otherSubsetItem;
        }

        private boolean adNewTie(NewTie newTie) {
            String mode = newTie.getMode();
            if (newTieMap.containsKey(mode)) {
                return false;
            }
            newTieMap.put(mode, newTie);
            removedModeSet.remove(mode);
            return true;
        }

        private void addRemovedMode(String mode) {
            removedModeSet.add(mode);
            cancelNewTie(mode);
        }

        private void setRemoveAll(boolean removeAll) {
            this.removeAll = removeAll;
        }

        private NewTie getNewTie(String mode) {
            return newTieMap.get(mode);
        }

        private void cancelNewTie(String mode) {
            NewTie newTie = newTieMap.remove(mode);
            if (newTie != null) {
                newTie.cancelNewTie();
            }
        }

        private boolean isRemoved() {
            return removeAll;
        }

        private boolean isCancelled() {
            return ((removedModeSet.isEmpty()) && (newTieMap.isEmpty()));
        }

        private void checkCurrentTie(Tie currentTie, short changeType) {
            String mode = currentTie.getMode();
            if (removedModeSet.contains(mode)) {
                return;
            }
            NewTie newTie = getNewTie(mode);
            if (newTie == null) {
                if (changeType == CLEAR_EXISTING) {
                    addRemovedMode(mode);
                }
            } else if (changeType == APPEND_AND_WEIGHT_NOCHANGE) {
                cancelNewTie(mode);
            } else if (changeType == APPEND_AND_WEIGHT_MAX) {
                if (currentTie.getWeight() >= newTie.weight) {
                    cancelNewTie(mode);
                } else {
                    newTie.setPositions(currentTie);
                }
            } else if (changeType == APPEND_AND_WEIGHT_REPLACE) {
                if (currentTie.getWeight() == newTie.weight) {
                    cancelNewTie(mode);
                } else {
                    newTie.setPositions(currentTie);
                }
            } else if (changeType == CLEAR_EXISTING) {
                newTie.setMainPosition(currentTie);
            }
        }

        private JunctionChange toJunctionChange() {
            List<Tie> tieList = new ArrayList<Tie>();
            for (NewTie newTie : newTieMap.values()) {
                if (!newTie.isCanceled()) {
                    Tie tie = JunctionUtils.toTie(newTie.getMode(), newTie.weight, newTie.position1, newTie.position2);
                    tieList.add(tie);
                }
            }
            return JunctionChangeBuilder.toJunctionChange(removedModeSet, tieList);
        }

    }


    private static class NewTie {

        private final SubsetItem otherSubsetItem;
        private final String mode;
        private int weight;
        private int position1 = 0;
        private int position2 = 0;
        private final boolean mainIsFirst;
        private boolean canceled = false;

        private NewTie(SubsetItem otherSubsetItem, String mode, int weight, boolean mainIsFirst) {
            this.mode = mode;
            this.weight = weight;
            this.mainIsFirst = mainIsFirst;
            this.otherSubsetItem = otherSubsetItem;
        }

        private String getMode() {
            return mode;
        }

        private SubsetItem getOtherSubsetItem() {
            return otherSubsetItem;
        }

        private void setPositions(Tie currentTie) {
            this.position1 = currentTie.getPosition1();
            this.position2 = currentTie.getPosition2();
        }

        private boolean isCanceled() {
            return canceled;
        }

        private void cancelNewTie() {
            this.canceled = true;
        }

        private boolean isMainPositionUndefined() {
            if (mainIsFirst) {
                return (this.position1 == 0);
            } else {
                return (this.position2 == 0);
            }
        }

        private boolean isOtherPositionUndefined() {
            if (mainIsFirst) {
                return (this.position2 == 0);
            } else {
                return (this.position1 == 0);
            }
        }

        private void setMainPosition(int position) {
            if (mainIsFirst) {
                this.position1 = position;
            } else {
                this.position2 = position;
            }
        }

        private void setMainPosition(Tie currentTie) {
            if (mainIsFirst) {
                this.position1 = currentTie.getPosition1();
            } else {
                this.position2 = currentTie.getPosition2();
            }
        }

        private void setOtherPosition(int position) {
            if (mainIsFirst) {
                this.position2 = position;
            } else {
                this.position1 = position;
            }
        }

    }


    private static class PositionMaxima {

        private final Map<String, PositionMax> maximumMap = new HashMap<String, PositionMax>();

        private PositionMaxima() {
        }

        private void checkJunction(Junction junction, boolean firstPosition) {
            for (Tie tie : junction.getTieList()) {
                PositionMax positionMax = maximumMap.get(tie.getMode());
                if (positionMax == null) {
                    positionMax = new PositionMax();
                    maximumMap.put(tie.getMode(), positionMax);
                }
                positionMax.checkTie(tie, firstPosition);
            }
        }

        private int getNewPosition(String mode) {
            PositionMax positionMax = maximumMap.get(mode);
            if (positionMax == null) {
                positionMax = new PositionMax();
                maximumMap.put(mode, positionMax);
            }
            return positionMax.getNewPosition();
        }

    }


    private static class PositionMax {

        private int max = 0;

        private void checkTie(Tie tie, boolean firstPosition) {
            int pos = (firstPosition) ? tie.getPosition1() : tie.getPosition2();
            max = Math.max(max, pos);
        }

        private int getNewPosition() {
            max = max + 1;
            return max;
        }

    }

    private static String toStringKey(SubsetItem subsetItem) {
        return subsetItem.getSubsetKey() + "/" + subsetItem.getId();
    }

}
