/* FichothequeLib_Impl - Copyright (c) 2006-2023 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.impl;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import net.fichotheque.EditOrigin;
import net.fichotheque.ExistingIdException;
import net.fichotheque.Fichotheque;
import net.fichotheque.FichothequeEditor;
import net.fichotheque.Metadata;
import net.fichotheque.SubsetItem;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.thesaurus.Motcle;
import net.fichotheque.thesaurus.ParentRecursivityException;
import net.fichotheque.thesaurus.Thesaurus;
import net.fichotheque.thesaurus.ThesaurusEditor;
import net.fichotheque.thesaurus.metadata.ThesaurusMetadata;
import net.fichotheque.thesaurus.metadata.ThesaurusMetadataEditor;
import net.fichotheque.utils.FichothequeUtils;
import net.fichotheque.utils.ThesaurusUtils;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.text.Idalpha;
import net.mapeadores.util.text.Label;
import net.mapeadores.util.text.collation.map.SortedCollatedKeyMap;


/**
 *
 * @author Vincent Calame
 */
class ThesaurusImpl implements Thesaurus {

    private final SubsetKey thesaurusKey;
    private final FichothequeImpl fichotheque;
    private final short thesaurusType;
    private final Map<String, Motcle> idalphaMap;
    private final SortedCollatedKeyMap<List<Motcle>> babelienMap;
    private final Map<Lang, SortedCollatedKeyMap<List<Motcle>>> labelMaps;
    private final Satellites satellites = new Satellites();
    private final Motcles firstLevelMotcles = new Motcles();
    private final Map<Integer, AbstractMotcle> itemMap = new HashMap<Integer, AbstractMotcle>();
    private ThesaurusMetadataImpl thesaurusMetadata;
    private int availableId = 1;
    private List<SubsetItem> cacheSubsetItemList;
    private List<Motcle> cacheList;


    private ThesaurusImpl(SubsetKey thesaurusKey, short thesaurusType, FichothequeImpl fichotheque) {
        this.thesaurusKey = thesaurusKey;
        this.fichotheque = fichotheque;
        this.thesaurusType = thesaurusType;
        if (thesaurusType == ThesaurusMetadata.IDALPHA_TYPE) {
            idalphaMap = new HashMap<String, Motcle>();
        } else {
            idalphaMap = null;
        }
        if (thesaurusType == ThesaurusMetadata.BABELIEN_TYPE) {
            babelienMap = new SortedCollatedKeyMap<List<Motcle>>(Locale.FRENCH);
            labelMaps = null;
        } else {
            babelienMap = null;
            labelMaps = new HashMap<Lang, SortedCollatedKeyMap<List<Motcle>>>();
        }
    }

    static ThesaurusImpl.InitEditor fromInit(SubsetKey thesaurusKey, short thesaurusType, FichothequeImpl fichotheque, FichothequeImpl.InitEditor fichothequeInitEditor) {
        ThesaurusImpl thesaurus = new ThesaurusImpl(thesaurusKey, thesaurusType, fichotheque);
        return new ThesaurusImpl.InitEditor(thesaurus, fichothequeInitEditor);
    }

    static ThesaurusImpl fromNew(SubsetKey thesaurusKey, short thesaurusType, FichothequeImpl fichotheque) {
        ThesaurusImpl thesaurus = new ThesaurusImpl(thesaurusKey, thesaurusType, fichotheque);
        thesaurus.thesaurusMetadata = ThesaurusMetadataImpl.fromNew(thesaurus);
        return thesaurus;
    }

    @Override
    public Metadata getMetadata() {
        return thesaurusMetadata;
    }

    @Override
    public Fichotheque getFichotheque() {
        return fichotheque;
    }

    @Override
    public SubsetKey getSubsetKey() {
        return thesaurusKey;
    }

    @Override
    public SubsetItem getSubsetItemById(int id) {
        return itemMap.get(id);
    }

    @Override
    public int size() {
        return itemMap.size();
    }

