/* FichothequeLib_Impl - Copyright (c) 2006-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.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.ExistingSubsetException;
import net.fichotheque.Fichotheque;
import net.fichotheque.FichothequeEditor;
import net.fichotheque.FichothequeEditorProvider;
import net.fichotheque.FichothequeListener;
import net.fichotheque.NoRemoveableSubsetException;
import net.fichotheque.Subset;
import net.fichotheque.SubsetItem;
import net.fichotheque.SubsetKey;
import net.fichotheque.addenda.Addenda;
import net.fichotheque.addenda.AddendaEditor;
import net.fichotheque.album.Album;
import net.fichotheque.album.AlbumEditor;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.corpus.CorpusEditor;
import net.fichotheque.corpus.metadata.CorpusField;
import net.fichotheque.junction.JunctionKey;
import net.fichotheque.junction.JunctionRevision;
import net.fichotheque.metadata.FichothequeMetadata;
import net.fichotheque.metadata.FichothequeMetadataEditor;
import net.fichotheque.sphere.Redacteur;
import net.fichotheque.sphere.Sphere;
import net.fichotheque.sphere.SphereEditor;
import net.fichotheque.thesaurus.Motcle;
import net.fichotheque.thesaurus.Thesaurus;
import net.fichotheque.thesaurus.ThesaurusEditor;
import net.fichotheque.thesaurus.metadata.ThesaurusMetadata;
import net.fichotheque.utils.CorpusMetadataUtils;
import net.fichotheque.utils.FichothequeUtils;
import net.fichotheque.utils.SphereUtils;
import net.mapeadores.util.attr.Attribute;
import net.mapeadores.util.attr.AttributeKey;
import net.mapeadores.util.exceptions.SwitchException;
import net.mapeadores.util.html.HtmlCleaner;
import net.fichotheque.junction.JunctionChange;
import net.fichotheque.junction.Junction;
import net.fichotheque.junction.JunctionEditor;
import net.fichotheque.junction.Junctions;
import net.fichotheque.junction.JunctionChanges;


/**
 *
 * @author Vincent Calame
 */
public class FichothequeImpl implements Fichotheque {

    private final Map<SubsetKey, Subset> subsetMap = new HashMap<SubsetKey, Subset>();
    private final Map<JunctionKey, JunctionImpl> junctionMap = new HashMap<JunctionKey, JunctionImpl>();
    private final List<SphereImpl> sphereList = new ArrayList<SphereImpl>();
    private final List<CorpusImpl> corpusList = new ArrayList<CorpusImpl>();
    private final List<ThesaurusImpl> thesaurusList = new ArrayList<ThesaurusImpl>();
    private final List<AlbumImpl> albumList = new ArrayList<AlbumImpl>();
    private final List<AddendaImpl> addendaList = new ArrayList<AddendaImpl>();
    private List<Addenda> cacheAddendaList;
    private List<Album> cacheAlbumList;
    private List<Corpus> cacheCorpusList;
    private List<Sphere> cacheSphereList;
    private List<Thesaurus> cacheThesaurusList;
    private FichothequeMetadataImpl fichothequeMetadata;
    private final Listeners listeners = new Listeners();
    private FichothequeDataSource fichothequeDataSource;

    private FichothequeImpl() {

    }

    public static FichothequeImpl.InitEditor init() {
        FichothequeImpl fichotheque = new FichothequeImpl();
        return new InitEditor(fichotheque);
    }

    @Override
    public void addFichothequeListener(FichothequeListener fichothequeListener) {
        listeners.addFichothequeListener(fichothequeListener);
    }

    @Override
    public void removeFichothequeListener(FichothequeListener fichothequeListener) {
        listeners.removeFichothequeListener(fichothequeListener);
    }

    @Override
    public FichothequeMetadata getFichothequeMetadata() {
        return fichothequeMetadata;
    }

    @Override
    public List<Corpus> getCorpusList() {
        List<Corpus> result = cacheCorpusList;
        if (result == null) {
            result = initCorpusList();
        }
        return result;
    }

    @Override
    public List<Thesaurus> getThesaurusList() {
        List<Thesaurus> result = cacheThesaurusList;
        if (result == null) {
            result = initThesaurusList();
        }
        return result;
    }

    @Override
    public List<Sphere> getSphereList() {
        List<Sphere> result = cacheSphereList;
        if (result == null) {
            result = initSphereList();
        }
        return result;
    }

    @Override
    public List<Album> getAlbumList() {
        List<Album> result = cacheAlbumList;
        if (result == null) {
            result = initAlbumList();
        }
        return result;
    }


    @Override
    public List<Addenda> getAddendaList() {
        List<Addenda> result = cacheAddendaList;
        if (result == null) {
            result = initAddendaList();
        }
        return result;
    }

    @Override
    public Subset getSubset(SubsetKey subsetKey) {
        return subsetMap.get(subsetKey);
    }

    @Override
    public Junction getJunction(SubsetItem subsetItem1, SubsetItem subsetItem2) {
        JunctionKey junctionKey = JunctionKey.toJunctionKey(subsetItem1, subsetItem2);
        return junctionMap.get(junctionKey);
    }

    @Override
    public Junctions getJunctions(SubsetItem subsetItem, Subset subset) {
        return ((AbstractSubsetItem) subsetItem).getJunctions(subset);
    }

    @Override
    public boolean isRemoveable(Subset subset) {
        if (!subset.getSatelliteCorpusList().isEmpty()) {
            return false;
        }
        return (subset.size() == 0);
    }

