#!/usr/bin/env python3
"""
Reads csv file FILENAME.csv separated by "," generated by AMC in "0;1;0;1;1;0" mode.
File must have columns MANDATORY_COLUMNS (Exam, A:Matricule, Name, Mark, zmatr, zmatr[*]).
All questions that starts with QF (Question Fermées) will be transformed into numerical value.

Generates FILENAME_numerical_values.xlsx and .csv
"""

import csv
import sys
import os
import re
import argparse
from pprint import pprint # for debugging

assert sys.version_info[0] >= 3, "Python 3 please"

def warning(*args): # orange
    print('\033[33m' + 'Warning:', *args, '\033[0m')
    
def info(*args): # green
    print('\033[32m' + 'Info:', *args, '\033[0m')

def error(*args): # error
    print('\033[31m' + 'Error:', *args, '\033[0m')

p = argparse.ArgumentParser()

p.add_argument('filename')
p.add_argument('--policies', nargs='*', choices={'EmptyColumnMeansFirstTicked', 'NoSignMeansPlus'}, default=['EmptyColumnMeansFirstTicked'])

a = args = p.parse_args()

FILENAME = args.filename # 'generateurAMC_B.csv'
POLICIES = {'EmptyColumnMeansFirstTicked'} & {'EmptyColumnMeansFirstTicked', 'NoSignMeansPlus'} # set(args.policies)

assert FILENAME.endswith('.csv')

if os.name == 'nt': # and not args.win_no_cd:
    os.chdir(os.path.dirname(sys.argv[0]) or '.') # we right clicked on the file, so cd into the directory of the file

with open(FILENAME) as f:
    csvMatrix = list(csv.DictReader(f))

if not csvMatrix:
    print('Empty csv')
    sys.exit(1)

FIELDS = list(csvMatrix[0].keys()) # DictReader enforces that all(l.keys() == FIELDS for l in csvMatrix)

ClosedQuestionsTicked = [f for f in FIELDS if f.startswith('TICKED:QF')]

assert all(c.endswith('digits') or c.endswith('exp') for c in ClosedQuestionsTicked)
for a,b in (('digits', 'exp'), ('exp', 'digits')):
    assert all(c[:-len(a)] + b in ClosedQuestionsTicked
               for c in ClosedQuestionsTicked
               if c.endswith(a))

ClosedQuestions = [c[len('TICKED:'):-len('digits')] for c in ClosedQuestionsTicked if c.endswith('digits')]

class EmptyColumn(Exception):
    pass

class MultipleTicksInColumn(Exception):
    pass

