package net.sf.jabref.model.database;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import net.sf.jabref.model.database.event.EntryAddedEvent;
import net.sf.jabref.model.database.event.EntryRemovedEvent;
import net.sf.jabref.model.entry.BibEntry;
import net.sf.jabref.model.entry.event.FieldChangedEvent;

import com.google.common.eventbus.Subscribe;

/**
 * Determines which bibtex cite keys are duplicates in a single {@link BibDatabase}.
 */
public class DuplicationChecker {

    /** use a map instead of a set since I need to know how many of each key is in there */
    private final Map<String, Integer> allKeys = new HashMap<>();


    /**
     * Checks if there is more than one occurrence of this key
     */
    public boolean isDuplicateCiteKeyExisting(String citeKey) {
        return getNumberOfKeyOccurrences(citeKey) > 1;
    }

    /**
     * Checks if there is more than one occurrence of the cite key
     */
    public boolean isDuplicateCiteKeyExisting(BibEntry entry) {
        return isDuplicateCiteKeyExisting(entry.getCiteKeyOptional().orElse(null));
    }

    /**
     * Returns the number of occurrences of the given key in this database.
     */
    public int getNumberOfKeyOccurrences(String citeKey) {
        return allKeys.getOrDefault(citeKey, 0);
    }

    /**
     * Helper function for counting the number of the key usages.
     * Adds the given key to the internal keyset together with the count of it.
     * The counter is increased if the key already exists, otherwise set to 1.
     * <br>
     * Special case: If a null or empty key is passed, it is not counted and thus not added.
     *
     * Reasoning:
     * Consider this: I add a key xxx, then I add another key xxx. I get a warning. I delete the key xxx.
     * Consider JabRef simply removing this key from a set of allKeys.
     * Then I add another key xxx. I don't get a warning!
     * Thus, I need a way to count the number of keys of each type.
     * Solution: hashmap=>int (increment each time at add and decrement each time at remove)
     */
    private void addKeyToSet(String key) {
        if (key == null || key.isEmpty()) {
            return;
        }

        allKeys.put(key, getNumberOfKeyOccurrences(key) + 1);
    }

    /**
     * Helper function for counting the number of the key usages.
     * Removes the given key from the internal keyset together with the count of it, if the key is set to 1.
     * If it is not set to 1, the counter will be decreased.
     * <br>
     * Special case: If a null or empty key is passed, it is not counted and thus not removed.
     */
    private void removeKeyFromSet(String key) {
        if (key == null || key.isEmpty()) {
            return;
        }

        int numberOfKeyOccurrences = getNumberOfKeyOccurrences(key);
        if (numberOfKeyOccurrences > 1) {
            allKeys.put(key, numberOfKeyOccurrences - 1);
        } else {
            allKeys.remove(key);
        }
    }

    @Subscribe
    public void listen(FieldChangedEvent fieldChangedEvent) {
        if (fieldChangedEvent.getFieldName().equals(BibEntry.KEY_FIELD)) {
            removeKeyFromSet(fieldChangedEvent.getOldValue());
            addKeyToSet(fieldChangedEvent.getNewValue());
        }
    }

    @Subscribe
    public void listen(EntryRemovedEvent entryRemovedEvent) {
        Optional<String> citeKey = entryRemovedEvent.getBibEntry().getCiteKeyOptional();
        if (citeKey.isPresent()) {
            removeKeyFromSet(citeKey.get());
        }
    }

    @Subscribe
    public void listen(EntryAddedEvent entryAddedEvent) {
        Optional<String> citekey = entryAddedEvent.getBibEntry().getCiteKeyOptional();
        if (citekey.isPresent()) {
            addKeyToSet(citekey.get());
        }
    }

}
