/* FichothequeLib_Impl - Copyright (c) 2006-2024 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.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
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.ExistingNameException;
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.addenda.Addenda;
import net.fichotheque.addenda.AddendaEditor;
import net.fichotheque.addenda.Document;
import net.fichotheque.addenda.Version;
import net.fichotheque.addenda.VersionInfo;
import net.fichotheque.addenda.metadata.AddendaMetadataEditor;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.utils.AddendaUtils;
import net.fichotheque.utils.FichothequeUtils;
import net.mapeadores.util.primitives.FileLength;


/**
 *
 * @author Vincent Calame
 */
public class AddendaImpl implements Addenda {

    private final SubsetKey addendaKey;
    private final FichothequeImpl fichotheque;
    private AddendaMetadataImpl addendaMetadata;
    private int availableId = 1;
    private List<SubsetItem> cacheSubsetItemList;
    private List<Document> cacheList;
    private final Map<String, DocumentImpl> documentByBasenameMap = new HashMap<String, DocumentImpl>();
    private final Map<Integer, DocumentImpl> itemMap = new HashMap<Integer, DocumentImpl>();

    private AddendaImpl(SubsetKey addendaKey, FichothequeImpl fichotheque) {
        this.addendaKey = addendaKey;
        this.fichotheque = fichotheque;
    }

    static AddendaImpl.InitEditor fromInit(SubsetKey addendaKey, FichothequeImpl fichotheque, FichothequeImpl.InitEditor fichothequeInitEditor) {
        AddendaImpl addenda = new AddendaImpl(addendaKey, fichotheque);
        return new AddendaImpl.InitEditor(addenda, fichothequeInitEditor);
    }

    static AddendaImpl fromNew(SubsetKey addendaKey, FichothequeImpl fichotheque) {
        AddendaImpl addenda = new AddendaImpl(addendaKey, fichotheque);
        addenda.addendaMetadata = AddendaMetadataImpl.fromNew(addenda);
        return addenda;
    }

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

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

    @Override
    public Document getDocumentById(int id) {
        return itemMap.get(id);
    }

    @Override
    public Document getDocumentByBasename(String basename) {
        return documentByBasenameMap.get(basename);
    }

    @Override
    public List<Document> getDocumentList() {
        List<Document> result = cacheList;
        if (result == null) {
            result = initDocumentList();
        }
        return result;
    }

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

    @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<Corpus> getSatelliteCorpusList() {
        return FichothequeUtils.EMPTY_CORPUSLIST;
    }

    AddendaEditorImpl getAddendaEditor(FichothequeEditor fichothequeEditor) {
        return new AddendaEditorImpl(fichothequeEditor);
    }

    private AddendaDataSource getAddendaDataSource() {
        return fichotheque.getFichothequeDataSource().getAddendaDataSource();
    }


    private synchronized boolean innerRemoveDocument(Document document) {
        testDocument(document);
        if (!fichotheque.isRemoveable(document)) {
            return false;
        }
        documentByBasenameMap.remove(document.getBasename());
        itemMap.remove(document.getId());
        clearCache();
        return true;
    }

    private boolean innerRemoveVersion(Document document, String extension) {
        DocumentImpl implDocument = testDocument(document);
        return implDocument.deleteVersion(extension);
    }

    private synchronized Document innerCreateDocument(int id, Collection<VersionInfo> versionInfos) throws ExistingIdException {
        if (id < 1) {
            id = availableId;
        } else {
            if (itemMap.containsKey(id)) {
                throw new ExistingIdException();
            }
        }
        DocumentImpl document = new DocumentImpl(id);
        itemMap.put(id, document);
        documentByBasenameMap.put(document.getBasename(), document);
        if (versionInfos != null) {
            document.initVersionList(versionInfos);
        }
        availableId = Math.max(availableId, id + 1);
        clearCache();
        return document;
    }

    private boolean innerSetBasename(Document document, String newName) throws ExistingNameException, ParseException {
        if (!AddendaUtils.testBasename(newName)) {
            throw new ParseException("wrong basename", 0);
        }
        DocumentImpl bdfimpldoc = testDocument(document);
        String oldName = bdfimpldoc.getBasename();
        if (oldName.equals(newName)) {
            return false;
        }
        if (documentByBasenameMap.containsKey(newName)) {
            throw new ExistingNameException();
        }
        documentByBasenameMap.remove(oldName);
        bdfimpldoc.setBasename(newName);
        documentByBasenameMap.put(newName, bdfimpldoc);
        return true;
    }

