/* UtilLib - Copyright (c) 2005-2018 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.mapeadores.util.text.collation.map;

import java.text.Collator;
import java.text.RuleBasedCollator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import net.mapeadores.util.text.LevenshteinDistance;
import net.mapeadores.util.text.OneDistance;
import net.mapeadores.util.text.SubstringPosition;
import net.mapeadores.util.text.TextConstants;
import net.mapeadores.util.text.collation.CollationUnit;


/**
 * Classe permettant de stocker des valeurs suivant des clés qui sont des
 * chaînes traités par « collation ». Lorsque une valeur est stocké suivant une
 * clé, celle-ci subit d'abord une « collation » (dont le résultat s'appele
 * collatedKey). Autrement dit, deux chaînes key1 et key2 sont des clés
 * identiques non pas si key1.equals(key2) mais si
 * toCollatedString(key1).equals(toCollatedString(key2))
 *
 * @author Vincent Calame
 */
public class SortedCollatedKeyMap<E> implements CollatedKeyMap<E> {

    private final RuleBasedCollator collator;
    private final Locale locale;
    private final SortedMap<String, E> valuesMap = new TreeMap<String, E>();

    /**
     * Construit une instance de SortedCollatedKeyMap en prenant comme «
     * collator » Collator.getInstance(locale)
     */
    public SortedCollatedKeyMap(Locale locale) {
        this.locale = locale;
        collator = (RuleBasedCollator) Collator.getInstance(locale);
        collator.setStrength(Collator.PRIMARY);
    }

    @Override
    public Collection<E> values() {
        return valuesMap.values();
    }

    /**
     * Retourne l'instance de Collator utilisée pour la création des «
     * collatedKey » et la recherche.
     */
    @Override
    public RuleBasedCollator getCollator() {
        return collator;
    }

    /**
     * Retourne l'objet Locale qui a servi à la construction de l'objet.
     */
    @Override
    public Locale getLocale() {
        return locale;
    }

    /**
     * Retourne le nombre de valeurs stockées dans l'instance de
     * SortedCollatedKeyMap.
     */
    @Override
    public int size() {
        return valuesMap.size();
    }

    /**
     * Retourne la valeur stockée à la clé « s ». La chaîne s subit d'abord une
     * « collation ». Cette méthode est équivalente à
     * getValueByCollatedKey(toCollateKey(key))
     */
    @Override
    public E getValue(String key) {
        String collatedKey = CollationUnit.collate(key, collator);
        return valuesMap.get(collatedKey);
    }

    /**
     * Retourne la valeur stockée à la clé déjà « collatée », collatedKey.
     */
    @Override
    public E getValueByCollatedKey(String collatedKey) {
        return valuesMap.get(collatedKey);
    }

    /**
     * Stocke la valeur à la clé Key. Équivalent de
     * putValueByCollatedKey(toCollatedKey(key),value).
     */
    @Override
    public void putValue(String key, E value) {
        String collatedKey = CollationUnit.collate(key, collator);
        valuesMap.put(collatedKey, value);
    }

    /**
     * Stocke la valeur à la clé déjà « collatée » collatedKey.
     */
    @Override
    public void putValueByCollatedKey(String collatedKey, E value) {
        valuesMap.put(collatedKey, value);
    }

    /**
     * Supprime la valeur référencée à la clé key.
     */
    @Override
    public E removeValue(String key) {
        String collatedKey = CollationUnit.collate(key, collator);
        return valuesMap.remove(collatedKey);
    }

    /**
     * Supprime la valeur à la clé déjà « collatée » collatedKey.
     */
    @Override
    public E removeValueByCollatedKey(String collatedKey) {
        return valuesMap.remove(collatedKey);
    }

    /**
     * Effectue une recherche sur les clés de l'instance de SortedCollatedKeyMap
     * et retourne le résultat sous la forme d'une instance de
     * searchResultItemList. Cette méthode vérifie la présence de searchText
     * dans les clés suivant trois conditions : TextConstants.CONTAINS,
     * TextConstants.ENDSWITH, TextConstants.STARTSWITH searchText subit
     * évidemment une « collation » pour que les conditions répondent aux règles
     * du Collator.
     */
    @Override
    public List<SearchResultUnit<E>> search(String searchText, int type, ValueFilter valueFilter) {
        return internalSearch(searchText, type, valueFilter);
    }

