/* FichothequeLib_Impl - Copyright (c) 2006-2022 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.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
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.Subset;
import net.fichotheque.SubsetItem;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.corpus.CorpusEditor;
import net.fichotheque.corpus.FicheMeta;
import net.fichotheque.corpus.NoMasterIdException;
import net.fichotheque.corpus.fiche.Fiche;
import net.fichotheque.corpus.fiche.FicheAPI;
import net.fichotheque.corpus.fiche.FicheItem;
import net.fichotheque.corpus.fiche.FicheItems;
import net.fichotheque.corpus.fiche.PersonItem;
import net.fichotheque.corpus.metadata.CorpusMetadataEditor;
import net.fichotheque.history.FicheHistory;
import net.fichotheque.namespaces.BdfSpace;
import net.fichotheque.utils.CorpusUtils;
import net.fichotheque.utils.FichothequeUtils;
import net.mapeadores.util.attr.Attribute;
import net.mapeadores.util.attr.AttributeKey;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.date.FuzzyDate;
import net.mapeadores.util.text.StringUtils;
import net.fichotheque.history.JunctionHistory;


/**
 *
 * @author Vincent Calame
 */
class CorpusImpl implements Corpus {

    private final SubsetKey corpusKey;
    private final FichothequeImpl fichotheque;
    private CorpusMetadataImpl corpusMetadata;
    private int availableId = 1;
    private final Satellites satellites = new Satellites();
    private Subset masterSubset;
    private List<SubsetItem> cacheSubsetItemList;
    private List<FicheMeta> cacheList;
    private final Map<Integer, FicheMetaImpl> itemMap = new HashMap<Integer, FicheMetaImpl>();

    private CorpusImpl(SubsetKey corpusKey, FichothequeImpl fichotheque, Subset masterSubset) {
        this.fichotheque = fichotheque;
        this.corpusKey = corpusKey;
        this.masterSubset = masterSubset;
    }

    static CorpusImpl.InitEditor fromInit(SubsetKey corpusKey, FichothequeImpl fichotheque, Subset masterSubset, FichothequeImpl.InitEditor fichothequeInitEditor) {
        CorpusImpl corpus = new CorpusImpl(corpusKey, fichotheque, masterSubset);
        return new CorpusImpl.InitEditor(corpus, fichothequeInitEditor);
    }

    static CorpusImpl fromNew(SubsetKey corpusKey, FichothequeImpl fichotheque, Subset masterSubset) {
        CorpusImpl corpus = new CorpusImpl(corpusKey, fichotheque, masterSubset);
        corpus.corpusMetadata = CorpusMetadataImpl.fromNew(corpus);
        return corpus;
    }

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

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

    @Override
    public FicheMeta getFicheMetaById(int id) {
        return itemMap.get(id);
    }

    @Override
    public Fiche getFicheAPI(FicheMeta ficheMeta, boolean withSection) {
        return getCorpusDataSource().getFiche(ficheMeta, withSection);
    }

    @Override
    public Fiche getFiche(FicheMeta ficheMeta) {
        return getCorpusDataSource().getFiche(ficheMeta, true);
    }

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

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

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

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

    @Override
    public List<FicheMeta> getFicheMetaList() {
        List<FicheMeta> result = cacheList;
        if (result == null) {
            result = initFicheMetaList();
        }
        return result;
    }

    @Override
    public Subset getMasterSubset() {
        return masterSubset;
    }

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

    @Override
    public FicheHistory getFicheHistory(int id) {
        return getCorpusDataSource().getFicheHistory(corpusKey, id);
    }

    @Override
    public List<FicheHistory> getRemovedFicheHistoryList() {
        return getCorpusDataSource().getRemovedFicheHistoryList(this);
    }

    @Override
    public List<JunctionHistory> getJunctionHistoryList(int ficheId, SubsetKey subsetKey) {
        return getCorpusDataSource().getJunctionHistoryList(corpusKey, ficheId, subsetKey);
    }