    @Override
    public List<Motcle> getMotcleList() {
        List<Motcle> result = cacheList;
        if (result == null) {
            result = initMotcleList();
        }
        return result;
    }

    @Override
    public List<SubsetItem> getSubsetItemList() {
        List<SubsetItem> list = cacheSubsetItemList;
        if (list == null) {
            list = initSubsetItemList();
        }
        return list;
    }

    @Override
    public List<Motcle> getFirstLevelList() {
        return firstLevelMotcles.getCache();
    }

    @Override
    public Motcle getMotcleById(int id) {
        return itemMap.get(id);
    }

    @Override
    public Motcle getMotcleByIdalpha(String idalpha) {
        if (idalphaMap == null) {
            throw new UnsupportedOperationException("thesarus has no idalpha");
        }
        return idalphaMap.get(idalpha);
    }

    @Override
    public Motcle getMotcleByLabel(String labelString, Lang lang) {
        SortedCollatedKeyMap<List<Motcle>> map = getLabelMap(lang);
        List<Motcle> liste = map.getValue(labelString);
        if (liste == null) {
            return null;
        }
        return liste.get(0);
    }

    @Override
    public List<Corpus> getSatelliteCorpusList() {
        return satellites.getCache();
    }

    Motcles getFirstLevelMotcleList() {
        return firstLevelMotcles;
    }

    short getThesaurusType() {
        return thesaurusType;
    }

    void addSatelliteCorpus(CorpusImpl bdfimplCorpus) {
        satellites.add(bdfimplCorpus);
    }

    void removeSatelliteCorpus(CorpusImpl bdfimplCorpus) {
        satellites.remove(bdfimplCorpus);
    }

    ThesaurusEditorImpl getThesaurusEditor(FichothequeEditor fichothequeEditor) {
        return new ThesaurusEditorImpl(fichothequeEditor);
    }

    private synchronized Motcle innerCreateMotcle(int id, String idalpha) throws ExistingIdException, ParseException {
        if (id < 1) {
            id = availableId;
        } else {
            if (itemMap.containsKey(id)) {
                throw new ExistingIdException();
            }
        }
        if (idalphaMap != null) {
            if ((idalpha == null) || (idalpha.length() == 0)) {
                idalpha = "_" + id;
                while (true) {
                    if (idalphaMap.get(idalpha) != null) {
                        idalpha = idalpha + "_";
                    } else {
                        break;
                    }
                }
            } else {
                Idalpha.test(idalpha);
                if (idalphaMap.get(idalpha) != null) {
                    throw new ExistingIdException();
                }
            }
        } else {
            idalpha = null;
        }
        AbstractMotcle motcle;
        if (thesaurusType == ThesaurusMetadata.BABELIEN_TYPE) {
            motcle = new BabelienMotcle(this, id);
        } else {
            motcle = new MultiMotcle(this, id);
        }
        if (idalpha != null) {
            motcle.setIdalpha(idalpha);
            idalphaMap.put(idalpha, motcle);
        }
        itemMap.put(id, motcle);
        addInFirstLevel(motcle);
        availableId = Math.max(availableId, id + 1);
        clearCache();
        return motcle;
    }

    private synchronized boolean innerRemoveMotcle(Motcle motcle) {
        AbstractMotcle am = testMotcle(motcle);
        if (!fichotheque.isRemoveable(am)) {
            return false;
        }
        int id = motcle.getId();
        am.removeFromParent();
        if (idalphaMap != null) {
            idalphaMap.remove(am.getIdalpha());
        }
        for (Label label : am.getLabels()) {
            SortedCollatedKeyMap map = getLabelMap(label.getLang());
            remove(map, label.getLabelString(), am);
        }
        itemMap.remove(id);
        clearCache();
        return true;
    }

    private synchronized boolean innerSetChildIndex(Motcle motcle, int index) {
        AbstractMotcle am = testMotcle(motcle);
        boolean done = am.changeIndex(index);
        if (done) {
            clearCache();
        }
        return done;
    }