    @Override
    public List<SearchResultUnit<E>> searchNeighbours(String searchText, int type, ValueFilter valueFilter, int distance) {
        if (distance < 1) {
            return internalSearch(searchText, type, valueFilter);
        }
        if (valueFilter == null) {
            valueFilter = CollationMapUtils.ALL_VALUEFILTER;
        }
        switch (type) {
            case TextConstants.CONTAINS:
            case TextConstants.ENDSWITH:
                return Collections.emptyList();
            case TextConstants.MATCHES:
                if (distance == 1) {
                    return getOneDistanceNeighboursStartingWith(searchText, valueFilter, true);
                } else {
                    return getNeighboursStartingWith(searchText, valueFilter, distance, true);
                }
            case TextConstants.STARTSWITH:
                if (distance == 1) {
                    return getOneDistanceNeighboursStartingWith(searchText, valueFilter, false);
                } else {
                    return getNeighboursStartingWith(searchText, valueFilter, distance, false);
                }
            default:
                throw new IllegalArgumentException("unknwon TextConstants type : " + type);
        }
    }

    @Override
    public void clear() {
        valuesMap.clear();
    }

    private List<SearchResultUnit<E>> internalSearch(String searchText, int type, ValueFilter valueFilter) {
        if (valueFilter == null) {
            valueFilter = CollationMapUtils.ALL_VALUEFILTER;
        }
        switch (type) {
            case TextConstants.CONTAINS:
                return getValuesContaining(searchText, valueFilter);
            case TextConstants.ENDSWITH:
                return getValuesEndingWith(searchText, valueFilter);
            case TextConstants.MATCHES:
                String collatedKey = CollationUnit.collate(searchText, collator);
                E value = valuesMap.get(collatedKey);
                if ((value == null) || (!valueFilter.acceptValue(value))) {
                    return Collections.emptyList();
                } else {
                    SearchResultUnit<E> unique = new InternalSearchResultUnit<E>(value, collatedKey, new SubstringPosition(0, collatedKey.length()));
                    return Collections.singletonList(unique);
                }
            case TextConstants.STARTSWITH:
                return getValuesStartingWith(searchText, valueFilter);
            default:
                throw new IllegalArgumentException("unknwon TextConstants type : " + type);
        }
    }

    /*
     * Méthode appelée par search().
     * Retourne la liste des valeurs dont la clé commence par s
     */
    private List<SearchResultUnit<E>> getValuesStartingWith(String s, ValueFilter valueFilter) {
        String searchkey = CollationUnit.collate(s, collator);
        List<SearchResultUnit<E>> searchResultItemList = new ArrayList<SearchResultUnit<E>>();
        int minlength = searchkey.length();
        SortedMap<String, E> sorted = valuesMap.tailMap(searchkey);
        for (Map.Entry<String, E> couple : sorted.entrySet()) {
            String currentkey = couple.getKey();
            if (currentkey.length() < minlength) {
                break;
            } else if (currentkey.startsWith(searchkey)) {
                E value = couple.getValue();
                if (valueFilter.acceptValue(value)) {
                    InternalSearchResultUnit<E> searchResultItem = new InternalSearchResultUnit<E>(value, currentkey, new SubstringPosition(0, minlength));
                    searchResultItemList.add(searchResultItem);
                }
            } else {
                break;
            }
        }
        return searchResultItemList;
    }

    /*
     * Méthode appelée par search().
     * Retourne la liste des valeurs dont la clé termine par s
     */
    private List<SearchResultUnit<E>> getValuesEndingWith(String s, ValueFilter valueFilter) {
        String searchkey = CollationUnit.collate(s, collator);
        List<SearchResultUnit<E>> searchResultItemList = new ArrayList<SearchResultUnit<E>>();
        int minlength = searchkey.length();
        for (Map.Entry<String, E> couple : valuesMap.entrySet()) {
            String currentkey = couple.getKey();
            if (currentkey.length() < minlength) {
                continue;
            } else if (currentkey.endsWith(searchkey)) {
                E value = couple.getValue();
                if (valueFilter.acceptValue(value)) {
                    InternalSearchResultUnit<E> searchResultItem = new InternalSearchResultUnit<E>(value, currentkey, new SubstringPosition(currentkey.length() - minlength, minlength));
                    searchResultItemList.add(searchResultItem);
                }
            }
        }
        return searchResultItemList;
    }

