package meerkat.voting.gui.ui;

import com.google.common.util.concurrent.FutureCallback;
import meerkat.protobuf.Voting.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

import meerkat.voting.controller.callbacks.*;
import meerkat.voting.gui.ui.uicommands.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static java.lang.System.in;


/**
 * an asynchronous thread implementation of the VotingBoothUI interface
 * This is a mock-up implementation using just the console as our UI device
 */
public class SystemConsoleUI implements VotingBoothUI, Runnable {

    private static final Logger logger = LoggerFactory.getLogger(SystemConsoleUI.class);

    private BufferedReader bufferedReader;
    private CommandPend<UICommand> cmdPend;
    private Date startWaitingTime;

    private volatile boolean shutDownHasBeenCalled;


    public SystemConsoleUI() {
        final int tickDurationInMillisec = 10; // period between view update calls

        logger.info("A VB UI console is constructed");
        cmdPend = new CommandPend<>();
        bufferedReader = new BufferedReader(new InputStreamReader(in));

        startWaitingTime = null;
        Timer timer = new Timer();
        timer.scheduleAtFixedRate(new TickerTimerTask(cmdPend), new Date(), tickDurationInMillisec);

        shutDownHasBeenCalled = false;
    }


    /**
     * the run() method. Simply loops and takes the UI's pending command and handles it accordingly
     */
    @Override
    public void run () {
        logger.info("UI starts running");
        while (! wasShutDownCalled()) {
            try {
                UICommand command = cmdPend.take();
                handleSingleCommand(command);
            }
            catch (InterruptedException e) {
                logger.warn ("Interrupted while reading the pending command " + e);
            }
        }
    }


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

    /**
     * chooses the next method to run according to the type of the given UICommand.
     * Special case for the TickCommand.
     * As this command is registered in the queue constantly, we simply ignore this command if the UI is not in
     * a waiting state
     * @param command any valid UICommand
     */
    private void handleSingleCommand(UICommand command) {
        if (!(command instanceof TickCommand)) {
            if (startWaitingTime != null) {
                stopWaiting();
            }
        }

        if (command instanceof StartSessionUICommand) {
            doShowWelcomeScreen((StartSessionUICommand)command);
        }
        else if (command instanceof ChannelChoiceUICommand) {
            doAskChannelChoiceQuestions((ChannelChoiceUICommand)command);
        }
        else if (command instanceof RaceVotingUICommand) {
            doAskVotingQuestions((RaceVotingUICommand)command);
        }
        else if (command instanceof CastOrAuditUICommand) {
            doCastOrAudit ((CastOrAuditUICommand)command);
        }
        else if (command instanceof FatalErrorUICommand) {
            doFatalError((FatalErrorUICommand)command);
        }
        else if (command instanceof WaitForFinishUICommand) {
            doWaitForFinish((WaitForFinishUICommand)command);
        }
        else if (command instanceof TickCommand) {
            doTick ();
        }
        else {
            String errorMessage = "handleSingleCommand: unknown type of UICommand received: " +
                    command.getClass().getName();
            logger.error(errorMessage);
            throw new RuntimeException(errorMessage);
        }
    }


    /**
     * start a new session by registering a StartSessionUICommand
      * @param callback - a boolean future callback to return when done
     */
    @Override
    public void startNewVoterSession(FutureCallback<Void> callback) {
        logger.debug("UI interface call to startNewVoterSession");
        cmdPend.trample(new StartSessionUICommand((NewVoterCallback)callback));
    }

    /**
     * welcomes the new voter at the beginning of the session
     * @param command a StartSessionUICommand with a acallback
     */
    private void doShowWelcomeScreen(StartSessionUICommand command) {
        logger.debug("UI entered doShowWelcomeScreen");
        System.out.println("Welcome, new voter!");
        waitForEnter(null);
        ControllerCallback<Void> callback = command.getCallback();
        callback.onSuccess(null);
    }

