package de.renew.engine.simulator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import org.apache.log4j.Logger;

import de.renew.database.TransactionSource;
import de.renew.engine.common.SimulatorEventLogger;
import de.renew.engine.events.TransitionException;
import de.renew.engine.searcher.EarlyExecutable;
import de.renew.engine.searcher.Executable;
import de.renew.engine.searcher.LateExecutable;
import de.renew.engine.searcher.Occurrence;
import de.renew.engine.searcher.OccurrenceDescription;
import de.renew.engine.searcher.Searcher;
import de.renew.engine.searcher.VariableMapperCopier;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.simulatorontology.simulation.StepIdentifier;
import de.renew.unify.Copier;
import de.renew.unify.Impossible;
import de.renew.util.Detacher;
import de.renew.util.LockComparator;
import de.renew.util.Semaphor;

/**
 * Represents an executable transition binding.
 * Offers an execution method for a given sequence of executables,
 * as well as the option of locking and unlocking them after.
 * A {@link Searcher} provides the necessary occurrences and executables.
 */
public class Binding implements Runnable {
    private static final Logger LOGGER = Logger.getLogger(Binding.class);

    // Fields for execution.
    private List<EarlyExecutable> _earlyExecutables = new ArrayList<>();
    private List<LateExecutable> _lateExecutables = new ArrayList<>();

    // Fields for viewing.
    private final OccurrenceDescription[] _occurrenceDescriptions;
    private String _description;

    /**
     * The result variable becomes true, if the transition
     * can be successfully executed.
     */
    private boolean _result = false;

    /**
     * The semaphore is increased after the enabledness has
     * been finally determined.
     */
    private final Semaphor _semaphore = new Semaphor();

    //We got the current stepIdentifier before, but we don't need it
    //as we get it during the execution call anyway
    private StepIdentifier _stepIdentifier = null;


    /**
     * Create a new binding based on the current state of the
     * given searcher, which should have reported a valid
     * binding to the finder.
     *
     * @param searcher the searcher that found the binding.
     */
    public Binding(Searcher searcher) {
        // Get the executables.
        Copier copier = new Copier();
        VariableMapperCopier variableMapperCopier = new VariableMapperCopier(copier);
        Collection<Executable> executables = new ArrayList<>();
        Collection<Occurrence> occurrences = searcher.getOccurrences();
        for (Occurrence occurrence : occurrences) {
            executables.addAll(occurrence.makeExecutables(variableMapperCopier));
        }

        // Sort the executables into early and late.
        for (Executable executable : executables) {
            if (executable instanceof EarlyExecutable earlyExecutable) {
                _earlyExecutables.add(earlyExecutable);
            } else if (executable instanceof LateExecutable lateExecutable) {
                _lateExecutables.add(lateExecutable);
            } else {
                throw new RuntimeException("Unknown type of executable detected: " + executable);
            }
        }

        // Prepare the viewable information.
        _occurrenceDescriptions = occurrences.stream()
            .map(occurrence -> occurrence.makeOccurrenceDescription(variableMapperCopier))
            .toArray(OccurrenceDescription[]::new);

    }

    private String makeText() {
        return Arrays.stream(_occurrenceDescriptions).map(OccurrenceDescription::getDescription)
            .collect(Collectors.joining("\n"));
    }

    /**
     * Returns the description, if it's not empty.
     *
     * @return the description, if it's not empty
     */
    public String getDescription() {
        if (_description == null) {
            _description = makeText();
        }
        return _description;
    }

    /**
     * Execute a given sequence of executables.
     * This is the only valid way of executing a collection of
     * executables. This method must only be called once.
     *
     * @param stepIdentifier identifier for a simulation step, logged by the logger
     * @param asynchronous true, if the executable objects are supposed
     *                     to be run in a different thread and the current
     *                     thread is supposed to be continued as soon as
     *                     any action is done by the transition that
     *                     might block the secondary thread
     * @return {@code true}, if the transition can be successfully executed or {@code false}, otherwise
     */
    public synchronized boolean execute(StepIdentifier stepIdentifier, final boolean asynchronous) {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        // Check for misuse.
        if (_earlyExecutables == null) {
            throw new RuntimeException("Cannot execute binding twice.");
        }

        _stepIdentifier = stepIdentifier;

        // If asynchronous execution is requested, create
        // a different thread to execute the executables,
        // but block the current thread until either the executables
        // are all done or an executable started a long-running task
        // like a method call.
        // 
        // If synchronous execution is requested, just run to completion.
        if (asynchronous) {
            SimulationThreadPool.getCurrent().execute(this);
        } else {
            SimulationThreadPool.getCurrent().executeAndWait(this);
        }

        //Log events after they have happened (previously it was before)
        SimulatorEventLogger.log(stepIdentifier, "-------- Synchronously --------");

        // Wait for the result. (Just to be sure. It should already
        // be accessible.)
        _semaphore.P();
        return _result;
    }

