#! /usr/bin/env python3
#  coding: utf-8
#
#  Copyright (c) 2022 by Nicolas Mesnier <nmesnier@free.fr>
# 
#  This program is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License version 3 or
#  above as published by the Free Software Foundation.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program. If not, see <http://www.gnu.org/licenses/>.
#
#=======================================================================
"""%(prog)s -- Rapport et statistiques d'épreuve

description:
    Python script to intialise, add statistics and compile an exam
    report written in markdown from an ODS spreadsheet. It's optimaly
    simple if name is of the form "<classe>-<type><num>.ods".

examples:
    $ %(prog)s -f -m SII -c PCSI -t DS -n 2
    $ %(prog)s -i PCSI-DS2.ods
    $ %(prog)s -s PCSI-DS2.ods
"""

__version__   = "0.1.6"
__homepage__  = "<http://nmesnier.free.fr/devoir.html>"
__author__    = "Nicolas Mesnier <nmesnier@free.fr>"
__copyright__ = "Copyright (c) 2023 by "+__author__
__bugs__      = "Email bug reports to "+__author__+"."
__license__="""\
There is NO warranty. Redistribution of this software is covered
by the terms the GNU General Public License version 3 or above."""

#
#  Parameters
#  ----------
#
feuille = "/home/$(whoami)/bin/devoir.ods"
matiere = "Sciences industrielles de l'ingénieur"
classes = ["MPSI", "PCSI", "PTSI", "MP", "PSI", "PT"]
typesEval = ["DS", "DM", "I"]

short={
    "Sciences industrielles de l'ingénieur": "SII",
    "Informatique commune": "Info",
    "Mathématiques": "Maths",
    "Physique-Chimie": "PC",
}

#  Dependencies
#  ------------
#
#   - python standard library:
#
try:
    import os, sys              # system
    import subprocess           # os subprocess management
    import shutil               # file operations
    import glob                 # Unix style pathname pattern expansion
    import re                   # Regular expression operations
    import argparse             # Parser for CLI arguments and options
    import datetime             # Basic date and time types
    import math as ma           # math
    import textwrap             # Text wrapping and filling
except ImportError:
    # Checks the installation of the necessary python modules
    print((os.linesep * 2).join(["An error found importing one module:",
          str(sys.exc_info()[1]), "You need to install it", "Exit..."]))
    sys.exit(-2)
#
#   - external executables:
#     - Latex & Co.: to compile *.tex documents
#     - libreoffice
#     - panpdf <http://nmesnier.free.fr/files/panpdf>, a pandoc wrapper
# 
EXE = {
    "pdflatex": ["pdflatex", "--shell-escape"],
    "panpdf": ["panpdf", "--no-num"],
    "ods2csv": ["libreoffice", 
        '--convert-to "csv:Text - txt - csv (StarCalc):59,34,0,1,4/2/1"'],    
    }
# 
#  Installation
#  ------------
#     - make as executable width "$ chmod +x devoir.py"
#     - copy file to "/home/$(whoami)/bin"
#
#  History
#  --------
#     25/02/2023 - change default "rapport" file name
#     07/11/2022 - add support for "interrogation"
#     14/10/2021 - script creation.
# 
#  TO DO
#  -----
#     - 
#
#  Any feedback is very welcome.
#  If you find problems, please submit bug reports/patches via email.
#  You can always get the latest version of this file at:
#    http://nmesnier.free.fr/files/devoir.html
#  If that URL should fail, try contacting the author.
#
#=======================================================================
# *** Generic functions
#=======================================================================
# Get current school year
Y=int(datetime.datetime.now().strftime("%Y"))
if int(datetime.datetime.now().strftime("%m"))>8:
    annee = str(Y)+"-"+str(Y+1)
else:
    annee = str(Y-1)+"-"+str(Y)