    private synchronized boolean innerSetIdalpha(Motcle motcle, String newIdalpha) throws ExistingIdException, ParseException {
        if (idalphaMap == null) {
            throw new UnsupportedOperationException("this is not a thesaurus with idalpha");
        }
        Idalpha.test(newIdalpha);
        AbstractMotcle am = testMotcle(motcle);
        Object obj = idalphaMap.get(newIdalpha);
        if (obj != null) {
            if (!obj.equals(am)) {
                throw new ExistingIdException();
            } else {
                return false;
            }
        }
        String oldIdalpha = am.getIdalpha();
        boolean done = am.setIdalpha(newIdalpha);
        if (done) {
            idalphaMap.remove(oldIdalpha);
            idalphaMap.put(newIdalpha, motcle);
        }
        return done;
    }

    private boolean innerPutLabel(Motcle motcle, Label label) {
        AbstractMotcle am = testMotcle(motcle);
        Lang lang = label.getLang();
        Label oldLabel = am.getLabels().getLabel(lang);
        boolean done = am.putLabel(label);
        if (done) {
            SortedCollatedKeyMap<List<Motcle>> map = getLabelMap(lang);
            if (oldLabel != null) {
                remove(map, oldLabel.getLabelString(), am);
            }
            add(map, label.getLabelString(), am);
        }
        return done;
    }

    private boolean innerRemoveLabel(Motcle motcle, Lang lang) {
        AbstractMotcle am = testMotcle(motcle);
        Label oldLabel = am.getLabels().getLabel(lang);
        boolean done = am.removeLabel(lang);
        if (done) {
            if (oldLabel != null) {
                SortedCollatedKeyMap<List<Motcle>> map = getLabelMap(lang);
                remove(map, oldLabel.getLabelString(), am);
            }
        }
        return done;
    }

    private synchronized boolean innerSetParent(Motcle motcle, Motcle parentMotcle) throws ParentRecursivityException {
        AbstractMotcle am = testMotcle(motcle);
        boolean done;
        if (parentMotcle == null) {
            done = am.setParent(null);
        } else {
            AbstractMotcle amparent = testMotcle(parentMotcle);
            if (am.equals(amparent)) {
                throw new IllegalArgumentException("motcle and parentMotcle are the same");
            }
            if (ThesaurusUtils.isDescendant(amparent, am)) {
                throw new ParentRecursivityException(am, amparent);
            }
            done = am.setParent(amparent);
        }
        if (done) {
            clearCache();
        }
        return done;
    }

    private boolean innerSetStatus(Motcle motcle, String status) {
        AbstractMotcle am = testMotcle(motcle);
        boolean done = am.setStatus(status);
        return done;
    }

    private synchronized void clearCache() {
        cacheSubsetItemList = null;
        cacheList = null;
    }

    private synchronized List<SubsetItem> initSubsetItemList() {
        SortedMap<Integer, AbstractMotcle> sortedMap = new TreeMap<Integer, AbstractMotcle>();
        sortedMap.putAll(itemMap);
        List<SubsetItem> list = FichothequeUtils.wrap(sortedMap.values().toArray(new Motcle[sortedMap.size()]));
        cacheSubsetItemList = list;
        return list;
    }

    private synchronized List<Motcle> initMotcleList() {
        List<Motcle> list = new ArrayList<Motcle>();
        for (Motcle motcle : firstLevelMotcles.getCache()) {
            addInList(list, motcle);
        }
        cacheList = list;
        return list;
    }

    private void addInList(List<Motcle> list, Motcle motcle) {
        list.add(motcle);
        for (Motcle child : motcle.getChildList()) {
            addInList(list, child);
        }
    }


    static class InitEditor implements ThesaurusEditor {

        private final ThesaurusImpl thesaurus;
        private final FichothequeImpl.InitEditor fichothequeInitEditor;
        private final ThesaurusMetadataImpl.InitEditor metadataInitEditor;

