package de.renew.call;

import de.renew.engine.searcher.Binder;
import de.renew.engine.searcher.ChannelBinder;
import de.renew.engine.searcher.ChannelTarget;
import de.renew.engine.searcher.Occurrence;
import de.renew.engine.searcher.Searchable;
import de.renew.engine.searcher.Searcher;
import de.renew.engine.searcher.TriggerCollection;
import de.renew.engine.searcher.TriggerCollectionImpl;
import de.renew.engine.searcher.Triggerable;
import de.renew.engine.searchqueue.SearchQueue;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.unify.IStateRecorder;
import de.renew.unify.Impossible;
import de.renew.unify.Tuple;
import de.renew.unify.Variable;
import de.renew.util.Lock;
import de.renew.util.Semaphor;

/**
 * The {@code SynchronisationRequest} class represents a request to perform a synchronisation
 * operation over a specified channel using given parameters.
 */
public class SynchronisationRequest implements Searchable, Triggerable {
    private ChannelTarget _channelTarget;
    private String _channelName;
    Tuple _parameters;
    Tuple _resultParameters;
    Semaphor _resultSemaphor;
    boolean _completed;
    final long _lockOrder;
    final Lock _lock;
    private TriggerCollection _triggers = new TriggerCollectionImpl(this);

    /**
     * Constructs a new {@code SynchronisationRequest} object and initializes its parameters.
     *
     * @param channelTarget the {@link ChannelTarget} to use.
     * @param channelName the name of the channel.
     * @param parameters the {@link Tuple} to use.
     */
    public SynchronisationRequest(
        ChannelTarget channelTarget, String channelName, Tuple parameters)
    {
        this._channelTarget = channelTarget;
        this._channelName = channelName;

        this._parameters = parameters;
        _resultParameters = null;
        _resultSemaphor = new Semaphor();

        _completed = false;
        _lockOrder = de.renew.util.Orderer.getTicket();
        _lock = new Lock();
    }

    /**
     * This is a convenience wrapper that allows the most simple call to issue a synchronisation request.
     *
     * @param channelTarget the {@link ChannelTarget} to use.
     * @param channelName the name of the channel.
     * @param parameters the {@link Tuple} to use.
     * @return the {@link Tuple}.
     */
    public static Tuple synchronize(
        ChannelTarget channelTarget, String channelName, Tuple parameters)
    {
        SynchronisationRequest request =
            new SynchronisationRequest(channelTarget, channelName, parameters);
        request.proposeSearch();
        return request.getResult();
    }

    @Override
    public TriggerCollection triggers() {
        return _triggers;
    }

    @Override
    public void proposeSearch() {
        SearchQueue.includeNow(this);
    }

    /**
     * Retrieves the result of the synchronisation request.
     * This method waits until the result is available.
     *
     * @return the {@link Tuple} containing the result parameters of the synchronisation request.
     */
    public Tuple getResult() {
        _resultSemaphor.P();
        return _resultParameters;
    }

    // I assume that I won't be searched concurrently multiple times.
    // Is that safe?
    @Override
    public synchronized void startSearch(final Searcher searcher) {
        final SynchronisationRequest object = this;
        SimulationThreadPool.getCurrent().executeAndWait(new Runnable() {
            @Override
            public void run() {
                // Has my synchronisation been done already?
                if (_completed) {
                    // Yes, no need to keep me in the search queue.
                    return;
                }

                IStateRecorder stateRecorder = searcher.getStateRecorder();

                // Require completeness of the parameters.
                Variable paramVariable = new Variable(_parameters, stateRecorder);
                try {
                    searcher.getCalculationChecker().addLateVariable(paramVariable, stateRecorder);
                } catch (Impossible e) { //NOTICEthrows
                    throw new RuntimeException(
                        "Calculation checker refused to make " + "fresh variable late.");
                }


                // We want to find a transition within the specified
                // channel target that can fire.
                Variable targetVariable = new Variable(_channelTarget, stateRecorder);


                // Create a binder that tries to find an appropriate channel.
                // This synchronisation is not optional.
                Binder initialBinder =
                    new ChannelBinder(targetVariable, _channelName, paramVariable, false);
                searcher.addBinder(initialBinder);


                // Create an occurrence that will inhibit multiple executions
                // of this request and that will notify me upon execution.
                Occurrence occurrence = new SynchronisationOccurrence(object);
                searcher.addOccurrence(occurrence);


                // Start the search.
                searcher.search();


                // Undo changes to searcher state.
                searcher.removeOccurrence(occurrence);
                searcher.removeBinder(initialBinder);
                stateRecorder.restore();
            }
        });
    }
}