def WriteFile(file, contents, mode="w"):
    """
    Write text file "file" from a list "contents"
    where each item is a line.
    """
    f = open(file, mode)
    for l in contents:
        f.writelines(l+'\n')
    f.close()

def ExtCommand(cmd, args, quiet=True):
    """
    Run an external command defined as a string with arguments
    given as a string or list of strings.
    """
    if type(args)==list:
        l=cmd+args
    else:
        l=cmd+[args]
    if quiet:
        subprocess.call(l, stdout=open(os.devnull, 'wb'))
    else:
        os.system(" ".join(l))

def RemoveFilesBut(pattern1, pattern2):
    """
    Remove files with pattern1, which have not pattern2.
    Example:
      >>> RemoveFilesBut("tmpfile.*",[".tex","*.pdf"])
      remove all "tmpfile.*" but not "tmpfile.tex" and "tmpfile.pdf".
    """
    L1=glob.glob(pattern1)
    if type(pattern2)==list:
        L2=[]
        for p in pattern2:
            L2=L2+glob.glob(p)
    else:
        L2=glob.glob(pattern2)
    for file in L1:
        if os.path.isfile(file) and file not in L2:
            os.remove(file)

def GetKey(dic, value):
    if value in dic.values():
        for e in dic.keys():
            if dic[e]==value:
                return(e)
    else:
        return(None)

#=======================================================================
# *** core functions
#=======================================================================
def Get_Questions(file):
    f=open(file,"r")
    for l in f:
        l=l.strip()
        C=l.split(";")
        nc=len(C)
        if re.search("Q([1-5]\.)?[0-9]{1,2}", l):
            n=1
            while n<nc-2 and re.search("Q([1-5]\.)?[0-9]{1,2}", C[2+n]):
                n+=1
            Q = C[3:2+n]
            break
    f.close()
    return(Q)

def Init_rapport(rapport, file, classe, typeEval, num):
    c=[]
    c.append(r"\begin{center}")
    c.append(matiere+r" \textemdash{} "+classe+", "+annee)
    c.append(r"\vskip1ex")
    c.append(r"\rule{1cm}{.5pt}")
    c.append(r"\vskip1em")
    if typeEval=="DS":
        c.append(r"{\LARGE \bfseries Devoir surveillé \no{}"+num+"}")
    elif typeEval=="DM":
        c.append(r"{\LARGE \bfseries Devoir libre \no{}"+num+"}")
    elif typeEval=="I":
        c.append(r"{\LARGE \bfseries Interrogation \no{}"+num+"}")
    c.append(r"\vskip1em")
    c.append(r"{\large Rapport d'épreuve}")
    c.append(r"\vskip.5em")
    c.append(r"\rule{1cm}{.5pt}")
    c.append(r"\end{center}")
    c.append(r"\pagestyle{plain}")
    c.append(r"")
    c.extend([r"## Généralités", "", r" - ", ""])
    c.append(r"")

    for Q in Get_Questions(file):
        qs = Q.split('.')
        if len(qs)==2 and int(qs[1])==1:
            c.extend([r'# Partie '+qs[0][1:], ""])
        c.extend([r'## '+Q, "", r" - ", ""])

    WriteFile(rapport, c)

def Get_Stats(file):
    # output:
    #   data[0] : questions
    #   data[1] : nb d'élèves ayant traité
    #   data[2] : taux de traitement
    #   data[3] : moyenne
    #   data[4] : écart-type
    #   data[5] : min
    #   data[6] : max
    #   histo
    #   brut : total des points brut
    data=[]
    histo=[]
    f=open(file,"r")
    nQ=False
    iH=False
    brut=False
    for l in f:
        l=l.strip()
        C=l.split(";")
        nc=len(C)
        if re.search("Q([1-5]\.)?[0-9]{1,2}", l):
            # n: nombre de questions +1
            n=1
            while n<nc-2 and re.search("Q([1-5]\.)?[0-9]{1,2}", C[2+n]):
                n+=1
            nQ=True
            data.append(C[3:2+n])
        elif nQ and not(brut):
            brut=C[2+n] 
        elif nQ and brut and C[0]=="" and C[3]!="":
            data.append(C[3:6+n])
        elif "Histo" in C:
            iH=C.index("Final")
        elif iH and C[iH-2]!="0":
            histo.append(C[iH])
    f.close()
    return(data, histo, brut)

