package de.duehl.basics.collections;

/*
 * Copyright 2019 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Diese Klasse stellt ein Verzeichnis von Schlüssel-Wert Paaren zur Verfügung, das außerdem die
 * Schlüssel in der Reihenfolge ihres Einfügens zurückgeben kann. Es dürfen keine Schlüssel
 * mehrfach hinzugefügt werden und null-Werte sind sowohl für Schlüssel als auch für Werte
 * verboten.
 *
 * Inspiriert von der folgenden Klasse aus dem Projekt MazOrgPlus:
 *     de.heinsundpartner.mazorg.plus.common.activemq.answer.Answer
 *
 * @version 1.01     2019-05-29
 * @author Christian Dühl
 */

public class OrderedNonOverwritingMap<K,V> {

    /** Verzeichnis der Schlüssel-Wert-Paare. */
    private final Map<K, V> valuesByKeys;

    /** Liste der Schlüssel in der Reihenfolge ihres Hinzufügens. */
    private final List<K> orderedKeys;

    /** Konstruktor. */
    public OrderedNonOverwritingMap() {
        valuesByKeys = new HashMap<>();
        orderedKeys = new ArrayList<>();
    }

    /**
     * Fügt ein Schlüssel-Wert-Paar hinzu.
     *
     * Der Schlüssel darf noch nicht vorhanden sein, außerdem dürfen Schlüssel und Wert nicht null
     * sein, in diesem Fällen wird eine Ausnahme geworfen.
     */
    public void put(K key, V value) {
        checkAddKeyValuePairConstraints(key, value);
        putUnchecked(key, value);
    }

    private void checkAddKeyValuePairConstraints(K key, V value) {
        if (null == key) {
            throw new IllegalArgumentException("Der Schlüssel darf nicht null sein!");
        }

        if (null == value) {
            throw new IllegalArgumentException("Der Wert darf nicht null sein!");
        }

        if (containsKey(key)) {
            throw new IllegalArgumentException("Der Schlüssel '" + key + "' ist bereits vorhanden!");
        }
    }

    private void putUnchecked(K key, V value) {
        valuesByKeys.put(key, value);
        orderedKeys.add(key);
    }

    /** Gibt die Anzahl an Schlüssel-Wert-Paaren an. */
    public int size() {
        return valuesByKeys.size();
    }

    /** Prüft ob zum gegebenen Schlüssel ein Schlüssel-Wert-Paar gespeichert ist. */
    public boolean containsKey(K key) {
        return valuesByKeys.containsKey(key);
    }

    /**
     * Ermittelt den Wert zum gegebenen Schlüssel.
     *
     * Ist der Schlüssel unbekannt, wird eine Ausnahme geworfen.
     */
    public V get(K key) {
        if (!containsKey(key)) {
            throw new IllegalArgumentException("Kein Eintrag zum Schlüssel '" + key + "' gefunden!");
        }
        return valuesByKeys.get(key);
    }

    /** Gibt eine ungeordnete Menge der gespeicherten Schlüssel zurück. */
    public Set<K> keySet() {
        return valuesByKeys.keySet();
    }

    /** Gibt die in der Reihenfolge ihres Hinzufügens geordnete Liste der Schlüssel zurück. */
    public List<K> getOrderedKeys() {
        return orderedKeys;
    }

    /** Prüft, ob die Map leer ist. */
    public boolean isEmpty() {
        return orderedKeys.isEmpty();
    }

    /** Gibt an, ob der angegebene Wert in mindestens einem Schlüssel-Wert-Paar enthalten ist. */
    public boolean containsValue(V value) {
        return valuesByKeys.containsValue(value);
    }

    /** Leert die Map. */
    public void clear() {
        valuesByKeys.clear();
        orderedKeys.clear();
    }

    /**
     * Fügt die Schlüssel-Wert-Paare einer anderen OrderedNonOverwritingMap mit den gleichen
     * Schlüssel- und Wert-Typen dieser hinzu.
     *
     * Wenn ein Schlüssel der übergebenen OrderedNonOverwritingMap in dieser Antwort bereits
     * bekannt ist, wird eine Ausnahme geworfen.
     */
    public void addOrderedNonOverwritingMap(OrderedNonOverwritingMap<K,V> that) {
        for (K key : that.getOrderedKeys()) {
            if (containsKey(key)) {
                throw new IllegalArgumentException(
                        "Die Antwort hat bereits ein Feld mit dem " + "Schlüssel '" + key + "'.");
            }
            putUnchecked(key, that.get(key));
            // Ohne die Prüfungen in put, da diese beim Einfügen in that bereits erfolgten!
        }
    }

    /**
     * Gibt eine String-Representation des Objekts mit seinen Schlüssel-Wert-Paaren an, wobei die
     * Schlüssel aus Gründen der Testbarkeit sortiert ausgegeben werden.
     */
    @Override
    public String toString() {
        return toString("OrderedNonOverwritingMap");
    }

    /**
     * Gibt eine String-Representation des Objekts mit seinen Schlüssel-Wert-Paaren an, wobei die
     * Schlüssel aus Gründen der Testbarkeit sortiert ausgegeben werden.
     *
     * @param title
     *            Titel des Objekts.
     */
    public String toString(String title) {
        StringBuilder builder = new StringBuilder();
        builder.append(title + " [");
        boolean first = true;
        for (K key : getOrderedKeys()) {
            if (first) {
                first = false;
            }
            else {
                builder.append(", ");
            }

            V value = get(key);
            builder.append(key).append("=").append(value);
        }
        builder.append("]");
        return builder.toString();
    }

    /** Löscht den ersten bzw. ältesten Eintrag in dem Hash. */
    public void removeOldestEntry() {
        if (!isEmpty()) {
            K oldestKey = orderedKeys.get(0);
            orderedKeys.remove(oldestKey);
            valuesByKeys.remove(oldestKey);
        }
    }

}