    @Override
    public FicheAPI getFicheRevision(int id, String revisionName) {
        return getCorpusDataSource().getFicheRevision(corpusKey, id, revisionName);
    }

    CorpusEditorImpl getCorpusEditor(FichothequeEditor fichothequeEditor) {
        return new CorpusEditorImpl(fichothequeEditor);
    }

    void clear() {
        corpusMetadata.clear();
        masterSubset = null;
    }

    Listeners getListeners() {
        return fichotheque.getListeners();
    }

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

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

    private CorpusDataSource getCorpusDataSource() {
        return fichotheque.getFichothequeDataSource().getCorpusDataSource();
    }

    private synchronized FicheMeta innerCreateFiche(int id) throws ExistingIdException, NoMasterIdException {
        if (masterSubset != null) {
            SubsetItem subsetItem = masterSubset.getSubsetItemById(id);
            if (subsetItem == null) {
                throw new NoMasterIdException();
            }
            if (itemMap.containsKey(id)) {
                throw new ExistingIdException();
            }
        } else {
            if (id < 1) {
                id = availableId;
            } else if (itemMap.containsKey(id)) {
                throw new ExistingIdException();
            }
        }
        FicheMetaImpl ficheMeta = new FicheMetaImpl(id);
        itemMap.put(id, ficheMeta);
        availableId = Math.max(availableId, id + 1);
        clearCache();
        return ficheMeta;
    }

    private boolean innerSetDate(FicheMeta ficheMeta, FuzzyDate date, boolean modification) {
        FicheMetaImpl ficheMetaImpl = testFicheMeta(ficheMeta);
        return ficheMetaImpl.setDate(date, modification);
    }

    private void innerUpdateFiche(FicheMeta ficheMeta, FicheAPI fiche) {
        FicheMetaImpl ficheMetaImpl = testFicheMeta(ficheMeta);
        ficheMetaImpl.update(fiche);
    }

    private synchronized boolean innerRemoveFiche(FicheMeta ficheMeta) {
        FicheMetaImpl ficheMetaImpl = testFicheMeta(ficheMeta);
        if (!fichotheque.isRemoveable(ficheMetaImpl)) {
            return false;
        }
        int id = ficheMetaImpl.getId();
        itemMap.remove(id);
        clearCache();
        return true;
    }

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

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

    private synchronized List<FicheMeta> initFicheMetaList() {
        SortedMap<Integer, FicheMetaImpl> sortedMap = new TreeMap<Integer, FicheMetaImpl>();
        sortedMap.putAll(itemMap);
        List<FicheMeta> list = CorpusUtils.wrap(sortedMap.values().toArray(new FicheMeta[sortedMap.size()]));
        cacheList = list;
        return list;
    }


    static class InitEditor implements CorpusEditor {

        private final CorpusImpl corpus;
        private final FichothequeImpl.InitEditor fichothequeInitEditor;
        private final CorpusMetadataImpl.InitEditor metadataInitEditor;

        private InitEditor(CorpusImpl corpus, FichothequeImpl.InitEditor fichothequeInitEditor) {
            this.corpus = corpus;
            this.fichothequeInitEditor = fichothequeInitEditor;
            this.metadataInitEditor = CorpusMetadataImpl.fromInit(this);
            corpus.corpusMetadata = (CorpusMetadataImpl) metadataInitEditor.getMetadata();
        }

        @Override
        public Corpus getCorpus() {
            return corpus;
        }

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

        @Override
        public CorpusMetadataEditor getCorpusMetadataEditor() {
            return metadataInitEditor;
        }

        @Override
        public FicheMeta createFiche(int id) throws ExistingIdException, NoMasterIdException {
            return corpus.innerCreateFiche(id);
        }

        @Override
        public void saveFiche(FicheMeta ficheMeta, FicheAPI fiche) {
            corpus.innerUpdateFiche(ficheMeta, fiche);
        }

