package de.renew.net;

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

import de.renew.engine.searchqueue.SearchQueue;


/**
 * A TokenBag contains all the tokens stored in a place.
 */
public class TokenBag implements Serializable {
    /**
     * The amount of tokens that the TokenBag contains.
     */
    private int _size;
    /**
     * A Map associating Objects that are in the TokenBag at some point with the {@link TimeSet sets of times} at which
     * they are in it.
     */
    private final Map<Object, TimeSet> _map;

    TokenBag() {
        _map = new HashMap<Object, TimeSet>();
        _size = 0;
    }

    /**
     * Returns the delay until the availability of a given token.
     * Positive infinity signals that the tokens will never
     * become available by simply waiting.
     *
     * @param elem the token whose delay should be returned
     * @param delays the set of delays that will be applied to the earliest times the token is in the TokenBag
     * @return the delay until the availability of a given token
     */
    public double computeEarliestTime(Object elem, TimeSet delays) {
        TimeSet value = _map.get(elem);
        if (value != null) {
            // Earliest
            return value.computeEarliestTime(delays);
        }

        if (delays.getSize() == 0) {
            // This should not happen, but we are always enabled.
            return 0;
        }

        return Double.POSITIVE_INFINITY;
    }

    /**
     * Gets the times when a given {@code Object} is in the {@code TokenBag}.
     *
     * @param elem the {@code Object} whose times should be returned
     * @return the set of times when the {@code Object} is in the {@code TokenBag}
     */
    public synchronized TimeSet getTimeSet(Object elem) {
        return _map.getOrDefault(elem, TimeSet.EMPTY);
    }

    /**
     * Returns the multiplicity of a given Object in the TokenBag.
     *
     * @param elem the Object to check
     * @return the multiplicity of {@code elem} in the tokenBag
     */
    public synchronized int getMultiplicity(Object elem) {
        var value = _map.get(elem);
        return (value != null) ? value.getSize() : 0;
    }

    /**
     * Returns the unique elements contained by this {@code TokenBag}.
     *
     * @return a {@code Collection} containing every unique element of this {@code TokenBag}
     */
    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<>(_map.keySet());
    }

    /**
     *  Checks if a given Object was contained in the TokenBag, ignoring timestamps.
     *
     * @param elem the Object to check
     * @return whether the Object was contained in the TokenBag at any time
     */
    public synchronized boolean includesAnytime(Object elem) {
        return _map.containsKey(elem);
    }

    /**
     * Checks if a given Object was contained in the TokenBag before or at a given time.
     *
     * @param elem the Object to check
     * @param time the time to check for
     * @return whether the Object was in the TokenBag before or at the given time
     */
    public synchronized boolean includesBefore(Object elem, double time) {
        TimeSet times = _map.get(elem);
        if (times == null) {
            return false;
        }
        return time >= times.earliestTime();
    }

    void add(Object elem, double time) {
        add(elem, time, 1);
    }

    // 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 add(Object elem, double time, int n) {
        _map.compute(
            elem, (k, times) -> times != null ? times.including(time, n) : TimeSet.make(time, n));

        _size += n;
    }

    // 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 removeOneOf(Object elem, double time) {
        TimeSet times = _map.get(elem);
        if (times == null) {
            throw new RuntimeException("Negative number of tokens detected.");
        }

        times = times.excluding(time);

        if (times.isEmpty()) {
            _map.remove(elem);
        } else {
            _map.put(elem, times);
        }

        --_size;
    }

    // A token bag should only be updated under control of its place.
    // Therefore, this method is not made public.
    // 
    // This method returns the times stamp of the token that is actually
    // removed so that the token can be put back easily.
    synchronized double removeWithDelay(Object elem, double delay) {
        TimeSet times = _map.get(elem);
        if (times == null) {
            throw new NoSuchElementException();
        }

        double time = times.latestWithDelay(delay, SearchQueue.getTime());
        times = times.excluding(time);

        if (times.isEmpty()) {
            _map.remove(elem);
        } else {
            _map.put(elem, times);
        }

        --_size;
        return time;
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("TokenBag(size: ");
        buffer.append(_size);
        buffer.append("; token@timeset:");
        for (Object token : _map.keySet()) {
            buffer.append(' ');
            buffer.append(token);
            buffer.append('@');
            buffer.append(_map.get(token));
        }
        buffer.append(')');
        return buffer.toString();
    }
}