/* 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.croisement;

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.croisement.Croisement;
import net.fichotheque.croisement.CroisementChange;
import net.fichotheque.croisement.CroisementChanges;
import net.fichotheque.croisement.CroisementKey;
import net.fichotheque.croisement.Croisements;
import net.fichotheque.croisement.Lien;
import net.fichotheque.include.IncludeKey;
import net.fichotheque.utils.CroisementUtils;


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

    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, NewCroisement> croisementMap = new LinkedHashMap<String, NewCroisement>();
    private final Map<SubsetKey, SubsetInfo> subsetMap = new HashMap<SubsetKey, SubsetInfo>();
    private final boolean withIncludeKeyFilter;

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

    private CroisementChangeEngine(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 CroisementChangeEngine() {
        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 addCroisements(Croisements croisements, Predicate<SubsetItem> predicate) {
        for (Croisements.Entry entry : croisements.getEntryList()) {
            SubsetItem otherSubsetItem = entry.getSubsetItem();
            if (predicate.test(otherSubsetItem)) {
                for (Lien lien : entry.getCroisement().getLienList()) {
                    try {
                        addLien(otherSubsetItem, lien.getMode(), lien.getWeight());
                    } catch (IllegalArgumentException iae) {
                    }
                }
            }
        }
    }

    public void addLien(LienBuffer lienBuffer) {
        addLien(lienBuffer.getSubsetItem(), lienBuffer.getMode(), lienBuffer.getWeight());
    }

    public void addLien(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 = (CroisementKey.getOrder(mainSubsetItem, otherSubsetItem) == 1);
        } else {
            mainInFirst = true;
        }
        NewLien newLien = new NewLien(otherSubsetItem, mode, weight, mainInFirst);
        boolean done = getOrCreateNewCroisement(otherSubsetItem).adNewLien(newLien);
        if (done) {
            subsetInfo.addNewLien(newLien);
        }
    }

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

    public CroisementChanges toCroisementChanges() {
        if (mainSubsetItem == null) {
            checkNew();
        } else {
            checkExisting();
        }
        CroisementChangesBuilder builder = new CroisementChangesBuilder();
        for (NewCroisement newCroisement : croisementMap.values()) {
            if (newCroisement.isRemoved()) {
                builder.addRemoved(newCroisement.otherSubsetItem);
            } else if (!newCroisement.isCancelled()) {
                CroisementChange croisementChange = newCroisement.toCroisementChange();
                builder.addEntry(newCroisement.otherSubsetItem, croisementChange);
            }
        }
        return builder.toCroisementChanges();
    }

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

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

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

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

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

    public static CroisementChangeEngine appendOrWeightMaxEngine(SubsetItem mainSubsetItem) {
        return new CroisementChangeEngine(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 NewCroisement getOrCreateNewCroisement(SubsetItem otherSubsetItem) {
        String stringKey = toStringKey(otherSubsetItem);
        NewCroisement newCroisement = croisementMap.get(stringKey);
        if (newCroisement == null) {
            newCroisement = new NewCroisement(otherSubsetItem);
            croisementMap.put(stringKey, newCroisement);
        }
        return newCroisement;
    }

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

    private void checkExisting() {
        Fichotheque fichotheque = mainSubsetItem.getFichotheque();
        for (Map.Entry<SubsetKey, SubsetInfo> entry : subsetMap.entrySet()) {
            SubsetInfo subsetInfo = entry.getValue();
            Croisements currentCroisements = fichotheque.getCroisements(mainSubsetItem, subsetInfo.subset);
            PositionMaxima positionMaxima = new PositionMaxima();
            if (changeType != CLEAR_EXISTING) {
                for (Croisements.Entry currentEntry : currentCroisements.getEntryList()) {
                    Croisement croisement = currentEntry.getCroisement();
                    positionMaxima.checkCroisement(croisement, (croisement.getCroisementKey().getOrder(mainSubsetItem) != 1));
                }
            }
            for (Croisements.Entry currentEntry : currentCroisements.getEntryList()) {
                SubsetItem otherSubsetItem = currentEntry.getSubsetItem();
                Croisement currentCroisement = currentEntry.getCroisement();
                String key = toStringKey(otherSubsetItem);
                NewCroisement newCroisement = croisementMap.get(key);
                if (newCroisement == null) {
                    if (changeType == CLEAR_EXISTING) {
                        newCroisement = initRemovedNewCroisement(subsetInfo, otherSubsetItem, currentCroisement);
                        croisementMap.put(key, newCroisement);
                    }
                } else {
                    for (Lien currentLien : currentCroisement.getLienList()) {
                        if (subsetInfo.containsMode(currentLien.getMode())) {
                            newCroisement.checkCurrentLien(currentLien, changeType);
                        }
                    }
                }
            }
            for (NewLien newLien : subsetInfo.newLienList) {
                if (!newLien.isCanceled()) {
                    if (newLien.isMainPositionUndefined()) {
                        PositionMaxima otherPositionMaxima = subsetInfo.getMainPositionMaxima(newLien.getOtherSubsetItem(), mainSubsetItem.getSubset());
                        newLien.setMainPosition(otherPositionMaxima.getNewPosition(newLien.getMode()));
                    }
                    if (newLien.isOtherPositionUndefined()) {
                        newLien.setOtherPosition(positionMaxima.getNewPosition(newLien.getMode()));
                    }
                }
            }
        }
    }

    private NewCroisement initRemovedNewCroisement(SubsetInfo subsetInfo, SubsetItem otherSubsetItem, Croisement croisement) {
        List<Lien> lienList = croisement.getLienList();
        int lienCount = lienList.size();
        int removeCount = 0;
        NewCroisement newCroisement = new NewCroisement(otherSubsetItem);
        for (Lien lien : lienList) {
            String mode = lien.getMode();
            ModeInfo modeInfo = subsetInfo.getModeInfo(mode);
            if (modeInfo != null) {
                int weight = lien.getWeight();
                if (modeInfo.containsWeight(weight)) {
                    newCroisement.addRemovedMode(mode);
                    removeCount++;
                }
            }
        }
        if (removeCount == lienCount) {
            newCroisement.setRemoveAll(true);
        }
        return newCroisement;
    }


    private static class SubsetInfo {

        private final Fichotheque fichotheque;
        private final Subset subset;
        private final Map<String, ModeInfo> modeInfoMap = new HashMap<String, ModeInfo>();
        private List<NewLien> newLienList = new ArrayList<NewLien>();
        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 addNewLien(NewLien newLien) {
            newLienList.add(newLien);
        }

        private PositionMaxima getMainPositionMaxima(SubsetItem otherSubsetItem, Subset mainSubset) {
            PositionMaxima positionMaxima = maximaMap.get(otherSubsetItem.getId());
            if (positionMaxima == null) {
                Croisements mainSubsetCroisements = fichotheque.getCroisements(otherSubsetItem, mainSubset);
                positionMaxima = new PositionMaxima();
                for (Croisements.Entry entry : mainSubsetCroisements.getEntryList()) {
                    Croisement croisement = entry.getCroisement();
                    positionMaxima.checkCroisement(croisement, (croisement.getCroisementKey().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 NewCroisement {

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

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

        private boolean adNewLien(NewLien newLien) {
            String mode = newLien.getMode();
            if (newLienMap.containsKey(mode)) {
                return false;
            }
            newLienMap.put(mode, newLien);
            removedModeSet.remove(mode);
            return true;
        }

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

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

        private NewLien getNewLien(String mode) {
            return newLienMap.get(mode);
        }

        private void cancelNewLien(String mode) {
            NewLien newLien = newLienMap.remove(mode);
            if (newLien != null) {
                newLien.cancelNewLien();
            }
        }

        private boolean isRemoved() {
            return removeAll;
        }

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

        private void checkCurrentLien(Lien currentLien, short changeType) {
            String mode = currentLien.getMode();
            if (removedModeSet.contains(mode)) {
                return;
            }
            NewLien newLien = getNewLien(mode);
            if (newLien == null) {
                if (changeType == CLEAR_EXISTING) {
                    addRemovedMode(mode);
                }
            } else if (changeType == APPEND_AND_WEIGHT_NOCHANGE) {
                cancelNewLien(mode);
            } else if (changeType == APPEND_AND_WEIGHT_MAX) {
                if (currentLien.getWeight() >= newLien.weight) {
                    cancelNewLien(mode);
                } else {
                    newLien.setPositions(currentLien);
                }
            } else if (changeType == APPEND_AND_WEIGHT_REPLACE) {
                if (currentLien.getWeight() == newLien.weight) {
                    cancelNewLien(mode);
                } else {
                    newLien.setPositions(currentLien);
                }
            } else if (changeType == CLEAR_EXISTING) {
                newLien.setMainPosition(currentLien);
            }
        }

        private CroisementChange toCroisementChange() {
            List<Lien> lienList = new ArrayList<Lien>();
            for (NewLien newLien : newLienMap.values()) {
                if (!newLien.isCanceled()) {
                    Lien lien = CroisementUtils.toLien(newLien.getMode(), newLien.weight, newLien.position1, newLien.position2);
                    lienList.add(lien);
                }
            }
            return CroisementChangeBuilder.toCroisementChange(removedModeSet, lienList);
        }

    }


    private static class NewLien {

        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 NewLien(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(Lien currentLien) {
            this.position1 = currentLien.getPosition1();
            this.position2 = currentLien.getPosition2();
        }

        private boolean isCanceled() {
            return canceled;
        }

        private void cancelNewLien() {
            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(Lien currentLien) {
            if (mainIsFirst) {
                this.position1 = currentLien.getPosition1();
            } else {
                this.position2 = currentLien.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 checkCroisement(Croisement croisement, boolean firstPosition) {
            for (Lien lien : croisement.getLienList()) {
                PositionMax positionMax = maximumMap.get(lien.getMode());
                if (positionMax == null) {
                    positionMax = new PositionMax();
                    maximumMap.put(lien.getMode(), positionMax);
                }
                positionMax.checkLien(lien, 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 checkLien(Lien lien, boolean firstPosition) {
            int pos = (firstPosition) ? lien.getPosition1() : lien.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();
    }

}
