package meerkat.voting.gui.uiFX;

import com.google.common.util.concurrent.FutureCallback;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.Text;
import meerkat.protobuf.Voting;
import meerkat.protobuf.Voting.BallotAnswer;
import meerkat.protobuf.Voting.BallotQuestion;
import meerkat.protobuf.Voting.UIElement;
import meerkat.protobuf.Voting.UIElementDataType;
import meerkat.voting.controller.callbacks.*;
import meerkat.voting.gui.controllersFX.VistaNavigator;
import meerkat.voting.gui.ui.CommandPend;
import meerkat.voting.gui.ui.TickerTimerTask;
import meerkat.voting.gui.ui.UIUtils;
import meerkat.voting.gui.ui.VotingBoothUI;
import meerkat.voting.gui.ui.uicommands.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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


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

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

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

    private volatile boolean shutDownHasBeenCalled;

    private MainFX ui;


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

        logger.info("A graphical UI console is constructed");
        cmdPend = new CommandPend<>();
        VistaNavigator.setUIThread(this);

        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");
        //            Application.launch(MainFX.class);
        new Thread() {
            @Override
            public void run() {
                Application.launch(MainFX.class);
            }
        }.start();
        try {
            MainFX.waitUntilLaunched();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        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();
        try {
            ui.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 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");
        VistaNavigator.setCurrentCommand(command);
        VistaNavigator.loadVista(VistaNavigator.WELCOME_SCREEN);
    }

    public void answerWelcomeScreen(StartSessionUICommand command) {
        ControllerCallback<Void> callback = command.getCallback();
        callback.onSuccess(null);
    }

    public void answerChannelChoiceScreen(ChannelChoiceUICommand command, List<Voting.BallotAnswer> answers) {
        command.getCallback().onSuccess(answers);
    }


    public void answerVotingScreen(RaceVotingUICommand command, List<Voting.BallotAnswer> answers) {
        command.getCallback().onSuccess(answers);
    }

    public void answerCastOrAuditScreen(CastOrAuditUICommand command, VotingBoothUI.FinalizeBallotChoice fChoice) {
        ControllerCallback callback = command.getCallback();
        assert (callback instanceof CastOrAuditCallback);
        ((CastOrAuditCallback)callback).onSuccess(fChoice);
    }

    /**
     * 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");
        UIUtils.assertQuestionsAreValid(command.getQuestions(), logger);
        VistaNavigator.setCurrentCommand(command);
        VistaNavigator.loadVista(VistaNavigator.CHANNEL_CHOICE);
    }

    /**
     * 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");
        UIUtils.assertQuestionsAreValid(command.getQuestions(), logger);
        VistaNavigator.setCurrentCommand(command);
        VistaNavigator.loadVista(VistaNavigator.VOTING);
    }

    /**
     * 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");
        VistaNavigator.setCurrentCommand(command);
        VistaNavigator.loadVista(VistaNavigator.CAST_OR_AUDIT);
    }


    /**
     * 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());
        }
        VistaNavigator.loadLoadingVista(messageString);
    }

    /**
     * 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));
    }

    // TODO: make javafx interface for handling fatal error
    /**
     * 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()));
        }
    }

    /**
     * present a question in a JavaFX environment to the voter
     * @param question a text ballot question
     */
    public VBox showQuestionInFX(BallotQuestion question, ToggleGroup answerGroup) {
        if (!UIUtils.isQuestionOnlyText(question)) {
            System.err.println("debug: an element in question is not of TEXT type");
            throw new UnsupportedOperationException();
        }

        String questionText = UIUtils.bytesToString(question.getQuestion().getData());
        String description = UIUtils.bytesToString(question.getDescription().getData());

        VBox vbox = new VBox();
        vbox.setPadding(new Insets(10));
        vbox.setSpacing(8);

        Text title = new Text(questionText);
        vbox.getChildren().add(title);

        Text descr = new Text(description);
        descr.setFont(Font.font(Font.getDefault().getFamily(), FontPosture.ITALIC, Font.getDefault().getSize()));
        vbox.getChildren().add(descr);

        int answerIndex = 0;
        List<RadioButton> options = new ArrayList<RadioButton>();
        for (UIElement answer : question.getAnswerList()) {
            ++answerIndex;
            RadioButton rb1 = new RadioButton(UIUtils.bytesToString(answer.getData()));
            rb1.setUserData(answerIndex);
            rb1.setToggleGroup(answerGroup);
            VBox.setMargin(rb1, new Insets(0, 0, 0, 8));
            vbox.getChildren().add(rb1);
        }

        return vbox;
    }


    private boolean wasShutDownCalled () {
        return shutDownHasBeenCalled;
    }

    public String testPrint() {
        return "testPrint called";
    }

    public void setGraphicalFx(MainFX mainFX) {
        this.ui = mainFX;
    }

}