def parseQF(csvLine:dict, name:'QF1b', *, base=10, policies=POLICIES):
    def parsePart(part:'digits|exp', expectedNumbers:3, *, base=base, hasSign=True, direction:'plus|minus'='plus'):
        """
        Parse a signed number.
        if hasSign, the first two boxes contains the sign (first = +, second = -).
        Then we have base=10 expectedNumbers boxes representing numbers. For example expectedNumbers = 3, digits = [6,0,9].
        If direction == "plus" Then Digits represents 609
        If direction == "minus" Then Digits represents 6.09
        """
        assert part in ('digits', 'exp') and direction in ('plus', 'minus')
    
        field = 'TICKED:{}{}'.format(name, part)
        Ticks = list(map(int, csvLine[field].split(';')))
        
        # Example: Ticks = [0,1, 0,0,0,1,0,0,0,0,0,0, 0,1,0,0,0,0,0,0,0,0]
        
        assert len(Ticks) == (2 if hasSign else 0) + expectedNumbers * base
        
        if hasSign:
            Sign = Ticks[0:2]
            Digits = [Ticks[2+n : 2+n+base] for n in range(0, base * expectedNumbers, base)]
        else:
            Sign = [1,0] # Positive
            Digits = [Ticks[n : n+base] for n in range(0, base * expectedNumbers, base)]
        
        if Sign == [0,0] and 'NoSignMeansPlus' in policies:
            Sign = [1,0]
        
        # Example if hasSign == True and base == 10: Sign = [0,1]; Digits = [[0,0,0,1,0,0,0,0,0,0], [0,1,0,0,0,0,0,0,0,0]]
        
        for col in [Sign] + Digits:
            if sum(col) == 0:
                if 'EmptyColumnMeansFirstTicked' in policies:
                    col[0] = 1
                else:
                    raise EmptyColumn("User ticked zero columns in a column (for {})".format(part))
            if sum(col) > 1:
                raise MultipleTicksInColumn("User ticked two columns in a column (for {})".format(part))
        
        sign = +1 if Sign[0] else -1
        digits = [D.index(1) for D in Digits]
        
        # Example: sign = -1; digits = [3, 1]
        
        number = (sign * sum(n * base ** (-i) for i,n in enumerate(digits)) if direction == 'minus' else
                  sign * sum(n * base ** i for i,n in enumerate(reversed(digits))) if direction == 'plus' else None)
    
        # Example if direction == 'minus': number = -3.1
        # Example if direction == 'plus':  number = -31
        
        return number
    
    number = parsePart('digits', 3, hasSign=True, direction='minus')
    numberExp = parsePart('exp', 1, hasSign=True, direction='plus')
    numberFinal = number * base ** numberExp
    
    return {'value': numberFinal, 'mantissa':number, 'exp':numberExp, 'sign': 0 if numberFinal == 0 else -1 if numberFinal < 0 else 1}

def parseFrenchNumber(string):
    """
    >>> parseFrenchNumber('1,5')
    1.5
    """
    try:
        return float(string.replace(',', '.'))
    except:
        return string

def looksLikeFrenchNumber(string):
    return string and all(c in '0123456789,' for c in string)

info('ClosedQuestions', ClosedQuestions)

flatten = lambda X: [y for x in X for y in x]

QF_FIELDS = 'value mantissa exp sign'.split()

for USE_XL in (False, True):

    out_filename = FILENAME[:-4] + '_numerical_values' + ('.xlsx' if USE_XL else '.csv')

    if USE_XL:
        try:
            import openpyxl
        except ImportError:
            warning('openpyxl not installed, no xlsx generated.')
            continue

        wb = openpyxl.Workbook()

        def writerow(row):
            wb.active.append(row)
        
        def close():
            wb.save(out_filename)
            
    else:
        f = open(out_filename, 'w')
        writer = csv.writer(f)
        
        def writerow(row):
            writer.writerow(row)
        
        def close():
            f.close()

    try:
        writerow(flatten([
            [k]
            
            if k not in ClosedQuestionsTicked else
            
            [q + '_' + key for key in QF_FIELDS]
            
            if k.endswith('digits') else
            
            []
            
            for k in FIELDS
            for q in [
                k[len('TICKED:'):-len('digits')] if k.endswith('digits') else
                k[len('TICKED:'):-len('exp')]
            ]
        ]))
        
        for student in csvMatrix:
            
            answers = {}
            
            for q in ClosedQuestions:
                try:
                    answers[q] = parseQF(student, q)
                except Exception as e:
                    answers[q] = e
            
            writerow(flatten([
                [parseFrenchNumber(student[k]) if looksLikeFrenchNumber(student[k]) else str(student[k]) ]
                
                if k not in ClosedQuestionsTicked else
                
                (
                    [answers[q][key] for key in QF_FIELDS]
                    if not isinstance(answers[q], Exception) else
                    [str(answers[q].__class__.__name__)] * bool(QF_FIELDS) + ['NA'] * (len(QF_FIELDS) - 1)
                )
                
                if k.endswith('digits') else
                
                []
                
                for k in FIELDS
                for q in [
                    k[len('TICKED:'):-len('digits')] if k.endswith('digits') else
                    k[len('TICKED:'):-len('exp')]
                ]
            ]))
    finally:
        close()

    info('Generated', out_filename)

if os.name == 'nt':    
    os.system('pause')