    /**
     * marks that the waiting, for something else to have happened, is finished
     */
    private void stopWaiting () {
        System.out.println ();
        startWaitingTime = null;

    }

    /**
     * waits until the ENTER key is pressed in the console
     * @param message a message to show the user in the console
     */
    private void waitForEnter(String message) {
        if (message != null) {
            System.out.println(message);
        }
        System.out.println("\nPress ENTER to proceed.\n");
        String t = null;
        while (t == null) {
            try {
                t = readInputLine();
            }
            catch (IOException e) {
                String errorMessage = "waitForEnter: threw an IOException: " + e;
                logger.error(errorMessage);
                System.err.println(errorMessage);
            }
        }
    }

    /**
     * call for the channel choice phase by registering a ChannelChoiceUICommand in the queue
     * @param questions questions to determine the right voting channel for this voter
     * @param callback that's where we store the answers to decide channel upon for the current voter
     */
    @Override
    public void chooseChannel(List<BallotQuestion> questions, FutureCallback<List<BallotAnswer>> callback) {
        logger.debug("UI interface call to chooseChannel");
        cmdPend.trample(new ChannelChoiceUICommand(questions, (ChannelChoiceCallback)callback));
    }

    /**
     * lists the channel choice questions to the voter and gathers the voter's answers
     * @param command a ChannelChoiceUICommand with the data and a callback
     */
    private void doAskChannelChoiceQuestions (ChannelChoiceUICommand command) {
        logger.debug("UI: doAskChannelChoiceQuestions");
        System.out.println("Showing questions for choosing channel:\n");
        try {
            List<BallotAnswer> answers = askVoterForAnswers(command.getQuestions());
            command.getCallback().onSuccess(answers);
        }
        catch (VoterCancelThrowable e) {
            command.getCallback().onFailure(e);
        }
        catch (IOException e) {
            String errorMessage = "Channel choice failed due to IOException: " + e;
            logger.error (errorMessage);
            System.err.println(errorMessage);
            command.getCallback().onFailure(e);
        }
    }

    /**
     * call for the race voting question phase by registering a RaceVotingUICommand in the queue
     * @param questions all ballot questions to present to the voter
     * @param callback the responses to the questions collected by the UI, to send back to the controller. Responses are null if voter chose to cancel session
     */
    @Override
    public void askVoterQuestions(List<BallotQuestion> questions, FutureCallback<List<BallotAnswer>> callback) {
        logger.debug("UI interface call to chooseChannel");
        cmdPend.trample(new RaceVotingUICommand(questions, (VotingCallback)callback));
    }

    /**
     * lists the race voting questions to the voter and gathers the voter's answers
     * @param command a RaceVotingUICommand with a callback
     */
    private void doAskVotingQuestions (RaceVotingUICommand command) {
        logger.debug("UI: doAskVotingQuestions");
        System.out.println("Showing questions for race voting:\n");
        try {
            List<BallotAnswer> answers = askVoterForAnswers(command.getQuestions());
            command.getCallback().onSuccess(answers);
        }
        catch (VoterCancelThrowable e) {
            command.getCallback().onFailure(e);
        }
        catch (IOException e) {
            String errorMessage = "Asking voting questions failed due to IOException: " + e;
            logger.error (errorMessage);
            System.err.println(errorMessage);
            command.getCallback().onFailure(e);
        }
    }

    /**
     * call for the cast-or-audit phase by registering a CastOrAuditUICommand in the queue
     * @param callback the returned choice of how to finalize the ballot
     */
    @Override
    public void castOrAudit(FutureCallback<FinalizeBallotChoice> callback) {
        logger.debug("UI interface call to castOrAudit");
        cmdPend.trample(new CastOrAuditUICommand((CastOrAuditCallback)callback));
    }

