package de.renew.net;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;

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

/**
 * A net, consisting of places and transitions.
 * <p>
 * Nets are not robust against concurrent updates.
 * Specifically, it is not allowed to change the structure
 * of a net while a simulation is running. The standard
 * pattern is to create a net, make it public, and then
 * use it without ever modifying it again.
 */
public class Net implements Serializable {
    /**
     * The logger used in this net.
     */
    private static final Logger LOGGER = Logger.getLogger(Net.class);
    @Serial
    private static final long serialVersionUID = 4424943946669800287L;
    /**
     * The set of places in the net.
     */
    private final Set<Place> _places = new HashSet<>();
    /**
     * The set of transitions in the net.
     */
    private final Set<Transition> _transitions = new HashSet<>();
    /**
     * A Map identifying the places of the net by their ID.
     */
    private final Map<NetElementID, Place> _placesByID = new HashMap<>();
    /**
     * A Map identifying the transitions of the net by their ID.
     */
    private final Map<NetElementID, Transition> _transitionsByID = new HashMap<>();
    /**
     * The name of the net.
     */
    private String _name = null;
    /**
     * Whether the initial tokens should be inserted early.
     */
    private boolean _earlyTokens = false;
    /**
     * Was previously used to count/number the amount of instances the net (?). Now unused.
     */
    private int _netCount = 0;
    /**
     * The object responsible for creating instances of this net.
     */
    private INetInstantiator _instantiator = null;


    /**
     * Constructs an empty net body without a name.
     */
    public Net() {
        this(null);
    }

    /**
     * Creates an empty net body with the given name. Since the net
     * is not functional yet, it is not announced to the map for
     * the {@link INetLookup#findForName(String)} method. This has to be done
     * separately after full construction of the net by using the
     * {@link INetLookup#makeNetKnown(Net)} method.
     *
     * @param name the name to give to the emtpy net body
     **/
    public Net(String name) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        _name = name;
    }

    /**
     * Gets the places of the net.
     *
     * @return the places of the net
     */
    public Collection<Place> places() {
        return _places;
    }

    /**
     * Gets the amount of places in the net.
     *
     * @return the amount of places the net contains
     */
    public int placeCount() {
        return _places.size();
    }

    /**
     * Gets the transitions of the net.
     *
     * @return the transitions of the net
     */
    public Collection<Transition> transitions() {
        return _transitions;
    }

    /**
     * Gets the amount of transitions in the net.
     *
     * @return the amount of transitions the net contains
     */
    public int transitionCount() {
        return _transitions.size();
    }

    synchronized int makeNetNumber() {
        return ++_netCount;
    }

    @Override
    public String toString() {
        return _name;
    }

    /**
     * Gets the name of the net.
     *
     * @return the name of the net
     */
    public String getName() {
        return _name;
    }

    /**
     * Sets the name of this {@code Net}. Callers are responsible to ensure that there are no known nets of
     * the same name simultaneously.
     *
     * @param newName the new name
     */
    public void setName(String newName) {
        _name = newName;
    }

    /**
     * Creates a new instantiator for this net. Subclasses can override this method to inject their own instantiators.
     *
     * @return a new instantiator for this net
     */
    protected INetInstantiator createNetInstantiator() {
        return new NetInstantiator(this);
    }

    /**
     * Returns the instantiator for this net.
     *
     * @return the instantiator for this net
     */
    public INetInstantiator getInstantiator() {
        if (_instantiator == null) {
            _instantiator = createNetInstantiator();
        }
        return _instantiator;
    }

    void add(Place place) {
        NetElementID id = place.getID();
        assert !_places.contains(place) : "Tried to add existing place: " + place;
        assert !_placesByID.containsKey(id) : "Tried to add place with existing ID: " + id
            + ", old: " + _placesByID.get(id) + ", new: " + place;
        _places.add(place);
        _placesByID.put(id, place);
    }

    void add(Transition transition) {
        NetElementID id = transition.getID();
        assert !_transitions.contains(transition)
            : "Tried to add existing transition: " + transition;
        assert !_transitionsByID.containsKey(id) : "Tried to add transition with existing ID: " + id
            + ", old: " + _transitionsByID.get(id) + ", new: " + transition;
        _transitions.add(transition);
        _transitionsByID.put(id, transition);
    }

    void remove(Place place) {
        _places.remove(place);
        _placesByID.remove(place.getID());
    }

    void remove(Transition transition) {
        _transitions.remove(transition);
        _transitionsByID.remove(transition.getID());
    }

    /**
     * Getter for the {@code earlyToken}
     *
     * @return a boolean on the initial tokens should be inserted early
     */
    public boolean getEarlyTokens() {
        return _earlyTokens;
    }

    /**
     * Sets whether the net's initial tokens should be inserted early when starting a simulation.
     *
     * @param earlyTokens whether the initial tokens should be inserted early
     */
    public void setEarlyTokens(boolean earlyTokens) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        _earlyTokens = earlyTokens;
    }

    /**
     * Serialization method, behaves like default writeObject
     * method except using the domain trace feature, if the
     * output is a RenewObjectOutputStream.
     * <p>
     * See also: {@link de.renew.util.RenewObjectOutputStream}
     *
     * @param out target stream (see note about RenewObjectOutputStream)
     * @throws IOException if an error occurs while writing
     **/
    @Serial
    private void writeObject(ObjectOutputStream out) throws IOException {
        RenewObjectOutputStream rOut = null;
        if (out instanceof RenewObjectOutputStream) {
            rOut = (RenewObjectOutputStream) out;
        }
        if (rOut != null) {
            rOut.beginDomain(this);
        }
        out.defaultWriteObject();
        if (rOut != null) {
            rOut.endDomain(this);
        }
    }

    // ----------------------------------------------- ID handling ----


    /**
     * Return the place of the net with the given ID.
     * Return {@code null}, if no such place exists.
     * <p>
     * Added Mon Jul 17  2000 by Michael Duvigneau
     *
     * @param id the place's ID, may not be null
     * @return a place or {@code null}
     */
    public Place getPlaceWithID(NetElementID id) {
        return _placesByID.get(id);
    }

    /**
     * Return the transition of the net with the given ID.
     * Return {@code null}, if no such place exists.
     * <p>
     * Added Mon Jul 17  2000 by Michael Duvigneau
     *
     * @param id the transition's ID, may not be null
     * @return a transition or {@code null}
     */
    public Transition getTransitionWithID(NetElementID id) {
        return _transitionsByID.get(id);
    }
}