    /*
     * Méthode appelée par search().
     * Retourne la liste des valeurs dont la clé contient s
     */
    private List<SearchResultUnit<E>> getValuesContaining(String s, ValueFilter valueFilter) {
        String searchkey = CollationUnit.collate(s, collator);
        List<SearchResultUnit<E>> searchResultItemList = new ArrayList<SearchResultUnit<E>>();
        int minlength = searchkey.length();
        for (Map.Entry<String, E> couple : valuesMap.entrySet()) {
            String currentkey = couple.getKey();
            if (currentkey.length() < minlength) {
                continue;
            } else {
                int idx = currentkey.indexOf(searchkey);
                if (idx != -1) {
                    E value = couple.getValue();
                    if (valueFilter.acceptValue(value)) {
                        InternalSearchResultUnit<E> searchResultItem = new InternalSearchResultUnit<E>(couple.getValue(), currentkey, new SubstringPosition(idx, minlength));
                        searchResultItemList.add(searchResultItem);
                    }
                }
            }
        }
        return searchResultItemList;
    }

    private List<SearchResultUnit<E>> getNeighboursStartingWith(String s, ValueFilter valueFilter, int distance, boolean matches) {
        String searchkey = CollationUnit.collate(s, collator);
        LevenshteinDistance levenshteinDistance = new LevenshteinDistance(distance);
        List<SearchResultUnit<E>> searchResultItemList = new ArrayList<SearchResultUnit<E>>();
        int length = searchkey.length();
        SubstringPosition defaultPosition = new SubstringPosition(0, length - 1);
        for (Map.Entry<String, E> couple : valuesMap.entrySet()) {
            String currentkey = couple.getKey();
            int dist = levenshteinDistance.apply(searchkey, currentkey);
            if (!matches) {
                if ((dist == -1) && (currentkey.length() > length)) {
                    dist = levenshteinDistance.apply(searchkey, currentkey.substring(0, length));
                    if (dist == -1) {
                        dist = levenshteinDistance.apply(searchkey, currentkey.substring(0, (length + 1)));
                    }
                }
            }
            if (dist != -1) {
                E value = couple.getValue();
                if (valueFilter.acceptValue(value)) {
                    InternalSearchResultUnit<E> searchResultItem = new InternalSearchResultUnit<E>(value, currentkey, defaultPosition);
                    searchResultItemList.add(searchResultItem);
                }
            }
        }
        return searchResultItemList;
    }

    private List<SearchResultUnit<E>> getOneDistanceNeighboursStartingWith(String s, ValueFilter valueFilter, boolean matches) {
        String searchkey = CollationUnit.collate(s, collator);
        List<SearchResultUnit<E>> searchResultItemList = new ArrayList<SearchResultUnit<E>>();
        int length = searchkey.length();
        SubstringPosition defaultPosition = new SubstringPosition(0, length - 1);
        for (Map.Entry<String, E> couple : valuesMap.entrySet()) {
            String currentkey = couple.getKey();
            boolean done = OneDistance.apply(searchkey, currentkey);
            if (!matches) {
                if ((!done) && (currentkey.length() > length)) {
                    done = OneDistance.apply(searchkey, currentkey.substring(0, length));
                    if (!done) {
                        done = OneDistance.apply(searchkey, currentkey.substring(0, (length + 1)));
                    }
                }
            }
            if (done) {
                E value = couple.getValue();
                if (valueFilter.acceptValue(value)) {
                    InternalSearchResultUnit<E> searchResultItem = new InternalSearchResultUnit<E>(value, currentkey, defaultPosition);
                    searchResultItemList.add(searchResultItem);
                }
            }
        }
        return searchResultItemList;
    }


    /**
     * La classe InternalSearchResultItem stocke les informations d'un résultat
     * de recherche effectuée via la méthode
     */
    private static class InternalSearchResultUnit<E> implements SearchResultUnit<E> {

        private final E value;
        private final SubstringPosition collatedSearchTextPosition;
        private final String collatedKey;

        /*
         * Construction du résultat de recherche.
         */
        private InternalSearchResultUnit(E value, String collatedKey, SubstringPosition collatedSearchTextPosition) {
            this.value = value;
            this.collatedKey = collatedKey;
            this.collatedSearchTextPosition = collatedSearchTextPosition;
        }

        /**
         * Retourne la valeur trouvée par la recherche.
         */
        @Override
        public E getValue() {
            return value;
        }

        /**
         * Retourne la position du texte de recherche (après « collation ») au
         * sein de la clé.
         *
         */
        @Override
        public SubstringPosition getSearchTextPositionInCollatedKey() {
            return collatedSearchTextPosition;
        }

        /**
         * Retourne la clé à laquelle est stockée la valeur trouvée dans
         * l'instance SortedCollatedKeyMap.
         */
        @Override
        public String getCollatedKey() {
            return collatedKey;
        }

    }

}