    @Override
    public boolean isRemoveable(SubsetItem subsetItem) {
        int id = subsetItem.getId();
        for (Corpus corpus : subsetItem.getSubset().getSatelliteCorpusList()) {
            if (corpus.getSubsetItemById(id) != null) {
                return false;
            }
        }
        AbstractSubsetItem abstractSubsetItem = (AbstractSubsetItem) subsetItem;
        if (abstractSubsetItem.hasJunction()) {
            return false;
        }
        if (subsetItem instanceof Motcle) {
            if (!((Motcle) subsetItem).getChildList().isEmpty()) {
                return false;
            }
        } else if (subsetItem instanceof Redacteur) {
            Redacteur redacteur = (Redacteur) subsetItem;
            for (Corpus corpus : getCorpusList()) {
                List<CorpusField> personneFieldList = CorpusMetadataUtils.getCorpusFieldListByFicheItemType(corpus.getCorpusMetadata(), CorpusField.PERSON_FIELD, false);
                boolean test = SphereUtils.testRedacteursAndFields(corpus, redacteur, personneFieldList);
                if (test) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public JunctionRevision getJunctionRevision(JunctionKey junctionKey, String revisionName) {
        return fichothequeDataSource.getJunctionDataSource().getJunctionRevision(junctionKey, revisionName);
    }

    private FichothequeEditor getFichothequeEditor(EditOrigin editOrigin) {
        return new FichothequeEditorImpl(editOrigin);
    }

    Listeners getListeners() {
        return listeners;
    }

    FichothequeDataSource getFichothequeDataSource() {
        return fichothequeDataSource;
    }

    private void testCorpusCreate(SubsetKey corpusKey, Subset masterSubset) throws ExistingSubsetException {
        if (corpusKey == null) {
            throw new IllegalArgumentException("corpusKey == null");
        }
        if (!corpusKey.isCorpusSubset()) {
            throw new IllegalArgumentException("corpusKey.isCorpusKey() == false");
        }
        if (subsetMap.containsKey(corpusKey)) {
            throw new ExistingSubsetException();
        }
        if (masterSubset != null) {
            if (masterSubset instanceof CorpusImpl) {
                if (((CorpusImpl) masterSubset).getMasterSubset() != null) {
                    throw new IllegalArgumentException("masterSubset : " + masterSubset.getSubsetKeyString() + " cannot be a satellite subset");
                }
            } else if (!(masterSubset instanceof ThesaurusImpl)) {
                throw new IllegalArgumentException("bad implementation of masterSubset : " + masterSubset.getClass().getName());
            }
        }
    }

    private void testSphereCreate(SubsetKey sphereKey) throws ExistingSubsetException {
        if (sphereKey == null) {
            throw new IllegalArgumentException("sphereKey == null");
        }
        if (!sphereKey.isSphereSubset()) {
            throw new IllegalArgumentException("sphereKey.isSphereKey() == false");
        }
        if (subsetMap.containsKey(sphereKey)) {
            throw new ExistingSubsetException();
        }
    }

    private void testThesaurusCreate(SubsetKey thesaurusKey, String thesaurusType) throws ExistingSubsetException {
        ThesaurusMetadata.checkThesaurusType(thesaurusType);
        if (thesaurusKey == null) {
            throw new IllegalArgumentException("thesaurusKey == null");
        }
        if (!thesaurusKey.isThesaurusSubset()) {
            throw new IllegalArgumentException("thesaurusKey.isThesaurusKey() == false");
        }
        if (subsetMap.containsKey(thesaurusKey)) {
            throw new ExistingSubsetException();
        }
    }

    private void testAlbumCreate(SubsetKey albumKey) throws ExistingSubsetException {
        if (albumKey == null) {
            throw new IllegalArgumentException("albumKey == null");
        }
        if (!albumKey.isAlbumSubset()) {
            throw new IllegalArgumentException("albumKey.isAlbumKey() == false");
        }
        if (subsetMap.containsKey(albumKey)) {
            throw new ExistingSubsetException();
        }
    }

    private void testAddendaCreate(SubsetKey addendaKey) throws ExistingSubsetException {
        if (addendaKey == null) {
            throw new IllegalArgumentException("addendaKey == null");
        }
        if (!addendaKey.isAddendaSubset()) {
            throw new IllegalArgumentException("addendaKey.isAddendaKey() == false");
        }
        if (subsetMap.containsKey(addendaKey)) {
            throw new ExistingSubsetException();
        }
    }

    private synchronized void endCorpusCreation(CorpusImpl corpus, Subset masterSubset) {
        if (masterSubset != null) {
            if (masterSubset instanceof CorpusImpl) {
                CorpusImpl masterCorpus = (CorpusImpl) masterSubset;
                masterCorpus.addSatelliteCorpus(corpus);
            } else {
                ThesaurusImpl masterThesaurus = (ThesaurusImpl) masterSubset;
                masterThesaurus.addSatelliteCorpus(corpus);
            }
        }
        subsetMap.put(corpus.getSubsetKey(), corpus);
        corpusList.add(corpus);
        clearCache();
    }

    private synchronized void endSphereCreation(SphereImpl sphere) {
        subsetMap.put(sphere.getSubsetKey(), sphere);
        sphereList.add(sphere);
        clearCache();
    }

    private synchronized void endThesaurusCreation(ThesaurusImpl thesaurus) {
        subsetMap.put(thesaurus.getSubsetKey(), thesaurus);
        thesaurusList.add(thesaurus);
        clearCache();
    }

    private synchronized void endAlbumCreation(AlbumImpl album) {
        subsetMap.put(album.getSubsetKey(), album);
        albumList.add(album);
        clearCache();
    }

    private synchronized void endAddendaCreation(AddendaImpl addenda) {
        subsetMap.put(addenda.getSubsetKey(), addenda);
        addendaList.add(addenda);
        clearCache();
    }

    private synchronized void innerRemoveSphere(Sphere sphere) throws NoRemoveableSubsetException {
        SphereImpl sphereImpl = testSphere(sphere);
        if (!isRemoveable(sphere)) {
            throw new NoRemoveableSubsetException();
        }
        SubsetKey sphereKey = sphereImpl.getSubsetKey();
        sphereList.remove(sphereImpl);
        subsetMap.remove(sphereKey);
        clearCache();
    }

    private synchronized void innerRemoveThesaurus(Thesaurus thesaurus) throws NoRemoveableSubsetException {
        ThesaurusImpl thesaurusImpl = testThesaurus(thesaurus);
        if (!isRemoveable(thesaurus)) {
            throw new NoRemoveableSubsetException();
        }
        SubsetKey thesaurusKey = thesaurusImpl.getSubsetKey();
        thesaurusList.remove(thesaurusImpl);
        subsetMap.remove(thesaurusKey);
        clearCache();
    }

    private synchronized void innerRemoveAlbum(Album album) throws NoRemoveableSubsetException {
        AlbumImpl albumImpl = testAlbum(album);
        if (!isRemoveable(album)) {
            throw new NoRemoveableSubsetException();
        }
        SubsetKey albumKey = albumImpl.getSubsetKey();
        albumList.remove(albumImpl);
        subsetMap.remove(albumKey);
        albumImpl.clear();
        clearCache();
    }

    private synchronized void innerRemoveAddenda(Addenda addenda) throws NoRemoveableSubsetException {
        AddendaImpl addendaImpl = testAddenda(addenda);
        if (!isRemoveable(addenda)) {
            throw new NoRemoveableSubsetException();
        }
        SubsetKey addendaKey = addendaImpl.getSubsetKey();
        addendaList.remove(addendaImpl);
        subsetMap.remove(addendaKey);
        clearCache();
    }

    private synchronized void innerRemoveCorpus(Corpus corpus) throws NoRemoveableSubsetException {
        CorpusImpl corpusImpl = testCorpus(corpus);
        if (!isRemoveable(corpus)) {
            throw new NoRemoveableSubsetException();
        }
        SubsetKey corpusKey = corpusImpl.getSubsetKey();
        Subset masterSubset = corpusImpl.getMasterSubset();
        if (masterSubset != null) {
            if (masterSubset instanceof CorpusImpl) {
                ((CorpusImpl) masterSubset).removeSatelliteCorpus(corpusImpl);
            } else if (masterSubset instanceof ThesaurusImpl) {
                ((ThesaurusImpl) masterSubset).removeSatelliteCorpus(corpusImpl);
            } else {
                throw new SwitchException("masterSubset class = " + masterSubset.getClass().getName());
            }
        }
        corpusList.remove(corpusImpl);
        subsetMap.remove(corpusKey);
        corpusImpl.clear();
        clearCache();
    }

    private boolean innerPutAttribute(Object attributeHolder, Attribute attribute) {
        if (attributeHolder instanceof AbstractMetadata) {
            AbstractMetadata metadata = (AbstractMetadata) attributeHolder;
            return metadata.innerPutAttribute(attribute);
        } else if (attributeHolder instanceof AbstractSubsetItem) {
            AbstractSubsetItem subsetItem = (AbstractSubsetItem) attributeHolder;
            return subsetItem.innerPutAttribute(attribute);
        } else if (attributeHolder instanceof JunctionImpl) {
            JunctionImpl junction = (JunctionImpl) attributeHolder;
            return junction.innerPutAttribute(attribute);
        }
        return false;
    }

    private boolean innerRemoveAttribute(Object attributeHolder, AttributeKey attributeKey) {
        if (attributeHolder instanceof AbstractMetadata) {
            AbstractMetadata metadata = (AbstractMetadata) attributeHolder;
            return metadata.innerRemoveAttribute(attributeKey);
        } else if (attributeHolder instanceof AbstractSubsetItem) {
            AbstractSubsetItem subsetItem = (AbstractSubsetItem) attributeHolder;
            return subsetItem.innerRemoveAttribute(attributeKey);
        } else if (attributeHolder instanceof JunctionImpl) {
            JunctionImpl junction = (JunctionImpl) attributeHolder;
            return junction.innerRemoveAttribute(attributeKey);
        }
        return false;
    }

    private void clearCache() {
        cacheAddendaList = null;
        cacheAlbumList = null;
        cacheCorpusList = null;
        cacheSphereList = null;
        cacheThesaurusList = null;
    }


    public static class InitEditor implements FichothequeEditor {

        private final FichothequeImpl fichotheque;
        private final FichothequeMetadataImpl.InitEditor metadataInitEditor;
        private final Map<SubsetKey, Object> subsetEditorMap = new HashMap<SubsetKey, Object>();
        private final InitJunctionEditor initJunctionEditor;

        private InitEditor(FichothequeImpl fichotheque) {
            this.fichotheque = fichotheque;
            this.metadataInitEditor = FichothequeMetadataImpl.init(fichotheque, this);
            fichotheque.fichothequeMetadata = (FichothequeMetadataImpl) metadataInitEditor.getMetadata();
            this.initJunctionEditor = new InitJunctionEditor(fichotheque, this);
        }

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

        @Override
        public EditOrigin getEditOrigin() {
            throw new UnsupportedOperationException("No edit here, only init");
        }

        @Override
        public FichothequeMetadataEditor getFichothequeMetadataEditor() {
            return metadataInitEditor;
        }

        @Override
        public CorpusEditor createCorpus(SubsetKey corpusKey, Subset masterSubset) throws ExistingSubsetException {
            fichotheque.testCorpusCreate(corpusKey, masterSubset);
            CorpusImpl.InitEditor corpusInitEditor = CorpusImpl.fromInit(corpusKey, fichotheque, masterSubset, this);
            subsetEditorMap.put(corpusKey, corpusInitEditor);
            fichotheque.endCorpusCreation((CorpusImpl) corpusInitEditor.getCorpus(), masterSubset);
            return corpusInitEditor;
        }

        @Override
        public SphereEditor createSphere(SubsetKey sphereKey) throws ExistingSubsetException {
            fichotheque.testSphereCreate(sphereKey);
            SphereImpl.InitEditor sphereInitEditor = SphereImpl.fromInit(sphereKey, fichotheque, this);
            fichotheque.endSphereCreation((SphereImpl) sphereInitEditor.getSphere());
            return sphereInitEditor;
        }

        @Override
        public ThesaurusEditor createThesaurus(SubsetKey thesaurusKey, String thesaurusType) throws ExistingSubsetException {
            fichotheque.testThesaurusCreate(thesaurusKey, thesaurusType);
            ThesaurusImpl.InitEditor thesaurusInitEditor = ThesaurusImpl.fromInit(thesaurusKey, thesaurusType, fichotheque, this);
            fichotheque.endThesaurusCreation((ThesaurusImpl) thesaurusInitEditor.getThesaurus());
            return thesaurusInitEditor;
        }

        @Override
        public AlbumEditor createAlbum(SubsetKey albumKey) throws ExistingSubsetException {
            fichotheque.testAlbumCreate(albumKey);
            AlbumImpl.InitEditor albumInitEditor = AlbumImpl.fromInit(albumKey, fichotheque, this);
            fichotheque.endAlbumCreation((AlbumImpl) albumInitEditor.getAlbum());
            return albumInitEditor;
        }

        @Override
        public AddendaEditor createAddenda(SubsetKey addendaKey) throws ExistingSubsetException {
            fichotheque.testAddendaCreate(addendaKey);
            AddendaImpl.InitEditor addendaInitEditor = AddendaImpl.fromInit(addendaKey, fichotheque, this);
            fichotheque.endAddendaCreation((AddendaImpl) addendaInitEditor.getAddenda());
            return addendaInitEditor;
        }


        @Override
        public CorpusEditor getCorpusEditor(SubsetKey corpusKey) {
            return (CorpusEditor) subsetEditorMap.get(corpusKey);
        }

        @Override
        public AddendaEditor getAddendaEditor(SubsetKey addendaKey) {
            return (AddendaEditor) subsetEditorMap.get(addendaKey);
        }

        @Override
        public SphereEditor getSphereEditor(SubsetKey sphereKey) {
            return (SphereEditor) subsetEditorMap.get(sphereKey);
        }

        @Override
        public ThesaurusEditor getThesaurusEditor(SubsetKey thesaurusKey) {
            return (ThesaurusEditor) subsetEditorMap.get(thesaurusKey);
        }

        @Override
        public AlbumEditor getAlbumEditor(SubsetKey albumKey) {
            return (AlbumEditor) subsetEditorMap.get(albumKey);
        }

        @Override
        public JunctionEditor getJunctionEditor() {
            return initJunctionEditor;
        }

        @Override
        public void removeSphere(Sphere sphere) throws NoRemoveableSubsetException {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public void removeThesaurus(Thesaurus thesaurus) throws NoRemoveableSubsetException {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public void removeAlbum(Album album) throws NoRemoveableSubsetException {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public void removeCorpus(Corpus corpus) throws NoRemoveableSubsetException {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public boolean putAttribute(Object attributeHolder, Attribute attribute) {
            return fichotheque.innerPutAttribute(attributeHolder, attribute);
        }

        @Override
        public void removeAddenda(Addenda addenda) throws NoRemoveableSubsetException {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public boolean removeAttribute(Object attributeHolder, AttributeKey attributeKey) {
            return fichotheque.innerRemoveAttribute(attributeHolder, attributeKey);
        }

        public FichothequeEditorProvider endInit(FichothequeDataSource fichothequeDataSource, HtmlCleaner htmlCleaner) {
            fichotheque.fichothequeDataSource = fichothequeDataSource;
            return new FichothequeEditorProviderImpl(fichotheque, htmlCleaner);
        }

        @Override
        public void saveChanges() {
            throw new UnsupportedOperationException("Not during init");
        }

    }


    private static class InitJunctionEditor implements JunctionEditor {

        private final FichothequeImpl fichotheque;
        private final InitEditor initEditor;

        private InitJunctionEditor(FichothequeImpl fichotheque, InitEditor initEditor) {
            this.fichotheque = fichotheque;
            this.initEditor = initEditor;
        }

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

        @Override
        public boolean removeJunction(SubsetItem subsetItem1, SubsetItem subsetItem2) {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public Junction updateJunction(SubsetItem subsetItem1, SubsetItem subsetItem2, JunctionChange junctionChange) {
            JunctionKey junctionKey = JunctionKey.toJunctionKey(subsetItem1, subsetItem2);
            JunctionImpl junction = fichotheque.junctionMap.get(junctionKey);
            if (junction == null) {
                if (junctionChange.getChangedTieList().isEmpty()) {
                    return null;
                }
                junction = JunctionImpl.newInstance(junctionKey, (AbstractSubsetItem) subsetItem1, (AbstractSubsetItem) subsetItem2);
                fichotheque.junctionMap.put(junctionKey, junction);
            }
            junction.change(junctionChange);
            return junction;
        }

        @Override
        public void updateJunctions(SubsetItem mainSubsetItem, JunctionChanges junctionChanges) {
            for (JunctionChanges.Entry entry : junctionChanges.getEntryList()) {
                SubsetItem otherSubsetItem = entry.getSubsetItem();
                updateJunction(mainSubsetItem, otherSubsetItem, entry.getJunctionChange());
            }
        }

    }


    private class FichothequeEditorImpl implements FichothequeEditor {

        private final EditOrigin editOrigin;
        private final Map<SubsetKey, Object> editorBufferMap = new HashMap<SubsetKey, Object>();
        private FichothequeMetadataImpl.FichothequeMetadataEditorImpl fichothequeMetadataEditor;
        private JunctionEditorImpl junctionEditor;

        private FichothequeEditorImpl(EditOrigin editOrigin) {
            this.editOrigin = editOrigin;
        }

        @Override
        public Fichotheque getFichotheque() {
            return FichothequeImpl.this;
        }

        @Override
        public EditOrigin getEditOrigin() {
            return editOrigin;
        }

        @Override
        public FichothequeMetadataEditor getFichothequeMetadataEditor() {
            if (fichothequeMetadataEditor == null) {
                fichothequeMetadataEditor = fichothequeMetadata.getFichothequeMetadataEditor(this);
            }
            return fichothequeMetadataEditor;
        }

        @Override
        public JunctionEditor getJunctionEditor() {
            if (junctionEditor == null) {
                junctionEditor = new JunctionEditorImpl(this);
            }
            return junctionEditor;
        }

        @Override
        public CorpusEditor createCorpus(SubsetKey corpusKey, Subset masterSubset) throws ExistingSubsetException {
            testCorpusCreate(corpusKey, masterSubset);
            CorpusImpl corpus = CorpusImpl.fromNew(corpusKey, FichothequeImpl.this, masterSubset);
            endCorpusCreation(corpus, masterSubset);
            getListeners().fireSubsetCreated(this, corpus);
            CorpusImpl.CorpusEditorImpl corpusEditor = (CorpusImpl.CorpusEditorImpl) getCorpusEditor(corpusKey);
            corpusEditor.setMetadataChange();
            return corpusEditor;
        }

        @Override
        public SphereEditor createSphere(SubsetKey sphereKey) throws ExistingSubsetException {
            testSphereCreate(sphereKey);
            SphereImpl sphere = SphereImpl.fromNew(sphereKey, FichothequeImpl.this);
            endSphereCreation(sphere);
            getListeners().fireSubsetCreated(this, sphere);
            SphereImpl.SphereEditorImpl sphereEditor = (SphereImpl.SphereEditorImpl) getSphereEditor(sphereKey);
            sphereEditor.setMetadataChange();
            return sphereEditor;
        }

        @Override
        public ThesaurusEditor createThesaurus(SubsetKey thesaurusKey, String thesaurusType) throws ExistingSubsetException {
            testThesaurusCreate(thesaurusKey, thesaurusType);
            ThesaurusImpl thesaurus = ThesaurusImpl.fromNew(thesaurusKey, thesaurusType, FichothequeImpl.this);
            endThesaurusCreation(thesaurus);
            getListeners().fireSubsetCreated(this, thesaurus);
            ThesaurusImpl.ThesaurusEditorImpl thesaurusEditor = (ThesaurusImpl.ThesaurusEditorImpl) getThesaurusEditor(thesaurusKey);
            thesaurusEditor.setMetadataChange();
            return thesaurusEditor;
        }

        @Override
        public AlbumEditor createAlbum(SubsetKey albumKey) throws ExistingSubsetException {
            testAlbumCreate(albumKey);
            AlbumImpl album = AlbumImpl.fromNew(albumKey, FichothequeImpl.this);
            endAlbumCreation(album);
            getListeners().fireSubsetCreated(this, album);
            AlbumImpl.AlbumEditorImpl albumEditor = (AlbumImpl.AlbumEditorImpl) getAlbumEditor(albumKey);
            albumEditor.setMetadataChange();
            return albumEditor;
        }

        @Override
        public AddendaEditor createAddenda(SubsetKey addendaKey) throws ExistingSubsetException {
            testAddendaCreate(addendaKey);
            AddendaImpl addenda = AddendaImpl.fromNew(addendaKey, FichothequeImpl.this);
            endAddendaCreation(addenda);
            getListeners().fireSubsetCreated(this, addenda);
            AddendaImpl.AddendaEditorImpl addendaEditor = (AddendaImpl.AddendaEditorImpl) getAddendaEditor(addendaKey);
            addendaEditor.setMetadataChange();
            return addendaEditor;
        }

        @Override
        public CorpusEditor getCorpusEditor(SubsetKey corpusKey) {
            Object editor = editorBufferMap.get(corpusKey);
            if (editor != null) {
                return (CorpusEditor) editor;
            }
            CorpusImpl corpus = (CorpusImpl) subsetMap.get(corpusKey);
            if (corpus == null) {
                return null;
            }
            CorpusImpl.CorpusEditorImpl corpusEditor = corpus.getCorpusEditor(this);
            editorBufferMap.put(corpusKey, corpusEditor);
            return corpusEditor;
        }

        @Override
        public AddendaEditor getAddendaEditor(SubsetKey addendaKey) {
            Object editor = editorBufferMap.get(addendaKey);
            if (editor != null) {
                return (AddendaEditor) editor;
            }
            AddendaImpl addenda = (AddendaImpl) subsetMap.get(addendaKey);
            if (addenda == null) {
                return null;
            }
            AddendaImpl.AddendaEditorImpl addendaEditor = addenda.getAddendaEditor(this);
            editorBufferMap.put(addendaKey, addendaEditor);
            return addendaEditor;
        }

        @Override
        public SphereEditor getSphereEditor(SubsetKey sphereKey) {
            Object editor = editorBufferMap.get(sphereKey);
            if (editor != null) {
                return (SphereEditor) editor;
            }
            SphereImpl sphere = (SphereImpl) subsetMap.get(sphereKey);
            if (sphere == null) {
                return null;
            }
            SphereImpl.SphereEditorImpl sphereEditor = sphere.getSphereEditor(this);
            editorBufferMap.put(sphereKey, sphereEditor);
            return sphereEditor;
        }

        @Override
        public ThesaurusEditor getThesaurusEditor(SubsetKey thesaurusKey) {
            Object editor = editorBufferMap.get(thesaurusKey);
            if (editor != null) {
                return (ThesaurusEditor) editor;
            }
            ThesaurusImpl thesaurus = (ThesaurusImpl) subsetMap.get(thesaurusKey);
            if (thesaurus == null) {
                return null;
            }
            ThesaurusImpl.ThesaurusEditorImpl thesaurusEditor = thesaurus.getThesaurusEditor(this);
            editorBufferMap.put(thesaurusKey, thesaurusEditor);
            return thesaurusEditor;
        }

        @Override
        public AlbumEditor getAlbumEditor(SubsetKey albumKey) {
            Object editor = editorBufferMap.get(albumKey);
            if (editor != null) {
                return (AlbumEditor) editor;
            }
            AlbumImpl album = (AlbumImpl) subsetMap.get(albumKey);
            if (album == null) {
                return null;
            }
            AlbumImpl.AlbumEditorImpl albumEditor = album.getAlbumEditor(this);
            editorBufferMap.put(albumKey, albumEditor);
            return albumEditor;
        }

        @Override
        public void removeSphere(Sphere sphere) throws NoRemoveableSubsetException {
            SubsetKey sphereKey = sphere.getSubsetKey();
            Object editor = editorBufferMap.get(sphereKey);
            if (editor != null) {
                editorBufferMap.remove(sphereKey);
                ((SphereImpl.SphereEditorImpl) editor).saveChanges();
            }
            innerRemoveSphere(sphere);
            fichothequeDataSource.removeSphere(sphereKey, editOrigin);
            getListeners().fireSubsetRemoved(this, sphereKey, null);
        }

        @Override
        public void removeThesaurus(Thesaurus thesaurus) throws NoRemoveableSubsetException {
            SubsetKey thesaurusKey = thesaurus.getSubsetKey();
            Object editor = editorBufferMap.get(thesaurusKey);
            if (editor != null) {
                editorBufferMap.remove(thesaurusKey);
                ((ThesaurusImpl.ThesaurusEditorImpl) editor).saveChanges();
            }
            innerRemoveThesaurus(thesaurus);
            fichothequeDataSource.removeThesaurus(thesaurusKey, editOrigin);
            getListeners().fireSubsetRemoved(this, thesaurusKey, null);
        }

        @Override
        public void removeAlbum(Album album) throws NoRemoveableSubsetException {
            SubsetKey albumKey = album.getSubsetKey();
            Object editor = editorBufferMap.get(albumKey);
            if (editor != null) {
                editorBufferMap.remove(albumKey);
                ((ThesaurusImpl.ThesaurusEditorImpl) editor).saveChanges();
            }
            innerRemoveAlbum(album);
            fichothequeDataSource.removeAlbum(albumKey, editOrigin);
            getListeners().fireSubsetRemoved(this, albumKey, null);
        }

        @Override
        public void removeAddenda(Addenda addenda) throws NoRemoveableSubsetException {
            SubsetKey addendaKey = addenda.getSubsetKey();
            Object editor = editorBufferMap.get(addendaKey);
            if (editor != null) {
                editorBufferMap.remove(addendaKey);
                ((ThesaurusImpl.ThesaurusEditorImpl) editor).saveChanges();
            }
            innerRemoveAddenda(addenda);
            fichothequeDataSource.removeAddenda(addendaKey, editOrigin);
            getListeners().fireSubsetRemoved(this, addendaKey, null);
        }

        @Override
        public void removeCorpus(Corpus corpus) throws NoRemoveableSubsetException {
            SubsetKey corpusKey = corpus.getSubsetKey();
            Subset masterSubset = corpus.getMasterSubset();
            Object editor = editorBufferMap.get(corpusKey);
            if (editor != null) {
                editorBufferMap.remove(corpusKey);
                ((CorpusImpl.CorpusEditorImpl) editor).saveChanges();
            }
            innerRemoveCorpus(corpus);
            fichothequeDataSource.removeCorpus(corpusKey, masterSubset, editOrigin);
            getListeners().fireSubsetRemoved(this, corpusKey, masterSubset);
        }

        @Override
        public boolean putAttribute(Object attributeHolder, Attribute attribute) {
            boolean done = innerPutAttribute(attributeHolder, attribute);
            if (done) {
                if (attributeHolder instanceof AbstractMetadata) {
                    ((AbstractMetadata) attributeHolder).fireChange(this);
                } else if (attributeHolder instanceof AbstractSubsetItem) {
                    saveAttributes((SubsetItem) attributeHolder);
                } else if (attributeHolder instanceof JunctionImpl) {
                    addJunctionChange(((JunctionImpl) attributeHolder).getJunctionKey());
                }
            }
            return done;
        }

        @Override
        public boolean removeAttribute(Object attributeHolder, AttributeKey attributeKey) {
            boolean done = innerRemoveAttribute(attributeHolder, attributeKey);
            if (done) {
                if (attributeHolder instanceof AbstractMetadata) {
                    ((AbstractMetadata) attributeHolder).fireChange(this);
                } else if (attributeHolder instanceof AbstractSubsetItem) {
                    saveAttributes((SubsetItem) attributeHolder);
                } else if (attributeHolder instanceof JunctionImpl) {
                    addJunctionChange(((JunctionImpl) attributeHolder).getJunctionKey());
                }
            }
            return done;
        }

        @Override
        public void saveChanges() {
            if (fichothequeMetadataEditor != null) {
                fichothequeMetadataEditor.saveChanges();
            }
            if (junctionEditor != null) {
                junctionEditor.saveChanges();
            }
            for (Object obj : editorBufferMap.values()) {
                if (obj instanceof ThesaurusImpl.ThesaurusEditorImpl) {
                    ((ThesaurusImpl.ThesaurusEditorImpl) obj).saveChanges();
                } else if (obj instanceof CorpusImpl.CorpusEditorImpl) {
                    ((CorpusImpl.CorpusEditorImpl) obj).saveChanges();
                } else if (obj instanceof SphereImpl.SphereEditorImpl) {
                    ((SphereImpl.SphereEditorImpl) obj).saveChanges();
                } else if (obj instanceof AlbumImpl.AlbumEditorImpl) {
                    ((AlbumImpl.AlbumEditorImpl) obj).saveChanges();
                } else if (obj instanceof AddendaImpl.AddendaEditorImpl) {
                    ((AddendaImpl.AddendaEditorImpl) obj).saveChanges();
                }
            }
        }

        private void saveAttributes(SubsetItem subsetItem) {
            SubsetKey subsetKey = subsetItem.getSubsetKey();
            int id = subsetItem.getId();
            if (subsetKey.isThesaurusSubset()) {
                ThesaurusEditor thesaurusEditor = getThesaurusEditor(subsetKey);
                ((ThesaurusImpl.ThesaurusEditorImpl) thesaurusEditor).addMotcleChange(id);
            } else if (subsetKey.isSphereSubset()) {
                SphereEditor sphereEditor = getSphereEditor(subsetKey);
                ((SphereImpl.SphereEditorImpl) sphereEditor).addRedacteurChange(id);
            } else if (subsetKey.isAlbumSubset()) {
                AlbumEditor albumEditor = getAlbumEditor(subsetKey);
                ((AlbumImpl.AlbumEditorImpl) albumEditor).addIllustrationChange(id);
            } else if (subsetKey.isAddendaSubset()) {
                AddendaEditor addendaEditor = getAddendaEditor(subsetKey);
                ((AddendaImpl.AddendaEditorImpl) addendaEditor).addDocumentChange(id);
            } else if (subsetKey.isCorpusSubset()) {
                CorpusEditor corpusEditor = getCorpusEditor(subsetKey);
                ((CorpusImpl.CorpusEditorImpl) corpusEditor).addAttributesChange(id);
            }
        }

        private void addJunctionChange(JunctionKey junctionKey) {
            ((FichothequeImpl.JunctionEditorImpl) getJunctionEditor()).addJunctionChange(junctionKey);
        }

    }

    private synchronized List<Addenda> initAddendaList() {
        if (cacheAddendaList != null) {
            return cacheAddendaList;
        }
        SortedMap<String, Addenda> sortedMap = new TreeMap<String, Addenda>();
        for (AddendaImpl addenda : addendaList) {
            sortedMap.put(addenda.getSubsetName(), addenda);
        }
        List<Addenda> list = FichothequeUtils.wrap(sortedMap.values().toArray(new Addenda[sortedMap.size()]));
        cacheAddendaList = list;
        return list;
    }

    private synchronized List<Album> initAlbumList() {
        if (cacheAlbumList != null) {
            return cacheAlbumList;
        }
        SortedMap<String, Album> sortedMap = new TreeMap<String, Album>();
        for (AlbumImpl album : albumList) {
            sortedMap.put(album.getSubsetName(), album);
        }
        List<Album> list = FichothequeUtils.wrap(sortedMap.values().toArray(new Album[sortedMap.size()]));
        cacheAlbumList = list;
        return list;
    }

    private synchronized List<Corpus> initCorpusList() {
        if (cacheCorpusList != null) {
            return cacheCorpusList;
        }
        SortedMap<String, Corpus> sortedMap = new TreeMap<String, Corpus>();
        for (CorpusImpl corpus : corpusList) {
            sortedMap.put(corpus.getSubsetName(), corpus);
        }
        List<Corpus> list = FichothequeUtils.wrap(sortedMap.values().toArray(new Corpus[sortedMap.size()]));
        cacheCorpusList = list;
        return list;
    }

    private synchronized List<Sphere> initSphereList() {
        if (cacheSphereList != null) {
            return cacheSphereList;
        }
        SortedMap<String, Sphere> sortedMap = new TreeMap<String, Sphere>();
        for (SphereImpl sphere : sphereList) {
            sortedMap.put(sphere.getSubsetName(), sphere);
        }
        List<Sphere> list = FichothequeUtils.wrap(sortedMap.values().toArray(new Sphere[sortedMap.size()]));
        cacheSphereList = list;
        return list;
    }

    private synchronized List<Thesaurus> initThesaurusList() {
        if (cacheThesaurusList != null) {
            return cacheThesaurusList;
        }
        SortedMap<String, Thesaurus> sortedMap = new TreeMap<String, Thesaurus>();
        for (ThesaurusImpl thesaurus : thesaurusList) {
            sortedMap.put(thesaurus.getSubsetName(), thesaurus);
        }
        List<Thesaurus> list = FichothequeUtils.wrap(sortedMap.values().toArray(new Thesaurus[sortedMap.size()]));
        cacheThesaurusList = list;
        return list;
    }


    private class JunctionEditorImpl implements JunctionEditor {

        private final FichothequeEditor fichothequeEditor;
        private final Set<JunctionKey> removedJunctionSet = new HashSet<JunctionKey>();
        private final Set<JunctionKey> changedJunctionSet = new HashSet<JunctionKey>();

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

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

        @Override
        public boolean removeJunction(SubsetItem subsetItem1, SubsetItem subsetItem2) {
            JunctionKey junctionKey = JunctionKey.toJunctionKey(subsetItem1, subsetItem2);
            JunctionImpl junction = junctionMap.get(junctionKey);
            if (junction == null) {
                return false;
            }
            junction.unlink();
            junctionMap.remove(junctionKey);
            removedJunctionSet.add(junctionKey);
            return true;
        }

        @Override
        public Junction updateJunction(SubsetItem subsetItem1, SubsetItem subsetItem2, JunctionChange junctionChange) {
            JunctionKey junctionKey = JunctionKey.toJunctionKey(subsetItem1, subsetItem2);
            JunctionImpl junction = junctionMap.get(junctionKey);
            if (junction == null) {
                if (junctionChange.getChangedTieList().isEmpty()) {
                    return null;
                }
                junction = JunctionImpl.newInstance(junctionKey, (AbstractSubsetItem) subsetItem1, (AbstractSubsetItem) subsetItem2);
                junctionMap.put(junctionKey, junction);
            }
            boolean done = junction.change(junctionChange);
            if (done) {
                addJunctionChange(junctionKey);
                if (junction.isEmpty()) {
                    junction.unlink();
                    junctionMap.remove(junctionKey);
                    removedJunctionSet.add(junctionKey);
                    return null;
                }
            }
            return junction;
        }

        @Override
        public void updateJunctions(SubsetItem mainSubsetItem, JunctionChanges junctionChanges) {
            for (SubsetItem otherSubsetItem : junctionChanges.getRemovedList()) {
                removeJunction(mainSubsetItem, otherSubsetItem);
            }
            for (JunctionChanges.Entry entry : junctionChanges.getEntryList()) {
                SubsetItem otherSubsetItem = entry.getSubsetItem();
                updateJunction(mainSubsetItem, otherSubsetItem, entry.getJunctionChange());
            }
        }

        void addJunctionChange(JunctionKey junctionKey) {
            changedJunctionSet.add(junctionKey);
        }

        void saveChanges() {
            EditOrigin editOrigin = fichothequeEditor.getEditOrigin();
            JunctionDataSource junctionDataSource = FichothequeImpl.this.getFichothequeDataSource().getJunctionDataSource();
            for (JunctionKey junctionKey : changedJunctionSet) {
                Junction junction = junctionMap.get(junctionKey);
                if (junction != null) {
                    junctionDataSource.saveJunction(junction, editOrigin);
                }
            }
            changedJunctionSet.clear();
            for (JunctionKey junctionKey : removedJunctionSet) {
                junctionDataSource.removeJunction(junctionKey, editOrigin);
            }
            removedJunctionSet.clear();
        }

    }

    private SphereImpl testSphere(Sphere sphere) {
        if (sphere == null) {
            throw new IllegalArgumentException("sphere argument cannot be null");
        }
        try {
            SphereImpl sphereImpl = (SphereImpl) sphere;
            if (sphereImpl.getFichotheque() != this) {
                throw new IllegalArgumentException("sphere argument does not come from this Fichotheque instance");
            }
            return sphereImpl;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("sphere argument does not come from this Fichotheque instance");
        }
    }

    private ThesaurusImpl testThesaurus(Thesaurus thesaurus) {
        if (thesaurus == null) {
            throw new IllegalArgumentException("thesaurus argument cannot be null");
        }
        try {
            ThesaurusImpl thesaurusImpl = (ThesaurusImpl) thesaurus;
            if (thesaurusImpl.getFichotheque() != this) {
                throw new IllegalArgumentException("thesaurus argument does not come from this Fichotheque instance");
            }
            return thesaurusImpl;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("thesaurus argument does not come from this Fichotheque instance");
        }
    }

    private CorpusImpl testCorpus(Corpus corpus) {
        if (corpus == null) {
            throw new IllegalArgumentException("corpus argument cannot be null");
        }
        try {
            CorpusImpl corpusImpl = (CorpusImpl) corpus;
            if (corpusImpl.getFichotheque() != this) {
                throw new IllegalArgumentException("corpus argument does not come from this Fichotheque instance");
            }
            return corpusImpl;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("corpus argument does not come from this Fichotheque instance");
        }
    }

    private AlbumImpl testAlbum(Album album) {
        if (album == null) {
            throw new IllegalArgumentException("album argument cannot be null");
        }
        try {
            AlbumImpl albumImpl = (AlbumImpl) album;
            if (albumImpl.getFichotheque() != this) {
                throw new IllegalArgumentException("album argument does not come from this Fichotheque instance");
            }
            return albumImpl;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("album argument does not come from this Fichotheque instance");
        }
    }

    private AddendaImpl testAddenda(Addenda addenda) {
        if (addenda == null) {
            throw new IllegalArgumentException("addenda argument cannot be null");
        }
        try {
            AddendaImpl addendaImpl = (AddendaImpl) addenda;
            if (addendaImpl.getFichotheque() != this) {
                throw new IllegalArgumentException("addenda argument does not come from this Fichotheque instance");
            }
            return addendaImpl;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("addenda argument does not come from this Fichotheque instance");
        }
    }


    private static class FichothequeEditorProviderImpl implements FichothequeEditorProvider {

        private final FichothequeImpl fichotheque;
        private final HtmlCleaner htmlCleaner;

        private FichothequeEditorProviderImpl(FichothequeImpl fichotheque, HtmlCleaner htmlCleaner) {
            this.fichotheque = fichotheque;
            this.htmlCleaner = htmlCleaner;
        }

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

        @Override
        public HtmlCleaner getHtmlCleaner() {
            return htmlCleaner;
        }

        @Override
        public FichothequeEditor newFichothequeEditor(EditOrigin editOrigin) {
            return fichotheque.getFichothequeEditor(editOrigin);
        }

    }

}