        @Override
        public boolean removeFiche(FicheMeta ficheMeta) {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public void setDate(FicheMeta ficheMeta, FuzzyDate date, boolean modification) {
            if (date == null) {
                throw new IllegalArgumentException("date is null");
            }
            corpus.innerSetDate(ficheMeta, date, modification);
        }

    }


    class CorpusEditorImpl implements CorpusEditor {

        private final FichothequeEditor fichothequeEditor;
        private final Set<Integer> chronoChangeSet = new HashSet<Integer>();
        private final Set<Integer> attributesChangeSet = new HashSet<Integer>();
        private final Set<Integer> removedFicheSet = new HashSet<Integer>();
        private CorpusMetadataEditor corpusMetadataEditor;
        private boolean metadataChanged = false;
        private boolean commitNeeded = false;

        private CorpusEditorImpl(FichothequeEditor fichothequeEditor) {
            this.fichothequeEditor = fichothequeEditor;
        }

        @Override
        public FicheMeta createFiche(int id) throws ExistingIdException, NoMasterIdException {
            FicheMeta ficheMeta = innerCreateFiche(id);
            getListeners().fireSubsetItemCreated(fichothequeEditor, ficheMeta);
            CorpusDataSource corpusDataSource = getCorpusDataSource();
            corpusDataSource.saveFiche(ficheMeta, new Fiche(), fichothequeEditor.getEditOrigin());
            commitNeeded = true;
            return ficheMeta;
        }

        @Override
        public Corpus getCorpus() {
            return CorpusImpl.this;
        }

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

        @Override
        public CorpusMetadataEditor getCorpusMetadataEditor() {
            if (corpusMetadataEditor == null) {
                corpusMetadataEditor = corpusMetadata.getCorpusMetadataEditor(this);
            }
            return corpusMetadataEditor;
        }

        @Override
        public boolean removeFiche(FicheMeta ficheMeta) {
            int id = ficheMeta.getId();
            boolean done = innerRemoveFiche(ficheMeta);
            if (done) {
                removedFicheSet.add(id);
            }
            return done;
        }

        @Override
        public void saveFiche(FicheMeta ficheMeta, FicheAPI fiche) {
            innerUpdateFiche(ficheMeta, fiche);
            getCorpusDataSource().saveFiche(ficheMeta, fiche, fichothequeEditor.getEditOrigin());
            getListeners().fireFicheSaved(fichothequeEditor, ficheMeta, fiche);
            commitNeeded = true;
        }

        @Override
        public void setDate(FicheMeta ficheMeta, FuzzyDate date, boolean modification) {
            if (date == null) {
                throw new IllegalArgumentException("date is null");
            }
            boolean done = innerSetDate(ficheMeta, date, modification);
            if (done) {
                chronoChangeSet.add(ficheMeta.getId());
            }
        }

        void addAttributesChange(int id) {
            attributesChangeSet.add(id);
        }

        void setMetadataChange() {
            metadataChanged = true;
        }

        void saveChanges() {
            EditOrigin editOrigin = fichothequeEditor.getEditOrigin();
            CorpusDataSource corpusDataSource = getCorpusDataSource();
            for (Integer id : chronoChangeSet) {
                FicheMeta ficheMeta = getFicheMetaById(id);
                if (ficheMeta != null) {
                    corpusDataSource.saveChrono(ficheMeta, editOrigin);
                }
            }
            chronoChangeSet.clear();
            for (Integer id : attributesChangeSet) {
                FicheMeta ficheMeta = getFicheMetaById(id);
                if (ficheMeta != null) {
                    corpusDataSource.saveAttributes(ficheMeta, editOrigin);
                }
            }
            attributesChangeSet.clear();
            for (Integer id : removedFicheSet) {
                corpusDataSource.removeFiche(CorpusImpl.this, id, editOrigin);
                commitNeeded = true;
            }
            removedFicheSet.clear();
            if (metadataChanged) {
                corpusDataSource.saveMetadata(CorpusImpl.this, editOrigin);
                metadataChanged = false;
            }
            if (commitNeeded) {
                corpusDataSource.commitChanges();
                commitNeeded = false;
            }
        }

    }