def digit(n):
    """ to format string number as "12,34" or "01,23"."""
    return("{:05.2f}".format(float(n)).replace(".",","))

def Append_stats_to_rapport(rapport, typeEval, data, brut):
    # nombre de questions
    n = len(data[0])
    # *** stats par question
    c=[
    r"",
    r"\newpage",
    r"# Statistiques", 
    r"", 
    r"## Par question", 
    r"",
    r"Chaque question est notée sur 5 points, en points entiers (pour éviter",
    r"toute demie-mesure).", 
    r"" 
    ]
    header=[
    "Question          ",
    "------------------",
    "Traitée (élèves)  ",
    "Taux traitement   ",
    "Moyenne           ",
    "Écart-type        ",
    "Min               ",
    "Max               "
    ]
    lc=8    # nombre colonnes pour les chiffres du tableau
    Nmax=10 # nombre max de questions par ligne de tableau
    nt=ma.ceil(n/Nmax) # nombres de lignes de stats
    stats=[header.copy() for e in range(nt)]
    for q in range(n):
        l=q//Nmax # get stats line
        stats[l][0]+=2*' '+'{:{align}{width}}'.format(data[0][q], align='^', width=lc)
        stats[l][1]+=2*' '+'{:{align}{width}}'.format(lc*"-", align='^', width=lc)
        stats[l][2]+=2*' '+'{:{align}{width}}'.format(data[1][q], align='^', width=lc)
        stats[l][3]+=2*' '+'{:{align}{width}}'.format(data[2][q].replace("%"," %"), align='^', width=lc)
        for i in [4,5]:
            stats[l][i]+=2*' '+'{:{align}{width}}'.format(data[i-1][q].replace(".",","), align='^', width=lc)
        for i in [6,7]:
            stats[l][i]+=2*' '+'{:{align}{width}}'.format(data[i-1][q], align='^', width=lc)
    for l in stats:
        for e in l:
            c.append(e)
        c.append("")
    # *** notes  du devoir
    if typeEval=="DS":
        c.extend([
        "## Devoir",
        r"",
        r"Les notes brutes étant assez faibles, elles ont été recalées avec la",
        r"formule",
        r"$$",
        r"N=",
        r"\left(20\times\left(\dfrac{n}{20}\right)^a-M\right)\times",
        r"\dfrac{\Sigma}{\sigma}+M",
        r",\qquad",
        r"a=\dfrac{\ln\left(\dfrac{M}{20}\right)}{\ln\left(\dfrac{m}{20}\right)}",
        r"$$",
        r"où",
        r"$n$ est la note brute, $N$ la note modifiée",
        r"et avec $m$ la moyenne et $\sigma$ l'écart-type des notes brutes,",
        r"et $M$ la moyenne et $\Sigma$ l'écart-type de la distribution de notes",
        r"désirée.",
        "",
        r"Notes DS   Brutes (/"+str(brut)+")    Finales",
        r"--------- -------------   --------",
        r"Moyenne     "+digit(data[3][n])+10*" "+digit(data[3][n+3]),
        r"Écart-type  "+digit(data[4][n])+10*" "+digit(data[4][n+3]),
        r"Min         "+digit(data[5][n])+10*" "+digit(data[5][n+3]),
        r"Max         "+digit(data[6][n])+10*" "+digit(data[6][n+3]),
        ])
    elif typeEval=="DM":
        c.extend([
        "## Devoir",
        r"",
        r"Pour un devoir libre, on se satisfait des notes brutes.",
        r"",
        r"Notes DM   Brutes (/20)",
        r"--------- -------------",
        r"Moyenne     "+digit(data[3][n+2]),
        r"Écart-type  "+digit(data[4][n+2]),
        r"Min         "+digit(data[5][n+2]),
        r"Max         "+digit(data[6][n+2]),
        ])
    elif typeEval=="I":
        c.extend([
        "## Interrogation",
        r"",
        r"Pour une interrogation, on se satisfait des notes brutes.",
        r"",
        r"Notes        (/20)",
        r"--------- -------------",
        r"Moyenne     "+digit(data[3][n+3]),
        r"Écart-type  "+digit(data[4][n+3]),
        r"Min         "+digit(data[5][n+3]),
        r"Max         "+digit(data[6][n+3]),
        ])
    c.extend([
    r"",
    r"\newpage",
    r"On représente ci-dessous la distribution des notes finales.",
    r"",
    r"\begin{center}",
    r"\includegraphics{plot.pdf}",
    r"\end{center}",
    ])

    WriteFile(rapport, c, "a")

