package de.renew.net;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;

import org.apache.log4j.Logger;

import de.renew.database.Transaction;
import de.renew.database.TransactionSource;
import de.renew.engine.common.SimulatorEventLogger;
import de.renew.engine.events.NetInstantiation;
import de.renew.engine.searcher.UplinkProvider;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.simulatorontology.simulation.StepIdentifier;
import de.renew.unify.Impossible;
import de.renew.util.DelayedFieldOwner;
import de.renew.util.RenewObjectInputStream;
import de.renew.util.RenewObjectOutputStream;

/**
 * The implementation of {@link NetInstance}.
 */
public class NetInstanceImpl implements DelayedFieldOwner, NetInstance {
    /** The logger that NetInstanceImpls use. */
    public static Logger _logger = Logger.getLogger(NetInstanceImpl.class);
    @Serial
    private static final long serialVersionUID = 6136525457931796546L;

    /**
     * Configurable flag, determines whether to use a global token
     * {@link IDRegistry} for all net instances or individual token
     * registries per instance.  The flag is evaluated during net
     * instance initialization (see {@link #initNet(Net, boolean)}).
     **/
    public static boolean _useGlobalIDRegistry = false;

    /**
     * Map from net elements to net instance elements.
     * Contains pairs of the following types:
     * <ul>
     * <li> {@link Place} -> {@link PlaceInstance} </li>
     * <li> {@link Transition} -> {@link TransitionInstance} </li>
     * </ul>
     * The map is initialized in the {@link #initNet(Net, boolean)}
     * method.
     **/
    private Hashtable<Object, Object> _instanceLookup;

    /**
     * Reference to the net template that underlies this instance.
     **/
    private Net _net;

    /**
     * The unique, persistent identifier of this net instance.
     * It persists for the lifetime of the net instance within
     * one Java VM.  It can change on deserialization given that
     * {@link RenewObjectInputStream#isCopiousBehaviour()}
     * returns {@code true}.
     **/
    private String _netID;

    /**
     * The IDRegistry that is responsible for this net instance.
     * The registry is kept in a field, because the net instance
     * has to deregister its tokens at exactly this net instance
     * during finalization. The current registry might have
     * changed by that time.
     * <p>
     * This field is not really transient, but as we want
     * to cut down the serialization recursion depth, it
     * is serialized manually.</p>
     */
    private transient IDRegistry _registry;

    /**
     * Constructs a new NetInstanceImpl without initializing it.
     */
    protected NetInstanceImpl() {}

    /**
     * Constructs a new NetInstanceImpl and initializes it based on a given Net.
     * Initial markings are calculated.
     *
     * @param net the Net the NetInstanceImpl is an instance of
     * @throws Impossible if the net is invalid (see {@link #initNet})
     */
    protected NetInstanceImpl(Net net) throws Impossible {
        this(net, true);
    }

    /**
     * Constructs a new NetInstanceImpl and initializes it based on a given Net.
     *
     * @param net the Net the NetInstanceImpl is an instance of
     * @param wantInitialTokens whether initial markings should be calculated
     * @throws Impossible if the net is invalid (see {@link #initNet})
     */
    protected NetInstanceImpl(Net net, boolean wantInitialTokens) throws Impossible {
        initNet(net, wantInitialTokens);
    }

    /**
     * Initializes this NetInstanceImpl based on a given Net.
     *
     * @param net the Net this should be an instance of
     * @param wantInitialTokens whether initial markings should be calculated
     * @throws Impossible if the net is invalid (is null or contains places of an invalid
     *                    {@link de.renew.net.Place#getPlacetype}.
     */
    protected void initNet(Net net, boolean wantInitialTokens) throws Impossible {
        if (net == null) {
            throw new Impossible();
        }

        _net = net;
        _netID = IDSource.createID();
        _registry = _useGlobalIDRegistry ? IDRegistry.getInstance() : new IDRegistry();
        _instanceLookup = new Hashtable<>();

        // Create instances for all places
        for (Place place : net.places()) {
            _instanceLookup.put(place, place.makeInstance(this, wantInitialTokens));
        }

        // Create instances for all transitions
        for (Transition transition : net.transitions()) {
            _instanceLookup.put(transition, new TransitionInstance(this, transition));
        }
    }

    @Override
    public String toString() {
        return _net + "[" + _netID + "]";
    }

    /**
     * Get the IDRegistry that is responsible for this net instance.
     */
    @Override
    public IDRegistry getRegistry() {
        return _registry;
    }

    /**
     * Query the ID of the net instance. By default the ID is a
     * simple number, but this is not guaranteed.
     *
     * @return the current ID string
     */
    @Override
    public String getID() {
        return _netID;
    }

    /**
     * Set the ID of the net instance. This should only be done
     * during the setup of the net instance, typeically after restoring
     * the net from a saved state.
     *
     * @param id the new ID string
     */
    @Override
    public void setID(String id) {
        _netID = id;
    }

    @Override
    public void earlyConfirmation() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        // First, the net is made public to the database.
        Transaction transaction = TransactionSource.get();
        try {
            transaction.createNet(this);
        } catch (Exception e) {
            _logger.error(e.getMessage(), e);
        }

