package meerkat.voting.output;

import com.google.common.util.concurrent.FutureCallback;
import meerkat.protobuf.Voting.BallotSecrets;
import meerkat.protobuf.Voting.PlaintextBallot;
import meerkat.protobuf.Voting.SignedEncryptedBallot;
import meerkat.voting.controller.callbacks.ControllerCallback;
import meerkat.voting.controller.callbacks.OutputDeviceCommitCallback;
import meerkat.voting.controller.callbacks.OutputDeviceFinalizeCallback;
import meerkat.voting.output.outputcommands.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;

/**
 * This is a base class for simple OutputDevices which run asynchronously (as a separate thread).
 * The methods of the BallotOutputDevice simply register a matching OutputCommand in the instance's queue
 * The Runnable.run method simply takes the next registered command and calls the matching (abstract) method
 */
public abstract class AsyncRunnableOutputDevice implements BallotOutputDevice, Runnable  {

    private Logger logger;
    private ArrayBlockingQueue<OutputCommand> queue;
    private volatile boolean shutDownHasBeenCalled;

    public AsyncRunnableOutputDevice() {
        logger = LoggerFactory.getLogger(AsyncRunnableOutputDevice.class);
        logger.info("AsyncRunnableOutputDevice is constructed");
        queue = new ArrayBlockingQueue<>(1);
        shutDownHasBeenCalled = false;
    }

    @Override
    public void run () {
        logger.info("starts running");
        while (! wasShutDownCalled()) {
            try {
                OutputCommand command = queue.take();
                handleSingleCommand(command);
            }
            catch (InterruptedException e) {
                logger.warn("Interrupted while reading from command queue " + e);
            }
        }
    }

    private boolean wasShutDownCalled () {
        return shutDownHasBeenCalled;
    }

    @Override
    public void callShutDown() {
        logger.info("callShutDown command has been called");
        shutDownHasBeenCalled = true;
        queue.clear();
    }

    /**
     * chooses the next method to run according to the type of the given OutputCommand
     * @param command any valid OutputCommand
     */
    private void handleSingleCommand(OutputCommand command) {
        if (command instanceof CommitOutputCommand) {
            doCommitToBallot((CommitOutputCommand)command);
        }
        else if (command instanceof AuditOutputCommand) {
            doAudit((AuditOutputCommand)command);
        }
        else if (command instanceof CastOutputCommand) {
            doCastBallot((CastOutputCommand)command);
        }
        else if (command instanceof CancelOutputCommand) {
            doCancel((CancelOutputCommand)command);
        }
        else {
            String errorMessage = "handleSingleCommand: unknown type of OutputCommand received: " +
                    command.getClass().getName();
            logger.error(errorMessage);
            throw new RuntimeException(errorMessage);
        }
    }


    @Override
    public void commitToBallot(PlaintextBallot plaintextBallot,
                               SignedEncryptedBallot signedEncryptedBallot,
                               ControllerCallback<Void> callback) {
        logger.debug("Output interface call to commit to ballot");
        queue.add(new CommitOutputCommand(plaintextBallot, signedEncryptedBallot, callback));
    }

    @Override
    public void audit(BallotSecrets ballotSecrets, ControllerCallback<Void> callback) {
        logger.debug("an interface call to audit");
        queue.add(new AuditOutputCommand(ballotSecrets, callback));
    }

    @Override
    public void castBallot(ControllerCallback<Void> callback) {
        logger.debug("an interface call to cast ballot");
        queue.add(new CastOutputCommand(callback));
    }

    @Override
    public void cancelBallot(ControllerCallback<Void> callback) {
        logger.debug("an interface call to cancel the output");
        queue.add(new CancelOutputCommand(callback));
    }


    /**
     * This method should be filled by an extending class. It should have the details of how to commit to a ballot
     * @param command a CommitOutputCommand with the details and the callback
     */
    public abstract void doCommitToBallot(CommitOutputCommand command);

    /**
     * This method should be filled by an extending class. It should have the details of how to audit the ballot
     * @param command a AuditOutputCommand with the details and the callback
     */
    public abstract void doAudit(AuditOutputCommand command);

    /**
     * This method should be filled by an extending class. It should have the details of how to cast the ballot
     * @param command a CastOutputCommand with the details and the callback
     */
    public abstract void doCastBallot(CastOutputCommand command);

    /**
     * This method should be filled by an extending class. It should have the details of how to cancel the ballot output
     * @param command a CancelOutputCommand with the details and the callback
     */
    public abstract void doCancel(CancelOutputCommand command);

}