    /**
     * The lock method must be called first. It cannot fail,
     * but it might take a while.
     * Ultimately, this method must be followed by a call to
     * the unlock() method.
     *
     * @see #unlock
     */
    private void lock() {
        for (EarlyExecutable earlyExecutable : _earlyExecutables) {
            earlyExecutable.lock();
        }
    }

    /**
     * Unlock a set of executables.
     */
    private void unlock() {
        for (EarlyExecutable earlyExecutable : _earlyExecutables) {
            earlyExecutable.unlock();
        }
    }

    /**
     * The verify method must be called after the lock method.
     * If it fails, the transition cannot fire. If it succeeds,
     * it must be followed by a call to executeEarly().
     *
     * @see #executeEarly(StepIdentifier)
     *
     * @return true, if verification succeeded
     */
    private boolean verify(StepIdentifier stepIdentifier) {
        List<EarlyExecutable> verified = new ArrayList<>();

        // Verify all early executables.
        try {
            for (EarlyExecutable earlyExecutable : _earlyExecutables) {
                earlyExecutable.verify(stepIdentifier);

                // Ok, I got the verification. Now I must make sure to
                // treat this executable correctly: rollback or execute.
                verified.add(earlyExecutable);
            }
        } catch (Impossible e) {
            // Something failed. Probably a missing token.
            // We make sure to undo all changes to
            // the database (if necessary).
            TransactionSource.rollback();

            // Now the executables can undo their actions.
            // The database must not be informed about these, because
            // it was not informed about the previous actions
            // in the first place.
            Collections.reverse(verified);
            verified.forEach(EarlyExecutable::rollback);

            // I undid all actions (in reverse order, if that matters).
            // Now I have to report failure.
            return false;
        }
        return true;
    }

    /**
     * If all verifications have succeeded, this method can call all early
     * executables in their appropriate order.
     */
    private void executeEarly(StepIdentifier stepIdentifier) {
        for (EarlyExecutable earlyExecutable : _earlyExecutables) {
            earlyExecutable.execute(stepIdentifier);
        }
    }

    /**
     * If all early executables were correctly executed and
     * the locks were released, we can call all late
     * executables in their appropriate order.
     */
    private void executeLate(StepIdentifier stepIdentifier) {
        // The following throwable becomes non-null when
        // some late executable throws and exception.
        // It is then passed to all following executables.
        Throwable firstThrowable = null;
        for (LateExecutable lateExecutable : _lateExecutables) {
            if (lateExecutable.isLong()) {
                Detacher.detach();
            }
            try {
                if (firstThrowable == null) {
                    lateExecutable.execute(stepIdentifier);
                } else {
                    lateExecutable.executeAfterException(stepIdentifier, firstThrowable);
                }
            } catch (ThreadDeath death) {
                // We are not supposed to stop a thread death.
                throw death;
            } catch (Throwable t) {
                if (firstThrowable == null) {
                    firstThrowable = t;
                }
                SimulatorEventLogger.log(stepIdentifier, new TransitionException(t));
                LOGGER.error(t.getMessage(), t);
            }
        }
    }

    /**
     * This method must not be called directly.
     * It has to be public in order to satisfy the
     * interface Runnable. Use the {@link #execute}-method
     * instead.
     */
    @Override
    public void run() {
        assert SimulationThreadPool.isSimulationThread() : "is not in a simulation thread";
        // Keep a note that this object is now executing.
        BindingList.register(this);

        // Wrap operations in a transaction, if that is
        // required.
        TransactionSource.start();

        // Lock in the required order.
        _earlyExecutables.sort(new LockComparator());
        lock();
        try {
            // The order of execution is
            // usually different from the order of locking.
            _earlyExecutables.sort(new PhaseComparator());

            // Check all early executables. Only those can fail.
            _result = verify(_stepIdentifier);

            // Execute the executables only if the verification succeeded.
            if (_result) {
                // The transition was still activated.
                executeEarly(_stepIdentifier);
            }
        } finally {
            // We must make sure to undo every lock even if
            // the execution of some operations failed.
            // The order of the unlock operations does not matter.
            unlock();
        }

        // We can already report success or failure to the calling
        // thread.
        _semaphore.V();

        // Execute the remaining executables only if the verification succeeded.
        if (_result) {
            // The remaining steps can now be performed without further problems.
            _lateExecutables.sort(new PhaseComparator());
            executeLate(_stepIdentifier);
            try {
                TransactionSource.commit();
            } catch (Exception e) {
                LOGGER.error(e.getMessage(), e);
            }
        }

        // Keep a note that this object is no longer executing.
        BindingList.remove(this);

        // Let's make life easier for the garbage collector.
        _earlyExecutables = null;
        _lateExecutables = null;
    }
}