        private InitEditor(ThesaurusImpl thesaurus, FichothequeImpl.InitEditor fichothequeInitEditor) {
            this.thesaurus = thesaurus;
            this.fichothequeInitEditor = fichothequeInitEditor;
            this.metadataInitEditor = ThesaurusMetadataImpl.fromInit(this);
            thesaurus.thesaurusMetadata = (ThesaurusMetadataImpl) metadataInitEditor.getMetadata();
        }

        @Override
        public Thesaurus getThesaurus() {
            return thesaurus;
        }

        @Override
        public FichothequeEditor getFichothequeEditor() {
            return fichothequeInitEditor;
        }

        @Override
        public ThesaurusMetadataEditor getThesaurusMetadataEditor() {
            return metadataInitEditor;
        }

        @Override
        public Motcle createMotcle(int id, String idalpha) throws ExistingIdException, ParseException {
            return thesaurus.innerCreateMotcle(id, idalpha);
        }

        @Override
        public boolean removeMotcle(Motcle motcle) {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public boolean setChildIndex(Motcle motcle, int index) {
            return thesaurus.innerSetChildIndex(motcle, index);
        }

        @Override
        public boolean setIdalpha(Motcle motcle, String newIdalpha) throws ExistingIdException, ParseException {
            return thesaurus.innerSetIdalpha(motcle, newIdalpha);
        }

        @Override
        public boolean putLabel(Motcle motcle, Label label) {
            return thesaurus.innerPutLabel(motcle, label);
        }

        @Override
        public boolean removeLabel(Motcle motcle, Lang lang) {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public boolean setParent(Motcle motcle, Motcle parentMotcle) throws ParentRecursivityException {
            return thesaurus.innerSetParent(motcle, parentMotcle);
        }

        @Override
        public boolean setStatus(Motcle motcle, String status) {
            return thesaurus.innerSetStatus(motcle, status);
        }

    }


    class ThesaurusEditorImpl implements ThesaurusEditor {

        private final FichothequeEditor fichothequeEditor;
        private final Set<Integer> changedMotcleSet = new HashSet<Integer>();
        private final Set<Integer> removedMotcleSet = new HashSet<Integer>();
        private ThesaurusMetadataEditor thesaurusMetadataEditor;
        private boolean treeChanged = false;
        private boolean metadataChanged = false;

        private ThesaurusEditorImpl(FichothequeEditor fichothequeEditor) {
            this.fichothequeEditor = fichothequeEditor;

        }

        @Override
        public Thesaurus getThesaurus() {
            return ThesaurusImpl.this;
        }

        @Override
        public FichothequeEditor getFichothequeEditor() {
            return fichothequeEditor;
        }

        @Override
        public ThesaurusMetadataEditor getThesaurusMetadataEditor() {
            if (thesaurusMetadataEditor == null) {
                thesaurusMetadataEditor = thesaurusMetadata.getThesaurusMetadataEditor(this);
            }
            return thesaurusMetadataEditor;
        }

        @Override
        public Motcle createMotcle(int id, String idalpha) throws ExistingIdException, ParseException {
            Motcle motcle = innerCreateMotcle(id, idalpha);
            addMotcleChange(id);
            setTreeChange();
            return motcle;
        }

        @Override
        public boolean removeMotcle(Motcle motcle) {
            int id = motcle.getId();
            boolean done = innerRemoveMotcle(motcle);
            if (done) {
                removedMotcleSet.add(id);
                setTreeChange();
            }
            return done;
        }

        @Override
        public boolean setChildIndex(Motcle motcle, int index) {
            boolean done = innerSetChildIndex(motcle, index);
            if (done) {
                setTreeChange();
            }
            return done;
        }

        @Override
        public boolean setIdalpha(Motcle motcle, String newIdalpha) throws ExistingIdException, ParseException {
            boolean done = innerSetIdalpha(motcle, newIdalpha);
            if (done) {
                setTreeChange();
            }
            return done;
        }

        @Override
        public boolean putLabel(Motcle motcle, Label label) {
            boolean done = innerPutLabel(motcle, label);
            if (done) {
                addMotcleChange(motcle.getId());
            }
            return done;
        }

        @Override
        public boolean removeLabel(Motcle motcle, Lang lang) {
            boolean done = innerRemoveLabel(motcle, lang);
            if (done) {
                addMotcleChange(motcle.getId());
            }
            return done;
        }

        @Override
        public boolean setParent(Motcle motcle, Motcle parentMotcle) throws ParentRecursivityException {
            boolean done = innerSetParent(motcle, parentMotcle);
            if (done) {
                setTreeChange();
            }
            return done;
        }

        @Override
        public boolean setStatus(Motcle motcle, String status) {
            boolean done = innerSetStatus(motcle, status);
            if (done) {
                setTreeChange();
            }
            return done;
        }

        void addMotcleChange(int id) {
            changedMotcleSet.add(id);
        }

        void setTreeChange() {
            treeChanged = true;
        }

        void setMetadataChange() {
            metadataChanged = true;
        }

        void saveChanges() {
            EditOrigin editOrigin = fichothequeEditor.getEditOrigin();
            ThesaurusDataSource thesaurusDataSource = fichotheque.getFichothequeDataSource().getThesaurusDataSource();
            for (Integer id : changedMotcleSet) {
                Motcle motcle = getMotcleById(id);
                if (motcle != null) {
                    thesaurusDataSource.saveMotcle(motcle, editOrigin);
                }
            }
            changedMotcleSet.clear();
            for (Integer id : removedMotcleSet) {
                thesaurusDataSource.removeMotcle(ThesaurusImpl.this, id, editOrigin);
            }
            removedMotcleSet.clear();
            if (treeChanged) {
                thesaurusDataSource.saveTree(ThesaurusImpl.this, editOrigin);
                treeChanged = false;
            }
            if (metadataChanged) {
                thesaurusDataSource.saveMetadata(ThesaurusImpl.this, editOrigin);
                metadataChanged = false;
            }
        }

    }

    /**
     * Appelé par AbstractMotcle
     */
    void removeFromFirstLevel(int index) {
        firstLevelMotcles.remove(index);
    }

    void addInFirstLevel(AbstractMotcle motcle) {
        firstLevelMotcles.add(motcle);
    }

    private AbstractMotcle testMotcle(Motcle motcle) {
        if (motcle == null) {
            throw new IllegalArgumentException("motcle argument cannot be null");
        }
        try {
            AbstractMotcle am = (AbstractMotcle) motcle;
            if (am.getThesaurus() != this) {
                throw new IllegalArgumentException("motcle argument does not come from this thesaurus");
            }
            return am;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("motcle argument does not come from this thesaurus");
        }
    }

    private void remove(SortedCollatedKeyMap map, String lib, Object obj) {
        List liste = (List) map.getValue(lib);
        if (liste.size() == 1) {
            if (!liste.get(0).equals(obj)) {
                throw new IllegalStateException(obj.toString() + " does not match " + liste.get(0));
            }
            map.removeValue(lib);
        } else {
            liste.remove(obj);
        }
    }

    private void add(SortedCollatedKeyMap<List<Motcle>> map, String lib, Motcle motcle) {
        List<Motcle> liste = map.getValue(lib);
        if (liste == null) {
            map.putValue(lib, Collections.singletonList(motcle));
        } else if (liste.size() == 1) {
            List<Motcle> newList = new ArrayList<Motcle>();
            newList.add(liste.get(0));
            newList.add(motcle);
            map.putValue(lib, newList);
        } else {
            liste.add(motcle);
        }
    }

    private SortedCollatedKeyMap<List<Motcle>> getLabelMap(Lang lang) {
        if (babelienMap != null) {
            return babelienMap;
        } else {
            SortedCollatedKeyMap<List<Motcle>> map = labelMaps.get(lang);
            if (map == null) {
                map = new SortedCollatedKeyMap<List<Motcle>>(lang.toLocale());
                labelMaps.put(lang, map);
            }
            return map;
        }
    }

}