def MakeHisto(histo):
    c=[
    r"\documentclass[border=1ex,tikz,preview]{standalone}",
    r"\usepackage{pgfplots}",
    r"\begin{document}",
    r"\begin{tikzpicture}",
    r"\begin{axis}[",
    r"xmin=0, xmax=20, xtick distance=5, minor xtick={0,1,...,20},",
    r"ymin=0, ymax="+str(1+max([int(e) for e in histo[:20]]))+", minor y tick num = 1, ",
    r"area style]",
    r"\addplot+[ybar interval,mark=no] plot coordinates {"
    ]
    for k in range(20):
        c.append("("+str(k)+","+histo[k]+")")
    c.extend([
    "(20,0)",
    "};",
    r"\end{axis}",
    r"\end{tikzpicture}",
    "\end{document}"
    ])
    WriteFile("plot.tex", c)
    for k in range(2):
        ExtCommand(EXE["pdflatex"], "plot.tex")
    RemoveFilesBut("plot.*", ["*.tex","*.pdf"])

def IsInName(name, choices):
    s=False
    for c in choices:
        if c in name:
            s = c
            break
    return(s)

def statsDevoir(rapport, file, classe, typeEval, num, mode):
    if mode:
        if os.path.isfile(rapport):
            a=input("File \""+rapport+"\" allready exists. Replace?  y/[n] ")
            if a not in ["y", "yes", "YES", "o"]:
                sys.exit(2)
        Init_rapport(rapport, file, classe, typeEval, num)
    else:
        if not(os.path.isfile(rapport)):
            a=input("File \""+rapport+"\" doesn't exists. Create?  y/[n] ")
            if a not in ["y", "yes", "YES", "o"]:
                sys.exit(2)
            Init_rapport(rapport, file, classe, typeEval, num)

        data, histo, brut = Get_Stats(file)
        Append_stats_to_rapport(rapport, typeEval, data, brut)
        MakeHisto(histo)
        ExtCommand(EXE["panpdf"], rapport) # compile rapport

#=======================================================================
# *** main
#=======================================================================