    private void innerUpdateVersion(Document document, String extension, InputStream inputStream) throws ParseException, IOException {
        if (!AddendaUtils.testExtension(extension)) {
            throw new ParseException("wrong extension", 0);
        }
        DocumentImpl bdfimpldoc = testDocument(document);
        bdfimpldoc.saveVersion(extension, inputStream);
    }

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

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

    private synchronized List<Document> initDocumentList() {
        SortedMap<Integer, DocumentImpl> sortedMap = new TreeMap<Integer, DocumentImpl>();
        sortedMap.putAll(itemMap);
        List<Document> list = AddendaUtils.wrap(sortedMap.values().toArray(new Document[sortedMap.size()]));
        cacheList = list;
        return list;
    }


    static class InitEditor implements AddendaEditor {

        private final AddendaImpl addenda;
        private final FichothequeImpl.InitEditor fichothequeInitEditor;
        private final AddendaMetadataImpl.InitEditor metadataInitEditor;

        private InitEditor(AddendaImpl addenda, FichothequeImpl.InitEditor fichothequeInitEditor) {
            this.addenda = addenda;
            this.fichothequeInitEditor = fichothequeInitEditor;
            this.metadataInitEditor = AddendaMetadataImpl.fromInit(this);
            addenda.addendaMetadata = (AddendaMetadataImpl) metadataInitEditor.getMetadata();
        }

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

        @Override
        public Addenda getAddenda() {
            return addenda;
        }

        @Override
        public AddendaMetadataEditor getAddendaMetadataEditor() {
            return metadataInitEditor;
        }

