/* 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.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.FichothequeConstants;
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.sphere.LoginKey;
import net.fichotheque.sphere.Redacteur;
import net.fichotheque.sphere.Sphere;
import net.fichotheque.sphere.SphereEditor;
import net.fichotheque.sphere.metadata.SphereMetadataEditor;
import net.fichotheque.utils.FichothequeUtils;
import net.fichotheque.utils.SphereUtils;
import net.mapeadores.util.models.EmailCore;
import net.mapeadores.util.models.PersonCore;
import net.mapeadores.util.models.PersonCoreUtils;
import net.mapeadores.util.text.collation.map.SortedCollatedKeyMap;


/**
 *
 * @author Vincent Calame
 */
class SphereImpl implements Sphere {

    private final SubsetKey sphereKey;
    private final FichothequeImpl fichotheque;
    private final SortedCollatedKeyMap<Redacteur> loginMap;
    private final Map<Integer, RedacteurImpl> itemMap = new HashMap<Integer, RedacteurImpl>();
    private int availableId = 1;
    private SphereMetadataImpl sphereMetadata;
    private List<SubsetItem> cacheSubsetItemList;
    private List<Redacteur> cacheList;


    private SphereImpl(SubsetKey sphereKey, FichothequeImpl fichotheque) {
        this.sphereKey = sphereKey;
        this.fichotheque = fichotheque;
        this.loginMap = new SortedCollatedKeyMap<Redacteur>(Locale.ENGLISH);
    }

    static SphereImpl.InitEditor fromInit(SubsetKey sphereKey, FichothequeImpl fichotheque, FichothequeImpl.InitEditor fichothequeInitEditor) {
        SphereImpl sphere = new SphereImpl(sphereKey, fichotheque);
        return new SphereImpl.InitEditor(sphere, fichothequeInitEditor);
    }

    static SphereImpl fromNew(SubsetKey sphereKey, FichothequeImpl fichotheque) {
        SphereImpl sphere = new SphereImpl(sphereKey, fichotheque);
        sphere.sphereMetadata = SphereMetadataImpl.fromNew(sphere);
        return sphere;
    }

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

    @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 Fichotheque getFichotheque() {
        return fichotheque;
    }

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

    @Override
    public List<Redacteur> getRedacteurList() {
        List<Redacteur> result = cacheList;
        if (result == null) {
            result = initRedacteurList();
        }
        return result;
    }

    @Override
    public Redacteur getRedacteurById(int id) {
        return itemMap.get(id);
    }

    @Override
    public Redacteur getRedacteurByLogin(String login) {
        return loginMap.getValue(login);
    }

    @Override
    public List<Corpus> getSatelliteCorpusList() {
        return FichothequeUtils.EMPTY_CORPUSLIST;
    }

    SphereEditorImpl getSphereEditor(FichothequeEditor fichothequeEditor) {
        return new SphereEditorImpl(fichothequeEditor);
    }

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

    private synchronized Redacteur innerCreateRedacteur(int id, String login) throws ExistingIdException, ParseException {
        if (id < 1) {
            id = availableId;
        } else {
            if (itemMap.containsKey(id)) {
                throw new ExistingIdException();
            }
        }
        if (loginMap.getValue(login) != null) {
            throw new ExistingIdException();
        }
        LoginKey.checkLogin(login);
        RedacteurImpl defredac = new RedacteurImpl(id, login);
        itemMap.put(id, defredac);
        loginMap.putValue(login, defredac);
        availableId = Math.max(availableId, id + 1);
        clearCache();
        return defredac;
    }

    private synchronized boolean innerRemoveRedacteur(Redacteur redacteur) {
        if (!fichotheque.isRemoveable(redacteur)) {
            return false;
        }
        RedacteurImpl redacteurImpl = testRedacteur(redacteur);
        int id = redacteurImpl.getId();
        String login = redacteurImpl.getLogin();
        itemMap.remove(id);
        loginMap.removeValue(login);
        clearCache();
        return true;
    }

    private synchronized boolean innerSetLogin(Redacteur redacteur, String newLogin) throws ExistingIdException, ParseException {
        LoginKey.checkLogin(newLogin);
        if (loginMap.getValue(newLogin) != null) {
            throw new ExistingIdException();
        }
        RedacteurImpl defredac = testRedacteur(redacteur);
        String currentLogin = defredac.getLogin();
        boolean done = defredac.setLogin(newLogin);
        if (done) {
            loginMap.removeValue(currentLogin);
            loginMap.putValue(newLogin, defredac);
        }
        return done;
    }

    private boolean innerSetEmail(Redacteur redacteur, EmailCore email) {
        RedacteurImpl defredac = testRedacteur(redacteur);
        return defredac.setEmail(email);
    }