    /**
     * asks the voter whether to cast or audit the ballot
     * @param command a simple CastOrAuditUICommand with the callback
     */
    private void doCastOrAudit(CastOrAuditUICommand command) {
        logger.debug("UI entered doCastOrAudit");
        System.out.println ("Finalizing your vote. Do you wish to (C)ast or (A)udit?");

        FinalizeBallotChoice fChoice;

        try {
            String s = readInputLine();
            if (s.equals("cast") || s.equals("c")) {
                fChoice = FinalizeBallotChoice.CAST;
            }
            else if (s.equals("audit") || s.equals("a")) {
                fChoice = FinalizeBallotChoice.AUDIT;
            }
            else {
                throw new IllegalArgumentException("UI could not understand the answer for cast/audit question '" + s + "'");
            }
            ControllerCallback callback = command.getCallback();
            assert (callback instanceof CastOrAuditCallback);
            ((CastOrAuditCallback)callback).onSuccess(fChoice);
        }
        catch (IllegalArgumentException|IOException e) {
            String errorMessage = "doCastOrAudit: some error with reading input from console. details: " + e;
            logger.error(errorMessage);
            command.getCallback().onFailure(e);
        }
    }


    /**
     * makes the UI (and voter) wait for something else to happen, by registering a WaitForFinishUICommand in the queue
     * @param message a message to show the user on the UI device while waiting
     * @param callback a success return value of the wait (cancelling returns false)
     */
    @Override
    public void notifyVoterToWaitForFinish(UIElement message, FutureCallback<Void> callback) {
        logger.debug("UI interface call to notifyVoterToWaitForFinish");
        cmdPend.trample(new WaitForFinishUICommand(message, (WaitForFinishCallback)callback));
    }

    /**
     * Tells the voter (in the console) to wait until some other process is finished
     * @param command a simple WaitForFinishUICommand with the callback
     */
    public void doWaitForFinish (WaitForFinishUICommand command) {
        logger.debug("UI entered doWaitForFinish");

        startWaitingTime = new Date();

        UIElement message = command.getMessage();
        String messageString;
        if (message.getType() != UIElementDataType.TEXT) {
            messageString = "Default message: encountered an error. System halting";
        } else {
            messageString = UIUtils.bytesToString(message.getData());
        }
        System.out.println(messageString);
        System.out.print ("Waiting : .");
    }

    /**
     * show an error to the voter. Halts the system until a technician handles it
     * @param errorMessage message to show in UI device
     * @param callback returns interrupt
     */
    @Override
    public void showErrorMessageAndHalt(UIElement errorMessage, FutureCallback<Boolean> callback) {
        logger.debug("UI interface call to showErrorMessageAndHalt");
        throw new UnsupportedOperationException("Not implemented becuase currently not sure if we ever use it.");
    }

    /**
     * show an error to the voter. let him press a (chosen) button for handling the error.
     * @param errorMessage message to show in UI device
     * @param buttonLabels labels for buttons to present to voter
     * @param callback the number of the selected button
     */
    @Override
    public void showErrorMessageWithButtons(UIElement errorMessage, UIElement[] buttonLabels, FutureCallback<Integer> callback) {
        logger.debug("UI interface call to showErrorMessageWithButtons");
        cmdPend.trample(new FatalErrorUICommand(errorMessage, buttonLabels, (ControllerCallback<Integer>)callback));
    }

