package de.renew.net;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import de.renew.engine.thread.SimulationThreadPool;
import de.renew.util.RenewObjectOutputStream;


/**
 * An IDRegistry assigns persistent IDs to objects and counts
 * references to the IDs. IDs are discarded when reference counter
 * reaches zero.
 * <p>
 * Since reference counting is not only used during simulation but
 * also during garbage collection, this class does not assume that
 * all methods are called within simulation threads. However,
 * serialization and de-serialization must occur within simulation
 * threads.
 *
 * @author Kummer
 */
public class IDRegistry implements Serializable {

    /** The single well-known instance of the IDRegistry. */
    private static IDRegistry _instance = null;
    private transient Map<Object, Object> _table;

    /**
     * Constructs a new, empty IDRegistry.
     */
    public IDRegistry() {
        _table = new HashMap<Object, Object>();
    }

    /**
     * Get the single well-known instance of the {@code IDRegistry} class.
     * Although other instances are possible, this instance is
     * specifically created to be used whenever a local
     * {@code IDRegistry} is not available.
     *
     * @return the single well-known instance of {@code IDRegistry}
     */
    public static synchronized IDRegistry getInstance() {
        if (_instance == null) {
            _instance = new IDRegistry();
        }
        return _instance;
    }

    // Needn't be synchronized, because only called from
    // synchronized methods.
    private IDCounter register(Object elem) {
        IDCounter counter = (IDCounter) _table.get(elem);
        if (counter == null) {
            counter = new IDCounter(IDSource.createID());
            _table.put(elem, counter);
        }
        return counter;
    }

    /**
     * Gets the ID of a given {@code Object}. If not already done, the {@code Object} is also registered.
     *
     * @param elem the Object to be registered
     * @return the Object's ID
     */
    public synchronized String getID(Object elem) {
        return register(elem).getID();
    }

    /**
     * Reserves the given token a given amount of times so that it keeps its ID.
     *
     * @param elem the token that should keep its ID
     * @param n the amount of times the token should be reserved
     */
    public synchronized void reserve(Object elem, int n) {
        register(elem).reserve(n);
    }

    /**
     * Reserves the given token once so that it keeps its ID.
     *
     * @param elem the token that should be reserved
     */
    public synchronized void reserve(Object elem) {
        register(elem).reserve();
    }

    /**
     * Reduces the reservation count of a given token by 1.
     * If the reservation count reaches 0, the current ID of the token is discarded.
     *
     * @param elem the object to be unreserved
     */
    public synchronized void unreserve(Object elem) {
        IDCounter counter = register(elem);
        counter.unreserve();
        if (counter.isDiscardable()) {
            _table.remove(elem);
        }
    }

    /**
     * Sets the ID of an object and reserves that ID once.
     *
     * @param elem the Object whose ID should be set and reserved
     * @param id the new ID of the object
     * @throws RuntimeException if the given Object already has an ID which is different to the given ID
     */
    public synchronized void setAndReserveID(Object elem, String id) {
        IDCounter counter = (IDCounter) _table.get(elem);
        if (counter == null) {
            counter = new IDCounter(id);
            _table.put(elem, counter);
        } else if (!counter.getID().equals(id)) {
            throw new RuntimeException("Token already in use.");
        }
        counter.reserve();
    }

    /**
     * Deserializes an IDRegistry from an ObjectInputStream, manually reading its transient {@code table} attribute.
     *
     * @param in the stream to read objects from
     * @throws IOException if an error occurs while reading from the stream
     * @throws ClassNotFoundException if an object of an unknown class is read from the stream
     */
    @Serial
    private synchronized void readObject(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        in.defaultReadObject();
        _table = new HashMap<Object, Object>();
        boolean finished = false;
        do {
            Object key = in.readObject();
            if (key == null) {
                finished = true;
            } else {
                Object counter = in.readObject();
                _table.put(key, counter);
            }
        } while (!finished);
    }

    /**
     * Serializes an IDRegistry from an ObjectOutputStream, manually writing its transient {@code table} attribute.
     *
     * @param out the stream to serialize to
     * @throws IOException if an error occurs while writing to the stream
     */
    @Serial
    private synchronized void writeObject(ObjectOutputStream out) throws IOException {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        RenewObjectOutputStream rOut = null;
        if (out instanceof RenewObjectOutputStream) {
            rOut = (RenewObjectOutputStream) out;
        }
        if (rOut != null) {
            rOut.beginDomain(this);
        }
        out.defaultWriteObject();
        for (Entry<Object, Object> entry : _table.entrySet()) {
            Object key = entry.getKey();
            Object counter = entry.getValue();
            out.writeObject(key);
            out.writeObject(counter);
        }
        out.writeObject(null);
        if (rOut != null) {
            rOut.endDomain(this);
        }
    }

    /**
     * Removes the single well-known instance of the IDRegistry.
     */
    public synchronized static void reset() {
        _instance = null;
    }

    /**
     * Writes the single well-known instance of the IDRegistry to a given ObjectOutputPort.
     *
     * @param out the output port to write to
     * @throws IOException if an error occurs while writing to the stream
     */
    public synchronized static void save(ObjectOutput out) throws IOException {
        out.writeObject(_instance);
    }

    /**
     * Replaces the single well-known instance of the IDRegistry with one read in from a given ObjectInputPort.
     *
     * @param in the input port to read from
     * @throws IOException if an error occurs while reading from the stream
     * @throws ClassNotFoundException if an object of an unknown class is read from the stream
     */
    public synchronized static void load(ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        _instance = (IDRegistry) in.readObject();
    }
}