    private boolean innerSetPerson(Redacteur redacteur, PersonCore person) {
        RedacteurImpl defredac = testRedacteur(redacteur);
        return defredac.setPerson(person);
    }

    private boolean innerSetStatus(Redacteur redacteur, String status) {
        RedacteurImpl defredac = testRedacteur(redacteur);
        return defredac.setStatus(status);
    }

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

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

    private synchronized List<Redacteur> initRedacteurList() {
        SortedMap<Integer, RedacteurImpl> sortedMap = new TreeMap<Integer, RedacteurImpl>();
        sortedMap.putAll(itemMap);
        List<Redacteur> list = SphereUtils.wrap(sortedMap.values().toArray(new Redacteur[sortedMap.size()]));
        cacheList = list;
        return list;
    }


    static class InitEditor implements SphereEditor {

        private final SphereImpl sphere;
        private final FichothequeImpl.InitEditor fichothequeInitEditor;
        private final SphereMetadataImpl.InitEditor metadataInitEditor;

        private InitEditor(SphereImpl sphere, FichothequeImpl.InitEditor fichothequeInitEditor) {
            this.sphere = sphere;
            this.fichothequeInitEditor = fichothequeInitEditor;
            this.metadataInitEditor = SphereMetadataImpl.fromInit(this);
            sphere.sphereMetadata = (SphereMetadataImpl) metadataInitEditor.getMetadata();
        }

        @Override
        public Sphere getSphere() {
            return sphere;
        }

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

        @Override
        public SphereMetadataEditor getSphereMetadataEditor() {
            return metadataInitEditor;
        }

        @Override
        public Redacteur createRedacteur(int id, String login) throws ExistingIdException, ParseException {
            return sphere.innerCreateRedacteur(id, login);
        }

        @Override
        public boolean removeRedacteur(Redacteur redacteur) {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public boolean setLogin(Redacteur redacteur, String newLogin) throws ExistingIdException, ParseException {
            throw new UnsupportedOperationException("Not during init");
        }

        @Override
        public boolean setEmail(Redacteur redacteur, EmailCore email) {
            return sphere.innerSetEmail(redacteur, email);
        }

        @Override
        public boolean setPerson(Redacteur redacteur, PersonCore person) {
            return sphere.innerSetPerson(redacteur, person);
        }

        @Override
        public boolean setStatus(Redacteur redacteur, String status) {
            return sphere.innerSetStatus(redacteur, status);
        }

    }


    class SphereEditorImpl implements SphereEditor {

        private final FichothequeEditor fichothequeEditor;
        private final Set<Integer> changedRedacteurSet = new HashSet<Integer>();
        private final Set<Integer> removedRedacteurSet = new HashSet<Integer>();
        private SphereMetadataEditor sphereMetadataEditor;
        private boolean listChanged = false;
        private boolean metadataChanged = false;

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

        @Override
        public Sphere getSphere() {
            return SphereImpl.this;
        }

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

        @Override
        public SphereMetadataEditor getSphereMetadataEditor() {
            if (sphereMetadataEditor == null) {
                sphereMetadataEditor = sphereMetadata.getSphereMetadataEditor(this);
            }
            return sphereMetadataEditor;
        }

        @Override
        public Redacteur createRedacteur(int id, String login) throws ExistingIdException, ParseException {
            Redacteur redacteur = innerCreateRedacteur(id, login);
            setListChange();
            return redacteur;
        }

        @Override
        public boolean removeRedacteur(Redacteur redacteur) {
            int id = redacteur.getId();
            boolean done = innerRemoveRedacteur(redacteur);
            if (done) {
                setListChange();
                removedRedacteurSet.add(id);
                getListeners().fireSubsetItemRemoved(fichothequeEditor, SphereImpl.this, id);
            }
            return done;
        }

        @Override
        public boolean setLogin(Redacteur redacteur, String newLogin) throws ExistingIdException, ParseException {
            boolean done = innerSetLogin(redacteur, newLogin);
            if (done) {
                setListChange();
            }
            return done;
        }

        @Override
        public boolean setEmail(Redacteur redacteur, EmailCore emailCore) {
            boolean done = innerSetEmail(redacteur, emailCore);
            if (done) {
                addRedacteurChange(redacteur.getId());
            }
            return done;
        }

        @Override
        public boolean setPerson(Redacteur redacteur, PersonCore personCore) {
            boolean done = innerSetPerson(redacteur, personCore);
            if (done) {
                addRedacteurChange(redacteur.getId());
            }
            return done;
        }

        @Override
        public boolean setStatus(Redacteur redacteur, String status) {
            boolean done = innerSetStatus(redacteur, status);
            if (done) {
                setListChange();
            }
            return done;
        }

        void addRedacteurChange(int id) {
            changedRedacteurSet.add(id);
        }

        void setListChange() {
            listChanged = true;
        }

