package meerkat.voting.controller.selector;

import meerkat.protobuf.Voting.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.lang.Math;

/**
 * A simple implementation of a QuestionSelector.
 * This implementation simply regards every single answer in the channel choice phase as an identifier of a category
 * Every category is an array of ballot race questions.
 * Data of categories is initialized and stored by a SimpleCategoriesSelectionData protobuf.
 * After receiving the answers from a channel choice phase, this class simply gathers all the categories
 * chosen and compiles the list of ballot questions to include in the ballot for this voter (a question
 * is included in the ballot if its index appears in any chosen category, or in the default category shared by all voters)
 */
public class SimpleListCategoriesSelector implements QuestionSelector {
    protected final static Logger logger = LoggerFactory.getLogger(SimpleListCategoriesSelector.class);

    // all the possible race questions
    private final BallotQuestion[] allBallotQuestions;

    // this category is presented to any voter (regardless of his answers to the channel choice questions)
    private final int[] sharedDefaults;

    // all the categories.
    // first index is the channel choice question number
    // second index is a possible answer to this question
    // categoryChoosers[questionNumber][answerNumber] is an array of indices (to the ballotQuestions array).
    // This category of questions is included in the ballot if voter answered this specific answer to this channel choice question
    private final int[][][] categoryChoosers;


    private final static byte QUESTION_SELECTED = (byte)1;
    private final static byte QUESTION_NOT_SELECTED = (byte)0;


    /**
     * A very straight-forward constructor for the SimpleListCategoriesSelector
     * @param allBallotQuestions all possible race questions for this election
     * @param data a protobuf containing all the index categories
     */
    public SimpleListCategoriesSelector(List<BallotQuestion> allBallotQuestions, SimpleCategoriesSelectionData data) {
        // copies the ballot race question list into a member array
        this.allBallotQuestions = new BallotQuestion[allBallotQuestions.size()];
        allBallotQuestions.toArray(this.allBallotQuestions);

        // copies the shared category list (as appears in the protobuf data) into a member array
        sharedDefaults = listToIntArray(data.getSharedDefaults().getQuestionIndexList());

        // copies the category lists (as appear in the protobuf data) into a 3-dimensional member array
        int[][][] selectionDataTmp = new int[data.getCategoryChooserList().size()][][];
        int channelChoiceQuestionNumber = 0;
        for (CategoryChooser catChooser: data.getCategoryChooserList()) {
            selectionDataTmp[channelChoiceQuestionNumber] = new int[catChooser.getCategoryList().size()][];
            int channelChoiceAnswerNumber = 0;
            for (Category category: catChooser.getCategoryList()) {
                selectionDataTmp[channelChoiceQuestionNumber][channelChoiceAnswerNumber] = listToIntArray(category.getQuestionIndexList());
                ++channelChoiceAnswerNumber;
            }
            ++channelChoiceQuestionNumber;
        }
        categoryChoosers = selectionDataTmp;

        // verifies in advance that there are not very suspicious indices in the selection data
        assertDataValid();
    }

    /**
     * asserts that the selection data does not contain a question index which is beyond the length of
     * the ballot race questions array. Otherwise, throws an IndexOutOfBoundsException
     */
    private void assertDataValid () {
        // find the maximum question index in the selection data
        int maxQuestionIndex = -1;
        for (int index: sharedDefaults) {
            maxQuestionIndex = Math.max(maxQuestionIndex, index);
        }
        for (int[][] categoryChooser: categoryChoosers) {
            for (int[] category: categoryChooser) {
                for (int index: category) {
                    maxQuestionIndex = Math.max(maxQuestionIndex, index);
                }
            }
        }

        // asserts that the maximal question index in the selection data does not overflow the ballot race questions array
        int questionsLength = allBallotQuestions.length;
        if (maxQuestionIndex >= questionsLength) {
            String errorMessage = "Selection data refers to question index " + maxQuestionIndex + " while we have only " + questionsLength + " questions totally";
            logger.error(errorMessage);
            throw new IndexOutOfBoundsException(errorMessage);
        }
    }


    /**
     * an implementation of the QuestionSelector interface method.
     * In this selector class the identifier simply marks all the ballot race questions which appear in at least one
     * category of the categories chosen by the voter (or in the shared defaults category) in the channel choice round.
     * @param channelChoiceAnswers The answers given by the voter to the channel choice questions
     * @return the channel identifier
     */
    @Override
    public byte[] getChannelIdentifier(List<BallotAnswer> channelChoiceAnswers) {
        /*
         * Currently, this implementation of the QuestionSelector interface returns an over-simplified identifier which
         * is merely an array of booleans (which flags the questions to appear in the ballot)
         * For elections with more than one possible channel we should return a more printable and recognizable
         * identifier to be put in the plaintext of the ballot
         */
        byte[] isSelected = new byte[allBallotQuestions.length];
        java.util.Arrays.fill(isSelected, QUESTION_NOT_SELECTED);

        for (int i: sharedDefaults) {
            isSelected[i] = QUESTION_SELECTED;
        }

        int channelChoiceQuestionNumber = 0;
        for (BallotAnswer ballotAnswer: channelChoiceAnswers) {
            assertAnswerLengthIsOne(ballotAnswer, channelChoiceQuestionNumber);
            for (int i: categoryChoosers[channelChoiceQuestionNumber][(int)ballotAnswer.getAnswer(0)]) {
                isSelected[i] = QUESTION_SELECTED;
            }
        }

        return isSelected;
    }

    /**
     * Verifies that the ballot answer is of length 1. (We do not yet handle multi-choice questions in the channel choice round).
     * Otherwise, throws an exception.
     * @param ballotAnswer the answer to verify whose length is one
     * @param questionNumber the number of the question (needed only for error message strings)
     */
    private void assertAnswerLengthIsOne (BallotAnswer ballotAnswer, int questionNumber) {
        if (ballotAnswer.getAnswerCount() != 1) {
            String errorMessage = "SimpleListCategoriesSelector expects a single answer for every channel choice question\n";
            errorMessage += "Answer to question number " + (questionNumber+1) + " is";
            for (long i : ballotAnswer.getAnswerList()) {
                errorMessage += " " + i;
            }
            logger.error(errorMessage);
            throw new IllegalArgumentException(errorMessage);
        }
    }

    @Override
    public List<BallotQuestion> selectQuestionsForVoter(byte[] channelIdentifier) {
        List<BallotQuestion> selectedQuestions = new ArrayList<>();
        for (int i = 0; i < channelIdentifier.length; ++i) {
            if (channelIdentifier[i] == QUESTION_SELECTED) {
                selectedQuestions.add(allBallotQuestions[i]);
            }
        }
        return selectedQuestions;
    }

    /**
     * copies a List of Integers into an int[] array of same length
     * @param l a list of Integers
     * @return an array of ints
     */
    private int[] listToIntArray(List<Integer> l) {
        int[] res = new int[l.size()];
        int index = 0;
        for (Integer i: l) {
            res[index] = i;
            ++index;
        }
        return res;
    }
}