        // Afterwards, the places are notified, too.
        for (Place place : _net.places()) {
            getInstance(place).earlyConfirmation();
        }
    }

    @Override
    public void earlyConfirmationTrace(StepIdentifier stepIdentifier) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        for (Place place : _net.places()) {
            getInstance(place).earlyConfirmationTrace(stepIdentifier);
        }

        NetInstanceList.add(this);
    }

    @Override
    public void lateConfirmation(StepIdentifier stepIdentifier) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        // log instance creation on net level
        SimulatorEventLogger.log(stepIdentifier, new NetInstantiation(this), this);


        // The places may now fully acquire their correct initial marking,
        // even if the initial marking is inserted late.
        for (Place place : _net.places()) {
            getInstance(place).lateConfirmation(stepIdentifier);
        }


        // Let's inform all transitions of the confirmation.
        // The transitions can insert themselves into the search queue.
        for (Transition transition : _net.transitions()) {
            getInstance(transition).createConfirmation();
        }
    }

    @Override
    public void createConfirmation(StepIdentifier stepIdentifier) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        earlyConfirmation();
        earlyConfirmationTrace(stepIdentifier);
        lateConfirmation(stepIdentifier);
    }

    @Override
    public Object getInstance(Object netObject) {
        return _instanceLookup.get(netObject);
    }

    @Override
    public PlaceInstance getInstance(Place place) {
        return (PlaceInstance) _instanceLookup.get(place);
    }

    @Override
    public TransitionInstance getInstance(Transition transition) {
        return (TransitionInstance) _instanceLookup.get(transition);
    }

    @Override
    public Net getNet() {
        return _net;
    }

    @Override
    public Collection<UplinkProvider> getUplinkProviders(String channel) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        List<UplinkProvider> result = new ArrayList<UplinkProvider>();

        for (Transition transition : getNet().transitions()) {
            TransitionInstance instance = getInstance(transition);
            if (instance.listensToChannel(channel)) {
                result.add(instance);
            }
        }
        return result;
    }

    /**
     * Serialization method, behaves like default writeObject
     * method except using the domain trace feature, if the
     * output is a {@link RenewObjectOutputStream}.
     *
     * @param out the stream to serialize to
     * @throws IOException if an error occurs while writing
     * @see de.renew.util.RenewObjectOutputStream
     **/
    @Serial
    private void writeObject(ObjectOutputStream out) throws IOException {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";

        RenewObjectOutputStream renewOut =
            (out instanceof RenewObjectOutputStream) ? (RenewObjectOutputStream) out : null;

        if (renewOut != null) {
            renewOut.beginDomain(this);
        }

        out.defaultWriteObject();

        if (renewOut != null) {
            renewOut.delayedWriteObject(_registry, this);
            renewOut.endDomain(this);
        } else {
            out.writeObject(_registry);
        }
    }

    /**
     * Deserialization method, behaves like default readObject
     * method except restoring additional transient fields.
     * The method restores the not-really-transient field
     * {@code registry}, <b>if</b> the stream used is
     * <b>not</b> a {@link RenewObjectInputStream}.
     * The method also repeats this instance's registration at the
     * {@link NetInstanceList}.
     * If the {@code copiousBehaviour} flag of the
     * {@code RenewObjectInputStream} is active, the net instance
     * assigns itself a new, unused net instance id.
     *
     * @param in the stream to read objects from
     * @throws IOException if an error occurs while reading
     * @throws ClassNotFoundException if an object is read whose class cannot be found
     */
    @Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {

        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";

        in.defaultReadObject();

        RenewObjectInputStream renewIn =
            (in instanceof RenewObjectInputStream) ? (RenewObjectInputStream) in : null;

        if (renewIn != null) {
            // Generate new ID for copied instances
            if (renewIn.isCopiousBehaviour()) {
                String oldNetID = _netID;
                _netID = IDSource.createID();

                if (_logger.isDebugEnabled()) {
                    _logger.debug(
                        "Deserialized NetInstance copy: changed id from " + oldNetID + " to "
                            + _netID + ".");
                }
            }
            // Besides this, do nothing.  The fields will be
            // reassigned by the stream soon.
        } else {
            _registry = (IDRegistry) in.readObject();
        }

        // This is normally done at the net instance's creation
        // confirmation, but the confirmation of this instance's creation
        // has taken place long ago and far away.
        // So we have to repeat it here, in our current universe.
        NetInstanceList.add(this);
    }

    /**
     * Deserialization method used by {@link RenewObjectInputStream}.
     * Reassigns a value to the not-really-transient field
     * {@code registry}.
     *
     * @exception java.io.NotSerializableException
     *    if the given value was not an IDRegistry object.
     */
    @Override
    public void reassignField(Object value) throws IOException {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        if (value instanceof IDRegistry) {
            _registry = (IDRegistry) value;
        } else {
            throw new NotSerializableException(
                "Value of unexpected type given to NetInstanceImpl.reassignField():"
                    + value.getClass().getName() + ".");
        }
    }
    // ### Problem: NetInstanceReference only used for database
    // serialization, not during normal simulation save.
    //    /**
    //     * Serialization method to replace the object
    //     * before it is serialized.
    //     * In this case, the NetInstanceImpl is replaced
    //     * by a NetInstanceImplReference.
    //     * @return A NetInstanceImplReference pointing to
    //     * the original NetInstanceImpl by its NetID.
    //     */
    //    private Object writeReplace()
    //    {
    //      return new NetInstanceReference(netID);
    //    }
}