if __name__ == '__main__':# and sys.argv[0]!='' and len(sys.argv)>0:
    """
    Standalone program
    """
    try:
        # command-line arguments and options       
        parser = argparse.ArgumentParser(
            prog=os.path.basename(sys.argv[0]),
            formatter_class=argparse.RawDescriptionHelpFormatter,
            description=__doc__,
            epilog=textwrap.dedent("\n".join([__homepage__,"\n",__bugs__ ]))
            )
        # *** mode
        mode = parser.add_mutually_exclusive_group(required=True)
        mode.add_argument("--feuille","-f",
                default=False,
                action='store_true',
                dest="copy",
                help="copier/créer la feuille de calcul de base")
        mode.add_argument("--init","-i",
                default=None,
                nargs=1,
                metavar='<input ods file>',
                dest='odsi',
                action='store',
                help="Initialiser le rapport")
        mode.add_argument("--stat","-s",
                default=None,
                nargs=1,
                metavar='<input ods file>',
                dest='odss',
                action='store',
                help="Ajouter les statistiques au rapport")
        # *** options
        parser.add_argument("--matiere","-m",
                default=None,
                action='store',
                dest="matiere",
                help="Matière du devoir")
        parser.add_argument("--classe","-c",
                default=None,
                action='store',
                dest="classe",
                choices=classes,
                help="Classe évaluée")
        parser.add_argument("--type","-t",
                default=None,
                action='store',
                dest="type",
                type=str,
                choices=typesEval,
                help="Type d'évaluation")
        parser.add_argument("--num","-n",
                default=None,
                action='store',
                dest="num",
                type=int,
                help="Numéro d'évaluation")
        parser.add_argument("--output","-o",
                default=None,
                action='store',
                dest="rapport",
                help="Nom du rapport")
        parser.add_argument('-v', '--version',
                default=False,
                action='version',
                version=textwrap.dedent("\n".join([
                    '%(prog)s '+__version__,
                    __copyright__,__license__])),
                help="Print version information and exit.")
         # read command-line arguments and options 
        args = parser.parse_args()

        # copy ods file
        if args.copy:
            if (args.rapport is not None):
                file = args.rapport
            elif (args.classe is not None) and (args.type is not None) and (args.num is not None):
                file = args.classe + "-" + args.type + str(args.num) + ".ods"
            else:
                file = "devoir.ods"
            if os.path.isfile(file):
                a=input("File \""+file+"\" allready exists. Replace?  y/[n] ")
                if a not in ["y", "yes", "YES", "o"]:
                    sys.exit(2)
            shutil.copyfile(feuille, file)
            sys.exit(0)
        # read matiere?
        if args.matiere is not None:
            matiere = args.matiere
        if (matiere in short.values()):
            matiere = GetKey(short, matiere)
        # case init or not
        if (args.odsi is not None):
            ods = args.odsi[0]
            init = True
        elif (args.odss is not None):
            ods = args.odss[0]
            init = False
        # let's got
        if os.path.isfile(ods):
            ExtCommand(EXE["ods2csv"], [ods, "1>/dev/null 2>&1"], quiet=False)
            csv = (ods).replace(".ods", ".csv")
            if os.path.isfile(csv):
                if (args.rapport is not None):
                    rapport = args.rapport
                else:
                    # get classe
                    if (args.classe is not None):
                        classe = args.classe
                    else:
                        classe = IsInName(csv, classes)
                        if not(classe):
                            classe = "<classe>"
                    # get typeEval
                    if (args.type is not None):
                        typeEval = args.type
                    else:
                        typeEval = IsInName(csv, typesEval)
                        if not(typeEval):
                            print("\n Attention : spécifiez le type d'évaluation parmi", typesEval,":")
                            print(" - soit avec l'option \"-t DS\" ou \"-t DM\" ;")
                            print(" - soit en renommant votre feuille de calcul.\n")
                            sys.exit(2)
                    if (args.num is not None):
                        num = str(args.num)
                    elif typeEval in csv:
                        num = csv.split(typeEval)[1].split('.')[0] # get num
                    else:
                        num="<num>"
                    rapport = ""
                    if (matiere in short.keys()):
                        rapport += short[matiere]+"-"
                    elif matiere is not None:
                        rapport += matiere+"-"
                    if classe != "<classe>":
                        rapport += classe+"-"
                    rapport += typeEval + (num != "<num>")*num+"_rapport.md"
                statsDevoir(rapport, csv, classe, typeEval, num, init)
        else:
            print("\n file "+args.ods+" does not exists!")
            sys.exit(2)

    except KeyboardInterrupt:
        print("\n! Emergency stop. Shutdown requested...exiting")
        sys.exit(2)
#====================================================================eof
