#!/usr/bin/env python3
# coding=utf-8

# !!! Falta fer shuffle d'alguns items, ara només ho fa dels choices


import sys
import string
import pprint
import yaml
import json
import random
import os
import time
import argparse
from colorama import init, Fore, Style

def error(title, reason):
    raise Exception(Style.BRIGHT + Fore.RED + '--- ERROR in question ' + str(title) + ' --- ' + str(reason) + Style.RESET_ALL)

def warning(reason):
    print(Fore.YELLOW + '--- WARNING --- ' + str(reason) + Style.RESET_ALL)


# function to build Single Choice questions
def build_sc(output, title):
    # make sure choices are provided
    if not output.get("choices", False):
        error(title, 'A list of Choices must be defined!')
    # make sure each choice has text
    correct = False
    false_answers = []
    for choice in output["choices"]:
        text = choice.get("text", False)
        if not text:
            error(title, 'Text must be provided for every choice!')

        # check types
        check_not_dict_list(text, title, "Text", "basic type")
        check_not_dict_list(choice.get("hint"), title, "Hint", "basic type")
        # check that at most one correct answer is provided
        if choice.get("correct", False):
            if correct:
                error(title, 'Only one correct answer must be provided!')
            correct = True
            correct_text = choice.get("text")
        else:
            false_answers.append(text)
    # check that at least one correct answer is provided
    if not correct:
        error(title, 'A correct answer must be provided!')
    # check that correct answer is not repeated
    if correct_text in false_answers:
        error(title, 'Only one correct answer with the same text must be provided!')
    # shuffle the choices if needed
    if (output.get('shuffle', True)):
        random.shuffle(output['choices'])

    return output


# function to build Multiple Choice questions
def build_mc(output, title):
    # make sure choices are provided
    if not output.get("choices", False):
        error(title, 'A list of Choices must be defined!')

    # make sure each choice has text
    correct = False
    for choice in output["choices"]:
        text = choice.get("text", False)
        if not text:
            error(title, 'Text must be provided for every choice!')
        check_not_dict_list(text, title, "Text", "basic type")
        check_not_dict_list(choice.get("hint"), title, "Hint", "basic type")

    # shuffle the choices if needed
    if (output.get('shuffle', True)):
        random.shuffle(output['choices'])

    return output


# function to build Ordering questions
def build_o(output, title):
    # check non empty label
    label = output.get("label")
    if label == None:
        error(title, 'A label for the choice list must be provided!')
    check_not_dict_list(label, title, "label", "text")
    # check non empty items
    items = output.get("items", False)
    if not items:
        error(title, 'A list of items must be provided!')
    check_list(items, title, "items")
    for item in items:
        check_not_dict_list(item, title, "Items", "text")
    output["items_rand"] = list(items)
    random.shuffle(output["items_rand"])
    return output

# function to build FillIn questions
def build_fi(output, title):
    # check non empty context
    context = output.get("context", False)
    if not context:
        error(title, 'A context must be provided!')
    check_not_dict_list(context, title, "Context", "text")
    # check non empty items
    if not output.get("items", False):
        error(title, 'An item list must be provided!')
    for name, item in output["items"].items():
        if not name in context:
            error(title, "Item " + str(name) + " is not in the context!")
        if not item.get("options", False):
            check_writable_item(item, title)
        else:
            check_dropdown_item(item, title)
    return output


def check_writable_item(item, title):
    # check for mandatory fields
    maxlength = item.get("maxlength")
    if maxlength == None:
        error(title, 'Invalid item! Did you forget the list of options or maxlength?')
    if not isinstance(maxlength, int):
        error(title, "Maxlength must be an integer!")

    correct = item.get("correct")
    if correct == None:
        error(title, 'All correct answers must be provided!')
    check_not_dict_list(correct, title, "Correct", "basic type")

    item.update({'ignorecase': item.get("ignorecase", True), 'trim': item.get(
        "trim", True), 'placeholder': item.get("placeholder", '?')})
    # ??? placeholder es opcional ??? TBD

    check_not_dict_list(item.get("ignorecase"), title, "ignorecase", "true or false")
    check_not_dict_list(item.get("trim"), title, "trim", "true or false")
    check_not_dict_list(item.get("placeholder"), title, "placeholder", "a text")


def check_dropdown_item(item, title):
    correct = item.get("correct")
    if correct == None:
        error(title, 'All correct answers must be provided!')
    check_not_dict_list(correct, title, "Correct", "basic type")

    options = item.get("options")
    if options == None:
        error(title, 'A list of options must be provided!')
    check_list(options, title, "options")
    # check that the correct option is available
    correct = False
    false_answers = []
    for option in options:
        if option == item.get("correct"):
            correct = True
        check_not_dict_list(option, title, "All options", "basic type")
    # !!! potser podriem cambiar la manera de llegir opcions per no haver d'escriure la correcta a la llista tambe
    if not correct:
        error(title, "The correct answer must be in the options!")


