package de.renew.net;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;


/**
 * A TestTokenBag contains all the tokens contained in a place that are currently tested by transitions.
 */
public class TestTokenBag implements Serializable {
    // I prepare an array with small integers to avoid
    // creating excessive amounts of temporary integer objects.
    private static final int MAX_STATIC_MULT = 15;
    private static final Integer[] MULTIPLICITIES;

    static {
        MULTIPLICITIES = new Integer[MAX_STATIC_MULT + 1];
        for (int i = 0; i <= MAX_STATIC_MULT; i++) {
            MULTIPLICITIES[i] = i;
        }
    }

    /**
     * The amount of tokens that the TestTokenBag contains.
     */
    private int _size;
    /**
     * A Map associating tokens in the TestTokenBag with how often the TestTokenBag contains each of them.
     */
    private final Map<Object, Integer> _testCount;
    /**
     * A Map associating tokens in the TestTokenBag with the time at which they were initially added to it.
     */
    private final Map<Object, Double> _orgTime;

    TestTokenBag() {
        _testCount = new HashMap<Object, Integer>();
        _orgTime = new HashMap<Object, Double>();
        _size = 0;
    }

    // Synchronize just to be safe. Maybe this is overly cautious.
    /**
     * Returns the amount of unique tokens in the TestTokenBag.
     *
     * @return the amount of unique tokens in the TestTokenBag
     */
    public synchronized int getUniqueSize() {
        return _testCount.size();
    }

    /**
     * Checks how often a given token is included in the TestTokenBag.
     *
     * @param elem the token to check for
     * @return how often {@code elem} is included in the TestTokenBag
     */
    public synchronized int getTestMultiplicity(Object elem) {
        return _testCount.getOrDefault(elem, 0);
    }

    private static Integer toInteger(int result) {
        if (result <= MAX_STATIC_MULT) {
            return MULTIPLICITIES[result];
        }
        return result;
    }

    /**
     * Gets the unique elements the TestTokenBag contains.
     *
     * @return a Collection containing every unique element of the TestTokenBag
     */
    public synchronized Collection<Object> uniqueElements() {
        // Unfortunately, we have to make a copy of the
        // elements, because the hashtable might be updated
        // asynchronously.
        return new ArrayList<>(_testCount.keySet());
    }

    /**
     * Checks if a given token is included in the TestTokenBag.
     *
     * @param elem the token to test for
     * @return whether {@code elem} is included in the TestTokenBag
     */
    public synchronized boolean includesTested(Object elem) {
        return _testCount.containsKey(elem);
    }

    // A token bag should only be updated under control of its place.
    // Therefore, this method is not made public.
    //
    // If this is supposed to change, a place instance may not
    // expose its token bag any longer.
    synchronized void addTested(Object elem, double time) {
        if (_testCount.containsKey(elem)) {
            _testCount.computeIfPresent(elem, (k, num) -> toInteger(num + 1));
        } else {
            // We are testing the first token.
            // For this token, we record its time.
            _testCount.put(elem, toInteger(1));
            _orgTime.put(elem, time);
        }
        ++_size;
    }

    // A token bag should only be updated under control of its place.
    // Therefore, this method is not made public.
    //
    // If this is supposed to change, a place instance may not
    // expose its token bag any longer.
    synchronized double removeTested(Object elem) {
        Integer count = _testCount.get(elem);
        if (count == null) {
            throw new RuntimeException("Negative number of tokens detected.");
        }

        double time = _orgTime.get(elem);
        int newCount = count - 1;

        if (newCount == 0) {
            _testCount.remove(elem);
            _orgTime.remove(elem);
        } else {
            _testCount.put(elem, newCount); // autoboxing
        }

        --_size;
        return time;
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("TestTokenBag(size: ");
        buffer.append(_size);
        buffer.append("; count'token@time:");
        for (Object token : _testCount.keySet()) {
            buffer.append(' ');
            buffer.append(_testCount.get(token));
            buffer.append('\'');
            buffer.append(token);
            buffer.append('@');
            buffer.append(_orgTime.get(token));
        }
        buffer.append(')');
        return buffer.toString();
    }
}