        void setMetadataChange() {
            metadataChanged = true;
        }

        void saveChanges() {
            EditOrigin editOrigin = fichothequeEditor.getEditOrigin();
            SphereDataSource sphereDataSource = fichotheque.getFichothequeDataSource().getSphereDataSource();
            for (Integer id : changedRedacteurSet) {
                Redacteur redacteur = getRedacteurById(id);
                if (redacteur != null) {
                    sphereDataSource.saveRedacteur(redacteur, editOrigin);
                }
            }
            changedRedacteurSet.clear();
            for (Integer id : removedRedacteurSet) {
                sphereDataSource.removeRedacteur(SphereImpl.this, id, editOrigin);
            }
            removedRedacteurSet.clear();
            if (listChanged) {
                sphereDataSource.saveList(SphereImpl.this, editOrigin);
                listChanged = false;
            }
            if (metadataChanged) {
                sphereDataSource.saveMetadata(SphereImpl.this, editOrigin);
                metadataChanged = false;
            }
        }

    }


    private class RedacteurImpl extends AbstractSubsetItem implements Redacteur {

        private final String globalId;
        private final int id;
        private String login;
        private PersonCore person = PersonCoreUtils.EMPTY_PERSONCORE;
        private InternalEmail email = null;
        private String status = FichothequeConstants.ACTIVE_STATUS;

        private RedacteurImpl(int id, String login) {
            this.id = id;
            this.globalId = FichothequeUtils.toGlobalId(SphereImpl.this.getSubsetKey(), id);
            this.login = login;
        }

        @Override
        public String getGlobalId() {
            return globalId;
        }

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

        @Override
        public String getLogin() {
            return login;
        }

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

        @Override
        public String getStatus() {
            return status;
        }

        private boolean setLogin(String newLogin) {
            if (newLogin.equals(login)) {
                return false;
            }
            login = newLogin;
            return true;
        }

        private boolean setStatus(String newStatus) {
            if (newStatus.equals(status)) {
                return false;
            }
            status = FichothequeConstants.checkRedacteurStatus(newStatus);
            return true;
        }

        private boolean setPerson(PersonCore newPerson) {
            if (newPerson == null) {
                throw new IllegalArgumentException("newPersonCore argument is null");
            }
            if (!(PersonCoreUtils.areEqual(newPerson, person))) {
                person = PersonCoreUtils.clonePersonCore(newPerson);
                return true;
            } else {
                return false;
            }
        }

        private boolean setEmail(EmailCore newEmail) {
            if (newEmail == null) {
                if (email != null) {
                    email = null;
                    return true;
                } else {
                    return false;
                }
            }
            String realName = newEmail.getRealName();
            if (realName.equals(person.toStandardStyle())) {
                realName = "";
            }
            InternalEmail newInternalEmail = new InternalEmail(newEmail.getAddrSpec(), realName);
            if (email == null) {
                email = newInternalEmail;
                return true;
            }
            if (email.equals(newInternalEmail)) {
                return false;
            } else {
                email = newInternalEmail;
                return true;
            }
        }

        @Override
        public EmailCore getEmailCore() {
            return email;
        }

        @Override
        public PersonCore getPersonCore() {
            return person;
        }


        private class InternalEmail implements EmailCore {

            private final String addrSpec;
            private final String realName;

            private InternalEmail(String addrSpec, String realName) {
                this.addrSpec = addrSpec;
                this.realName = realName;
            }

            @Override
            public String getAddrSpec() {
                return addrSpec;
            }

            @Override
            public String getRealName() {
                return realName;
            }

            @Override
            public String getComputedRealName() {
                if (realName.isEmpty()) {
                    return person.toStandardStyle();
                } else {
                    return realName;
                }
            }

            @Override
            public int hashCode() {
                return addrSpec.hashCode() + realName.hashCode();
            }

            @Override
            public boolean equals(Object other) {
                if (other == null) {
                    return false;
                }
                if (other == this) {
                    return true;
                }
                if (other.getClass() != this.getClass()) {
                    return false;
                }
                InternalEmail otherEmail = (InternalEmail) other;
                return ((otherEmail.addrSpec.equals(this.addrSpec)) && (otherEmail.realName.equals(this.realName)));
            }

        }

    }


    private RedacteurImpl testRedacteur(Redacteur redacteur) {
        if (redacteur == null) {
            throw new IllegalArgumentException("redacteur argument cannot be null");
        }
        try {
            RedacteurImpl defredacteur = (RedacteurImpl) redacteur;
            if (defredacteur.getSphere() != this) {
                throw new IllegalArgumentException("redacteur argument does not come from this sphere");
            }
            return defredacteur;
        } catch (ClassCastException cce) {
            throw new IllegalArgumentException("redacteur argument does not come from this sphere");
        }
    }

}