# function to build Matching questions
def build_m(output, title):
    # check for labels, left and
    labels = output.get("labels", False)
    check_list(labels, title, "labels")
    if not labels or len(labels) != 2:
        error(title, "A list with two labels must be provided!")
    left = output.get("left", False)
    right = output.get("right", False)
    if not left or not right:
        error(title, "Two lists (right and left) must be provided!")
    check_list(left, title, "left")
    check_list(right, title, "right")
    # make sure left and right have the same size
    if len(right) != len(left):
        error(title, "The right and left lists must have the same size")
    for item in left:
        check_not_dict_list(item, title, "left list's items", "basic type")
    for item in right:
        check_not_dict_list(item, title, "right list's items", "basic type")
    output["right_rand"] = list(right)
    output["left_rand"] = list(left)
    random.shuffle(output["right_rand"])
    random.shuffle(output["left_rand"])

    return output


# function to build Open Questions
def build_oq(output, title):
    # this is always good I guess
    return output


def build_q(fname, title):

    py_name = str(fname) + ".py"
    yml_name = str(fname) + ".yml"

    # read the python code
    if os.path.exists(py_name):
        code = open(py_name).read()
    else:
        code = ""

    # read the question description
    q = open(yml_name).read()

    # execute the code, using new global and local dictionaries
    def all_different(*x):
        return len(x) == len(set(x))
    ldict = {'all_different': all_different}
    exec(code, globals(), ldict)

    # modify the question description with the local dictionary
    subs = string.Template(q).substitute(ldict)

    # get the text back to data
    output = yaml.load(subs, Loader=yaml.FullLoader)
    # make sure we have the mandatory attributes
    text = output.get("text")
    if text == None:
        error(title, "Missing text!")
    check_not_dict_list(text, title, "Text", "string")

    qtype = output.get("type")
    if qtype == None:
        error(title, "Missing type!")
    check_not_dict_list(text, title, "Question type", "just a word")

    # fix fields according to type
    if output["type"] == "SingleChoice":
        return build_sc(output, title)
    elif output["type"] == "MultipleChoice":
        return build_mc(output, title)
    elif output["type"] == "Ordering":
        return build_o(output, title)
    elif output["type"] == "FillIn":
        return build_fi(output, title)
    elif output["type"] == "Matching":
        return build_m(output, title)
    elif output["type"] == "OpenQuestion":
        return build_oq(output, title)
    else:
        error(title, "Incorrect question type!")

# so many arguments :(


def check_not_dict_list(thing, title, name, goal):
    if isinstance(thing, list) or isinstance(thing, dict):
        error(title, str(name) + " must be a " + str(goal) + "!")


def check_list(thing, title, name):
    if not isinstance(thing, list):
        error(title, str(name) + " must be a list!")

def make_quiz(seed):
    quiz = yaml.load(open("quiz.yml"), Loader=yaml.FullLoader)

    random.seed(seed)

    if quiz.get('shuffle', True):
        random.shuffle(quiz['questions'])

    sample = quiz.get('sample', None)
    if sample != None:
        if sample <= 0:
            print(Style.BRIGHT + Fore.RED + '--- ERROR --- Sample number must be a natural number bigger than 0!' + Style.RESET_ALL)
            sys.exit(0)
        else:
            question_num = len(quiz['questions'])
            if sample <= question_num:
                quiz['questions'] = quiz['questions'][:-question_num+sample]
            else:
                warning('Sample number is bigger than the number of questions! All questions will be used.')

    quiz['seed'] = seed
    quiz['time-generation'] = time.ctime()  # !!! posar format YYYY-MM-DD HH:MM:SS

    # Check types
    check_not_dict_list(quiz.get("statement"), "Quiz", "Statement", "text")

    score_sum = 0
    for question in quiz['questions']:
        score = question.get("score", 0)
        weight = question.get("weight", 1)
        if not isinstance(score, int) or score <= 0:
            error("quiz", "All scores must be positive integers")
        if not isinstance(weight, int) or weight <= 0:
            error("quiz", "All weights must be positive integers")
        score_sum += score
        if not question.get("file", False) or not question.get("title", False):
            error("quiz", "All questions need a file and a title!")
        question['q'] = build_q(question['file'], question['title'])
        question['a'] = {}
        question['points'] = 0
    if score_sum != 100:
        error("quiz", "Scores don't add to 100!!!")

    json.dump(quiz, sys.stdout, indent=4)


def main():
    init()

    parser = argparse.ArgumentParser(
        usage="%(prog)s seed",
        description="Generate .json files for Jutge quizzes",
    )

    # Parse options with real arguments
    _, args = parser.parse_known_args()

    seed = 0
    if len(args) == 0:
        seed = random.randint(0,10000)
    else:
        seed = args[0]

    if 'quiz.yml' in os.listdir('.'):
        make_quiz(seed)
    else:
        for dir in next(os.walk('.'))[1]:
            os.chdir(dir)
            make_quiz(seed)
            os.chdir('..')


if __name__ == '__main__':
    main()