        @Override
        public boolean removeDocument(Document document) {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public boolean removeVersion(Document document, String extension) {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public Document createDocument(int id, Collection<VersionInfo> versionInfos) throws ExistingIdException {
            return addenda.innerCreateDocument(id, versionInfos);
        }

        @Override
        public boolean setBasename(Document document, String newName) throws ExistingNameException, ParseException {
            return addenda.innerSetBasename(document, newName);
        }

        @Override
        public void saveVersion(Document document, String extension, InputStream inputStream) throws ParseException, IOException {
            throw new UnsupportedOperationException("Not during init");
        }

    }


    class AddendaEditorImpl implements AddendaEditor {

        private final FichothequeEditor fichothequeEditor;
        private final Set<Integer> changedDocumentSet = new HashSet<Integer>();
        private final Set<Integer> removedDocumentSet = new HashSet<Integer>();
        private AddendaMetadataEditor addendaMetadataEditor;
        private boolean metadataChanged = false;

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

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

        @Override
        public Addenda getAddenda() {
            return AddendaImpl.this;
        }

        @Override
        public AddendaMetadataEditor getAddendaMetadataEditor() {
            if (addendaMetadataEditor == null) {
                addendaMetadataEditor = addendaMetadata.getAddendaMetadataEditor(this);
            }
            return addendaMetadataEditor;
        }

        @Override
        public boolean removeDocument(Document document) {
            int id = document.getId();
            boolean done = innerRemoveDocument(document);
            if (done) {
                removedDocumentSet.add(id);
            }
            return done;
        }

        @Override
        public boolean removeVersion(Document document, String extension) {
            return innerRemoveVersion(document, extension);
        }

        @Override
        public Document createDocument(int id, Collection<VersionInfo> versionInfos) throws ExistingIdException {
            Document document = innerCreateDocument(id, versionInfos);
            addDocumentChange(document.getId());
            return document;
        }

        @Override
        public boolean setBasename(Document document, String newName) throws ExistingNameException, ParseException {
            boolean done = innerSetBasename(document, newName);
            if (done) {
                addDocumentChange(document.getId());
            }
            return done;
        }

        @Override
        public void saveVersion(Document document, String extension, InputStream inputStream) throws ParseException, IOException {
            innerUpdateVersion(document, extension, inputStream);
        }

        void addDocumentChange(int id) {
            changedDocumentSet.add(id);
        }

        void setMetadataChange() {
            metadataChanged = true;
        }

        void saveChanges() {
            EditOrigin editOrigin = fichothequeEditor.getEditOrigin();
            AddendaDataSource addendaDataSource = getAddendaDataSource();
            if (metadataChanged) {
                addendaDataSource.saveMetadata(AddendaImpl.this, editOrigin);
                metadataChanged = false;
            }
            for (Integer id : changedDocumentSet) {
                Document document = getDocumentById(id);
                if (document != null) {
                    addendaDataSource.saveDocument(document, editOrigin);
                }
            }
            changedDocumentSet.clear();
            for (Integer id : removedDocumentSet) {
                addendaDataSource.removeDocument(AddendaImpl.this, id, editOrigin);
            }
            removedDocumentSet.clear();
        }

    }


    private class DocumentImpl extends AbstractSubsetItem implements Document {

        private final int id;
        private String basename;
        private final List<VersionImpl> versionList = new ArrayList<VersionImpl>();
        private List<Version> cacheList;

        private DocumentImpl(int id) {
            this.id = id;
            basename = "_" + id;
        }

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

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

        @Override
        public String getBasename() {
            return basename;
        }

        @Override
        public List<Version> getVersionList() {
            List<Version> result = cacheList;
            if (result == null) {
                result = initCacheList();
            }
            return result;
        }

        @Override
        public Version getVersionByExtension(String extension) {
            int size = versionList.size();
            for (int i = 0; i < size; i++) {
                Version version = versionList.get(i);
                if (version.getExtension().equals(extension)) {
                    return version;
                }
            }
            return null;
        }

        private void setBasename(String basename) {
            this.basename = basename;
        }

        private synchronized void initVersionList(Collection<VersionInfo> versionInfos) {
            SortedMap<String, VersionImpl> versionMap = new TreeMap<String, VersionImpl>();
            for (VersionInfo versionInfo : versionInfos) {
                VersionImpl version = new VersionImpl(versionInfo);
                versionMap.put(version.getExtension(), version);
            }
            versionList.addAll(versionMap.values());
            clearCache();
        }

        private synchronized void saveVersion(String extension, InputStream inputStream) throws IOException {
            VersionInfo versionInfo = getAddendaDataSource().saveVersion(DocumentImpl.this, extension, inputStream);
            int size = versionList.size();
            boolean done = false;
            for (int i = 0; i < size; i++) {
                VersionImpl other = versionList.get(i);
                int comp = other.getExtension().compareTo(extension);
                if (comp == 0) {
                    done = true;
                    other.update(versionInfo);
                    break;
                } else if (comp > 0) {
                    done = true;
                    versionList.add(i, new VersionImpl(versionInfo));
                    break;
                }
            }
            if (!done) {
                versionList.add(new VersionImpl(versionInfo));
            }
            clearCache();
        }

        private synchronized boolean deleteVersion(String extension) {
            if (versionList.size() == 1) {
                return false;
            }
            getAddendaDataSource().deleteVersion(DocumentImpl.this, extension);
            for (Iterator<VersionImpl> it = versionList.iterator(); it.hasNext();) {
                VersionImpl version = it.next();
                if (version.getExtension().equals(extension)) {
                    it.remove();
                    break;
                }
            }
            clearCache();
            return true;
        }

        private synchronized List<Version> initCacheList() {
            if (cacheList != null) {
                return cacheList;
            }
            List<Version> list = AddendaUtils.wrap(versionList.toArray(new Version[versionList.size()]));
            cacheList = list;
            return list;
        }

        private void clearCache() {
            cacheList = null;
        }


        private class VersionImpl implements Version {

            private final String extension;
            private FileLength fileLength;
            private String md5Checksum;

            private VersionImpl(VersionInfo versionInfo) {
                this.extension = versionInfo.getExtension();
                setFileLength(versionInfo.getLength());
                this.md5Checksum = versionInfo.getMd5Checksum();
            }

            @Override
            public Document getDocument() {
                return DocumentImpl.this;
            }

            @Override
            public String getExtension() {
                return extension;
            }

            @Override
            public FileLength getFileLength() {
                return fileLength;
            }

            @Override
            public String getMd5Checksum() {
                return md5Checksum;
            }


            @Override
            public InputStream getInputStream() {
                if (fileLength.getValue() == 0) {
                    return new ByteArrayInputStream(new byte[0]);
                }
                return getAddendaDataSource().getInputStream(DocumentImpl.this, extension);
            }

            @Override
            public void linkTo(Path destination, boolean symbolicLink) throws IOException {
                getAddendaDataSource().linkTo(DocumentImpl.this, extension, destination, symbolicLink);
            }

            private void update(VersionInfo versionInfo) {
                setFileLength(versionInfo.getLength());
                this.md5Checksum = versionInfo.getMd5Checksum();
            }

            private void setFileLength(long fln) {
                if (fln < 0) {
                    fileLength = FileLength.EMPTY_FILELENGTH;
                } else {
                    fileLength = new FileLength(fln);
                }
            }

        }

    }

    private DocumentImpl testDocument(Document document) {
        if (document == null) {
            throw new IllegalArgumentException("document argument cannot be null");
        }
        try {
            DocumentImpl bdfimpl = (DocumentImpl) document;
            if (bdfimpl.getAddenda() != this) {
                throw new IllegalArgumentException("document argument does not come from this documentSubse");
            }
            return bdfimpl;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("document argument does not come from this documentSubset");
        }
    }

}