    private class FicheMetaImpl extends AbstractSubsetItem implements FicheMeta {

        private final int id;
        private Lang lang = null;
        private String title = "";
        private FuzzyDate creationDate = null;
        private FuzzyDate modificationDate = null;
        private boolean discarded = false;
        private List<String> userGlobalIdList = StringUtils.EMPTY_STRINGLIST;

        private FicheMetaImpl(int id) {
            this.id = id;
        }

        @Override
        public Subset getSubset() {
            return CorpusImpl.this;
        }

        @Override
        public String getTitle() {
            return title;
        }

        @Override
        public int getId() {
            return id;
        }

        @Override
        public Lang getLang() {
            return lang;
        }

        @Override
        public FuzzyDate getModificationDate() {
            return modificationDate;
        }

        @Override
        public FuzzyDate getCreationDate() {
            return creationDate;
        }

        @Override
        public List<String> getUserGlobalIdList() {
            return userGlobalIdList;
        }

        @Override
        public boolean isDiscarded() {
            return discarded;
        }

        @Override
        protected boolean innerRemoveAttribute(AttributeKey attributeKey) {
            boolean done = super.innerRemoveAttribute(attributeKey);
            if ((done) && (attributeKey.equals(BdfSpace.DISCARDTEXT_KEY))) {
                discarded = false;
            }
            return done;
        }

        @Override
        protected boolean innerPutAttribute(Attribute attribute) {
            boolean done = super.innerPutAttribute(attribute);
            if (done) {
                AttributeKey attributeKey = attribute.getAttributeKey();
                if (attributeKey.equals(BdfSpace.DISCARDTEXT_KEY)) {
                    discarded = true;
                }
            }
            return done;
        }

        void update(FicheAPI fiche) {
            this.lang = fiche.getLang();
            this.title = fiche.getTitle();
            FicheItems ownerList = fiche.getOwners();
            if (ownerList == null) {
                this.userGlobalIdList = StringUtils.EMPTY_STRINGLIST;
            } else {
                List<String> tempList = new ArrayList<String>();
                for (FicheItem ficheItem : ownerList) {
                    if (ficheItem instanceof PersonItem) {
                        String userGlobalId = ((PersonItem) ficheItem).getRedacteurGlobalId();
                        if (userGlobalId != null) {
                            tempList.add(userGlobalId);
                        }
                    }
                }
                this.userGlobalIdList = StringUtils.toList(tempList);
            }
        }

        boolean setDate(FuzzyDate date, boolean modification) {
            int mask = 0;
            if (!modification) {
                if (FuzzyDate.compare(date, creationDate) != 0) {
                    mask = 1;
                    creationDate = date;
                    if (FuzzyDate.compare(modificationDate, date) < 0) {
                        modificationDate = date;
                        mask = 3;
                    }
                }
            } else {
                if (creationDate == null) {
                    creationDate = date;
                    modificationDate = date;
                    mask = 3;
                } else if ((!date.equals(modificationDate)) && (FuzzyDate.compare(date, creationDate) > 0)) {
                    modificationDate = date;
                    mask = 2;
                }
            }
            return (mask > 0);
        }

    }


    private FicheMetaImpl testFicheMeta(FicheMeta ficheMeta) {
        if (ficheMeta == null) {
            throw new IllegalArgumentException("ficheMeta argument cannot be null");
        }
        try {
            FicheMetaImpl ficheMetaImpl = (FicheMetaImpl) ficheMeta;
            if (ficheMetaImpl.getCorpus() != this) {
                throw new IllegalArgumentException("ficheMeta argument does not come from this corpus");
            }
            return ficheMetaImpl;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("ficheMeta argument does not come from this corpus");
        }
    }

}
