package de.renew.simulatorontology.shadow;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import org.apache.log4j.Logger;

import de.renew.simulatorontology.serialisation.SerialisationListener;
import de.renew.simulatorontology.serialisation.SerialisationListenerRegistry;

/**
 * This class represents shadow net systems that contain {@link ShadowNet}
 * instances.
 */
public class ShadowNetSystem implements Serializable {
    /** The logger for this class. */
    private static final Logger LOGGER = Logger.getLogger(ShadowNetSystem.class);
    @Serial
    private static final long serialVersionUID = 4619730346099695519L;
    /** The {@code ShadowNet} instances that belong to this {@code ShadowNetSystem}. */
    private final Set<ShadowNet> _nets = new HashSet<>();
    private transient Set<ShadowNet> _compiledNets = new HashSet<>();
    private transient Set<ShadowNet> _uncompiledNets = new HashSet<>();
    private transient ShadowNetLoader _netLoader = null;

    /**
     * Creates a new, empty {@code ShadowNetSystem}. Using this constructor is not recommended, as every {@code ShadowNetSystem}
     * should have a registered compiler.
     */
    public ShadowNetSystem() {

    }

    /**
     * Creates a new {@code ShadowNetSystem} and sets the default compiler factory
     * and the net loader to the given instances.
     *
     * @param netLoader the net loader to use when a net is missing from the system
     */
    public ShadowNetSystem(ShadowNetLoader netLoader) {
        setNetLoader(netLoader);
    }

    public void recompile() {
        _uncompiledNets.addAll(_compiledNets);
        _compiledNets.clear();
    }

    /**
     * Sets a shadow net loader to be asked when a net is missing
     * during compilation.
     *
     * @param netLoader the net loader to use when a net is missing from the system
     **/
    public void setNetLoader(ShadowNetLoader netLoader) {
        _netLoader = netLoader;
    }

    /**
     * Returns the net loader that is used when a net is missing from the system.
     *
     * @return the net loader that is used when a net is missing from the system
     */
    public ShadowNetLoader getNetLoader() {
        return _netLoader;
    }

    /**
     * Adds the given net to this {@code ShadowNetSystem}.
     * This method should be called indirectly by creating a new {@code ShadowNet},
     * so that inconsistencies in the data structure cannot arise.
     *
     * @param net the net to add to this {@code ShadowNetSystem}
     */
    public void add(ShadowNet net) {
        _nets.add(net);
        if (!_compiledNets.contains(net)) {
            _uncompiledNets.add(net);
        }
    }

    /**
     * Removes the given net from this {@code ShadowNetSystem}.
     * This method should be called indirectly via {@link ShadowNet#discard()},
     * so that inconsistencies in the data structure cannot arise.
     *
     * @param net the net to remove from this {@code ShadowNetSystem}
     */
    protected void remove(ShadowNet net) {
        _nets.remove(net);
        _compiledNets.remove(net);
        _uncompiledNets.remove(net);
    }

    /**
     * Marks the given net as compiled, if it's in this {@code ShadowNetSystem}.
     *
     * @param net the net that should be marked as compiled
     */
    public void markAsCompiled(ShadowNet net) {
        if (_nets.contains(net)) {
            _compiledNets.add(net);
            _uncompiledNets.remove(net);
        }
    }

    /**
     * Returns an enumeration of all nets in the system.
     * The enumeration will refuse to return more elements if the
     * {@code ShadowNetSystem} is modified.
     *
     * @return the {@code ShadowNet} instances of this {@code ShadowNetSystem}
     **/
    public Set<ShadowNet> elements() {
        return Collections.unmodifiableSet(_nets);
    }

    /**
     * Returns an enumeration of all nets in the system which are
     * not marked as compiled yet. The enumeration is independent
     * of future modifications of the {@code ShadowNetSystem}.
     **/
    public Set<ShadowNet> getUncompiledNets() {
        return new HashSet<>(_uncompiledNets);
    }

    /**
     * Returns {@code true}, if there is at least one uncompiled {@link ShadowNet} or {@code false}, otherwise.
     *
     * @return {@code true}, if there is at least one uncompiled {@link ShadowNet} or {@code false}, otherwise.
     **/
    public boolean hasUncompiledNets() {
        return !_uncompiledNets.isEmpty();
    }

    @Serial
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();
        Optional<SerialisationListener<ShadowNetSystem>> listener =
            SerialisationListenerRegistry.getListener(ShadowNetSystem.class);
        if (listener.isPresent()) {
            listener.get().onWrite(this, out);
        }
    }

    /**
     * Deserialization method, behaves like default readObject
     * method and additionally informs the Simulator so that its corresponding compiler can be added to the stream.
     * Reinitializes the transient fields:
     * - All nets are marked as uncompiled.
     * - No net loader is known.
     *
     * @param in the object to be read
     * @throws IOException if the object can't be read
     * @throws ClassNotFoundException if the class can't be found while reading the object
     */
    @Serial
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        _uncompiledNets = new HashSet<>(_nets);
        _compiledNets = new HashSet<>();
        Optional<SerialisationListener<ShadowNetSystem>> listener =
            SerialisationListenerRegistry.getListener(ShadowNetSystem.class);
        if (listener.isPresent()) {
            listener.get().onRead(this, in);
        }
    }
}