package de.renew.application;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.StreamCorruptedException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.apache.log4j.Logger;

import de.renew.api.ISimulationStateLoader;
import de.renew.engine.searchqueue.SearchQueue;
import de.renew.engine.thread.SimulationLockExecutorProvider;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.IDRegistry;
import de.renew.net.INetLookup;
import de.renew.net.NetInstance;
import de.renew.net.NetLookup;
import de.renew.net.serialisation.NetDeserializer;
import de.renew.plugin.PropertyHelper;
import de.renew.simulator.api.ISimulationLockExecutor;
import de.renew.simulator.api.ISimulationManager;
import de.renew.simulator.api.SimulatorPropertyConstants;
import de.renew.simulatorontology.simulation.SimulationRunningException;
import de.renew.util.RenewObjectInputStream;

/**
 * Implementation of a {@code SimulationStateLoader}.
 */
class SimulationStateLoader implements ISimulationStateLoader {
    private static final Logger LOGGER = Logger.getLogger(SimulationStateLoader.class);
    private final ISimulationManager _simulationManager;
    private static final ISimulationLockExecutor LOCK_EXECUTOR =
        SimulationLockExecutorProvider.provider();

    SimulationStateLoader(ISimulationManager simulationManager) {
        _simulationManager = simulationManager;
    }

    @Override
    public NetInstance[] loadState(final ObjectInput input, final Properties props)
        throws IOException, ClassNotFoundException, SimulationRunningException
    {
        INetLookup netLookup = new NetLookup(LOCK_EXECUTOR);

        LOCK_EXECUTOR.lock();
        try {
            Future<NetInstance[]> future = SimulationThreadPool.getNew().submitAndWait(() -> {
                List<NetInstance> explicitInstances;
                try {
                    LOGGER.debug("Loading simulation state...");

                    _simulationManager.setupSimulation(props);

                    // Check for valid header, which includes:
                    // - label
                    // - file format version number
                    // - type of simulator used
                    String streamLabel = (String) input.readObject();

                    if (!streamLabel.equals(SimulatorPlugin.STATE_STREAM_LABEL)) {
                        throw new StreamCorruptedException(
                            "Stream does not seem to contain renew state data.");
                    }
                    int streamVersion = input.readInt();

                    // that's most probably not ok.
                    if (streamVersion != SimulatorPlugin.STATE_STREAM_VERSION) {
                        throw new StreamCorruptedException(
                            "State data is of different version " + "(" + streamVersion
                                + ") than the current version ("
                                + SimulatorPlugin.STATE_STREAM_VERSION + ").");
                    }

                    int simulatorMultiplicity = PropertyHelper.getIntProperty(
                        _simulationManager.getSimulationProperties(),
                        SimulatorPropertyConstants.MULTIPLICITY_PROP_NAME, 1);
                    int streamSimulatorMultiplicity = input.readInt();

                    if (streamSimulatorMultiplicity != simulatorMultiplicity) {
                        LOGGER.warn(
                            "Simulation state was saved "
                                + "using a different simulator multiplicity " + "("
                                + streamSimulatorMultiplicity + ") " + "than currently selected ("
                                + simulatorMultiplicity + ").");
                    }

                    // First part: read all NetInstances stored
                    // explicitly.
                    int count = input.readInt();
                    explicitInstances = new ArrayList<>(count);
                    try {
                        for (int i = 0; i < count; i++) {
                            explicitInstances
                                .add((NetInstance) de.renew.util.ClassSource.readObject(input));
                        }
                    } catch (ClassCastException e) {
                        LOGGER.debug(e.getMessage(), e);
                        throw new StreamCorruptedException(
                            "Object other than NetInstance found "
                                + "when looking for net instances: " + e.getMessage());
                    }

                    // If a RenewObjectInputStream is used, read
                    // all delayed fields NOW.
                    if (input instanceof RenewObjectInputStream) {
                        ((RenewObjectInputStream) input).readDelayedObjects();
                    }

                    // Second part: read all compiled Nets
                    new NetDeserializer(netLookup).loadNets(input);

                    // Third part: add all necessary entries to the
                    // SearchQueue.
                    SearchQueue.loadQueue(input);

                    // Last part: reestablish the global ID registry
                    // for
                    // token IDs.
                    IDRegistry.load(input);
                } catch (IOException e) {
                    // If an exception occurs, pass it to the
                    // caller.
                    // But restore a known state first - the 'no
                    // simulation active' state.
                    _simulationManager.terminateSimulation();
                    throw e;
                } catch (ClassNotFoundException | StackOverflowError e) {
                    // The same as above, but for another exception
                    // type...
                    _simulationManager.terminateSimulation();
                    throw e;
                }

                // Return the list of read NetInstances.
                return explicitInstances.toArray(new NetInstance[0]);
            });
            return future.get();
        } catch (InterruptedException e) {
            LOGGER.error("Timeout while waiting for simulation thread to finish", e);
        } catch (ExecutionException e) {
            Throwable t = e.getCause();
            if (t instanceof SimulationRunningException) {
                throw new SimulationRunningException(e);
            } else if (t instanceof IOException exc) {
                throw exc;
            } else if (t instanceof ClassNotFoundException) {
                throw new ClassNotFoundException(t.getMessage(), e);
            } else if (t instanceof RuntimeException exc) {
                throw exc;
            } else if (t instanceof Error exc) {
                throw exc;
            } else {
                LOGGER.error("Simulation thread threw an exception", e);
            }
        } finally {
            LOCK_EXECUTOR.unlock();
        }

        // We should never return nothing but some error occurred before.
        return null;
    }
}