    /**
     * show an error to the voter. let him press a (chosen) button for handling the error.
     * @param command a FatalErrorUICommand with the callback
     */
    private void doFatalError (FatalErrorUICommand command) {
        logger.debug("UI entered doFatalError");

        UIElement errorMessage = command.getErrorMessage();
        String errorMessageString;
        if (errorMessage.getType() != UIElementDataType.TEXT) {
            errorMessageString = "Default message: encountered an error. System halting";
        } else {
            errorMessageString = UIUtils.bytesToString(errorMessage.getData());
        }

        UIElement[] buttonLabels = command.getButtonLabels();
        String[] buttonLabelStrings = new String[buttonLabels.length];
        for (int i = 0; i < buttonLabels.length; ++i) {
            if (buttonLabels[i].getType() != UIElementDataType.TEXT) {
                buttonLabelStrings[i] = "<NON_TEXT>";
            } else {
                buttonLabelStrings[i] = UIUtils.bytesToString(errorMessage.getData());
            }
        }

        System.out.println(errorMessageString);
        for (int i = 0; i < buttonLabelStrings.length; ++i) {
            System.out.println("" + i + " - " + buttonLabelStrings[i]);
        }

        try {
            String s = readInputLine();
            Integer chosenButton = new Integer(s);
            command.getCallback().onSuccess(chosenButton);
        }
        catch (IOException e) {
            String err = "doFatalError: some error with reading input from console. details: " + e;
            logger.error(err);
            command.getCallback().onFailure(e);
        }

    }


    /**
     * this method is run when a TickCommand was received while in waiting state
     */
    private void doTick () {
        if (startWaitingTime != null) {
            System.out.print (".");  // still waiting
        }
    }

    /**
     * get an input line from the console
     * @return a line from the voter
     * @throws IOException
     */
    private String readInputLine() throws IOException{
        String s;
        try {
            s = this.bufferedReader.readLine();
            if (null == s) {
                throw new IOException();
            }
        } catch (IOException e) {
            String errorMessage = "readInputLine: some error with reading input from console. details: " + e;
            logger.error(errorMessage);
            throw new IOException(e);
        }
        return s;
    }

    /**
     * present the questions to the voter console sequentially.
     * Voter may choose at any time to skip a question, go back or even cancel the whole session
     * @param questions list of questions to present
     * @return list of answers to the questions (at the same order)
     * @throws VoterCancelThrowable this is thrown if a voter chose to cancel in the middle of the process
     * @throws IOException
     */
    private List<BallotAnswer> askVoterForAnswers(List<BallotQuestion> questions) throws VoterCancelThrowable, IOException {

        UIUtils.assertQuestionsAreValid (questions, logger);

        List<BallotAnswer> answers = new ArrayList<>();
        int index = 0;
        while (index < questions.size()) {
            BallotQuestion question = questions.get(index);
            System.out.println("Question number " + index);
            showQuestionInConsole(question);

            System.out.println("UI screen: Enter your answer. You can also type '(b)ack' or '(c)ancel' or '(s)kip");
            String s = readInputLine();

            if ((s.equals("cancel") || s.equals("c")) || (index == 0 && (s.equals("back") || s.equals("b")))) {
                throw new VoterCancelThrowable();
            }
            else if (s.equals("back") || s.equals("b")) {
                --index;
                answers.remove(index);
            }
            else if (s.equals("skip") || s.equals("s")) {
                answers.add(UIUtils.translateStringAnswerToProtoBufMessageAnswer(""));
                ++index;
            }
            else {
                answers.add(UIUtils.translateStringAnswerToProtoBufMessageAnswer(s));
                ++index;
            }
        }
        return answers;
    }

    /**
     * present a question in the console to the voter
     * @param question a text ballot question
     */
    private void showQuestionInConsole(BallotQuestion question) {
        if (!UIUtils.isQuestionOnlyText(question)) {
            System.err.println("debug: an element in question is not of TEXT type");
            throw new UnsupportedOperationException();
        }

        System.out.println("Question text: " + UIUtils.bytesToString(question.getQuestion().getData()));
        System.out.println("Description: " + UIUtils.bytesToString(question.getDescription().getData()));
        int answerIndex = 0;
        for (UIElement answer : question.getAnswerList()) {
            ++answerIndex;
            System.out.println("Answer " + answerIndex + ": " + UIUtils.bytesToString(answer.getData()));
        }
    }

    private boolean wasShutDownCalled () {
        return shutDownHasBeenCalled;
    }

}
