#!/bin/env python3
# -*- coding: utf-8 -*-
#    TOMUSS: The Online Multi User Simple Spreadsheet
#    Copyright (C) 2020 Thierry EXCOFFIER, Universite Claude Bernard
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    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, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#    Contact: Thierry.EXCOFFIER@univ-lyon1.fr

"""
Only teachers can record texts with a background.
Nobody can modify objects defined by others.
"""

import os
import json
import fcntl
import collections
import io
import tempfile
import zipfile
import PIL
from reportlab.pdfgen import canvas
from PyPDF2 import PdfFileWriter, PdfFileReader
from . import note
from .. import plugin
from .. import files
from .. import document
from .. import utilities
from .. import configuration
from . import upload


files.add('FILES', 'Annotator.html')
files.add('FILES', 'Annotator.js')
files.add('FILES', 'Annotator.css')
files.add('FILES', 'fabric.js')

class Annotate(note.Note):
    tip_cell = "TIP_cell_Annotate"
    ondoubleclick = 'annotator_open'
    attributes_visible = ('minmax', 'weight', 'rounding', "groupcolumn",
                          'annotate', 'grade_type', 'grade_session', 'annotate_pdf',
                         )
    formatte_suivi = "annotator_format_suivi"
    type_change = "i_am_a_new_annotator"
    cell_is_modifiable = 0
    human_priority = 0


def allow_change(server):
    """Not using document.get_cell_from_table because no line
    """
    table = document.table(server.the_year, server.the_semester, server.the_ue, create=False)
    if not table or not table.modifiable:
        server.the_file.write('TABLE???')
        return None
    column = table.columns.from_id(server.the_path[0])
    if not column:
        server.the_file.write('COL???')
        return None
    if not table.authorized_column(server.ticket.user_name, column):
        server.the_file.write(server._("ERROR_value_not_modifiable"))
        return None
    return table, column, os.path.join('UPLOAD', str(table.year), table.semester, table.ue,
                                       column.the_id, '')

def store_stream_in_png(stream, png_filename, keep_pdf=False):
    """Return False if no error"""
    pdf_filename = png_filename.replace('.png', '.pdf')
    with open(pdf_filename, 'wb') as file:
        while True:
            data = stream.read(65536)
            if not data:
                break
            file.write(data)
    return os.system(f'''
        pdftoppm {pdf_filename} {png_filename}
        pnmcat -topbottom {png_filename}-*.ppm | pnmtopng >{png_filename}
        rm {png_filename}-*.ppm {'' if keep_pdf else pdf_filename}
        ''')
configuration.store_stream_in_png = store_stream_in_png

def annotator_upload(server):
    data = server.uploaded
    if data is None or 'data' not in data: # pragma: no cover
        server.the_file.write(server._('MSG_bad_ticket'))
        return

    t = allow_change(server)
    if not t:
        return
    _table, _column, dirname = t

    utilities.mkpath(dirname, create_init=False)
    letter = data.getfirst("letter")
    if not (len(letter) == 1 and 'A' <= letter <= 'Z'):
        server.the_file.write('Hacker!')
        return

    if store_stream_in_png(data["data"].file, dirname + letter + '.png', keep_pdf=True):
        server.the_file.write('BUG') # pragma: no cover
    else:
        server.the_file.write('OK!<script>window.parent.annotator_add("{}")</script>'.format(letter))


plugin.Plugin('annotator_upload', '/{Y}/{S}/{U}/annotator_upload/{*}',
              function=annotator_upload, launch_thread=True,
              upload_max_size=40000000,
             )

def get_letter_stats(table, column, letters):
    """Only get statistics for the usage of ALLOWED letters"""
    stats = {letter:0 for letter in letters}
    for line in table.lines.values():
        comment = line[column.data_col].comment
        if comment:
            letter = comment[0]
            if letter in stats:
                stats[letter] += 1
    return stats

def get_info(server):
    err = document.get_cell_from_table_ro(server, ('Annotate',))
    if isinstance(err, str):
        return
    table, column, lin = err
    if lin and isinstance(column.annotate, str):
        comment = table.lines[lin][column.data_col].comment
        annotate = json.loads(column.annotate)
        if annotate.get('images', ''):
            if comment:
                comment = comment[0]
            else:
                # Search the less used letter
                stats = get_letter_stats(table, column, annotate['images'])
                comment = min(stats, key=lambda x: stats[x])
                table.lock()
                try:
                    table.comment_change(table.get_rw_page(), column.the_id, lin, comment)
                finally:
                    table.unlock()
    else:
        comment = '' # pragma: no cover
    return comment, table, column, lin

def annotator_get(server):
    path = ['UPLOAD', str(server.the_year), server.the_semester,
            server.the_ue, server.the_path[0]]
    if server.the_path[1].endswith('.png') and '..' not in server.the_path[1]:
        path.append(server.the_path[1])
    else:
        info = get_info(server)
        if not info:
            return # pragma: no cover
        path.append(info[0] + '.png')

    upload.upload_get_done(server, 'image/png', os.path.join(*path))

plugin.Plugin('annotator_get', '/{Y}/{S}/{U}/annotator_get/{*}',
              function=annotator_get, launch_thread=True,
              mimetype=None,
              priority=-10 # Before student_redirection
             )

def annotator_load(server):
    info = get_info(server)
    if not info:
        return
    _comment, _table, column, lin = info
    filename = os.path.join('UPLOAD', str(server.the_year), server.the_semester,
                            server.the_ue, column.the_id, lin or 'form')
    if os.path.exists(filename):
        server.the_file.write(utilities.read_file(filename).encode('ascii'))

plugin.Plugin('annotator_load', '/{Y}/{S}/{U}/annotator_load/{*}',
              function=annotator_load, launch_thread=True,
              mimetype='text/plain',
              priority=-10 # Before student_redirection
             )

class Annotations:
    def __init__(self):
        self.states = [['', '', '', 0, 0, 0, 1]] # The authors/text/colors/x/y/h/sy of the graphic elements
        self.translate = 0
    def add(self, line):
        if self.translate and line[2] >= 1000000:
            line[2] -= self.translate
        action, _timestamp, obj_id, attr, user = line
        if action == "new":
            if len(self.states) != obj_id:
                if obj_id == 1000000:
                    self.translate = 1000000 - len(self.states)
                    obj_id -= self.translate
                    line[2] -= self.translate
                else:
                    raise ValueError('ID Hacker: ' + repr(line))
            self.states.append([user, '', '', 0, 0, 0, 1])
        s = self.states[obj_id]
        if user != s[0]:
            raise ValueError('Object Hacker: ' + repr(line))
        if s[2] == 'del':
            return None # Modification on a deleted object: may happen with 2 windows
        if action == "del":
            s[1] = ''
            s[2] = 'del'
            return True
        if 'backgroundColor' in attr:
            s[2] = attr['backgroundColor']
        if 'left' in attr:
            s[3] = attr['left']
        if 'top' in attr:
            s[4] = attr['top']
        if 'height' in attr:
            s[5] = attr['height']
        if 'scaleY' in attr:
            s[6] = attr['scaleY']
        if 'text' in attr and s[2] != '': # Only teacher texts
            s[1] = attr['text']
        return True
    def grade(self):
        points = 0
        smileys = collections.defaultdict(int)
        for infos in self.states:
            color = infos[2]
            if color:
                text = infos[1]
                try:
                    points += float(text)
                except ValueError:
                    if len(text) == 1 and color.startswith('#FFF'):
                        smileys[text] += 1
        return points, ''.join(sorted(k*v for k, v in smileys.items()))

def annotator_record(server):
    err = document.get_cell_from_table(server, ('Annotate',))
    if isinstance(err, str):
        server.the_file.write(err)
        utilities.send_backtrace(err, 'Annotate:Access')
        return
    table, page, column, lin = err
    filename = os.path.join('UPLOAD', str(server.the_year), server.the_semester,
                            server.the_ue, column.the_id, lin or 'form')
    if lin not in table.lines and not table.authorized_column(server.ticket.user_name, column): # pragma: no cover
        utilities.send_backtrace(err, 'Annotate:Access2')
        return
    annotations = Annotations()
    with open(filename, 'a+') as f:
        try:
            fcntl.flock(f.fileno(), fcntl.LOCK_EX)
            f.seek(0)
            # Load yet recorded history
            for line in f:
                if not line:
                    continue # pragma: no cover
                annotations.add(json.loads(line))

            # Load modification
            lines = []
            content = server.uploaded.getfirst("content")
            user = server.ticket.user_name
            for line in json.loads(content):
                if not server.ticket.is_a_teacher and 'backgroundColor' in line[3]:
                    # Students are not allowed to change background colors
                    raise ValueError('Color Hacker')
                if len(line) != 4:
                    raise ValueError(f"Hacker ? {line}")
                line.append(user)
                if annotations.add(line):
                    lines.append(line)

            # Record changes
            if lines:
                for line in lines:
                    f.write(json.dumps(line) + '\n')
        finally:
            fcntl.flock(f.fileno(), fcntl.LOCK_UN)

    if lin and lines:
        if server.ticket.is_a_teacher:
            points, smileys = annotations.grade()
        else:
            points = configuration.pre
            smileys = None
        table.lock()
        try:
            cell = table.lines[lin][column.data_col]
            if cell.value != points:
                table.cell_change(page, column.the_id, lin, points)
            if smileys is not None:
                new_comment = cell.comment.split(' ')[0] + ' ' + smileys
                if cell.comment != new_comment:
                    table.comment_change(page, column.the_id, lin, new_comment)
        finally:
            table.unlock()
    server.the_file.write('OK!')


plugin.Plugin('annotator_record', '/{Y}/{S}/{U}/annotator_record/{*}',
              function=annotator_record, launch_thread=True,
              upload_max_size=1000000,
              priority=-10 # Before student_redirection
             )

def annotator_form(server):
    """Generate PDF forms"""
    dirname = os.path.join('UPLOAD', str(server.the_year), server.the_semester,
                           server.the_ue, server.something)
    filename = os.path.join(dirname, 'form')
    if not os.path.exists(filename):
        from TOMUSS.ATTRIBUTES.columnuploadzip import upload_zip
        server.the_path = [server.something, *server.the_path]
        return upload_zip(server, png=True)

    table = document.table(server.the_year, server.the_semester, server.the_ue, create=False)
    if not table or not table.modifiable: # pragma: no cover
        utilities.send_backtrace('TABLE???')
        return None
    column = table.columns.from_id(server.something)
    if not column: # pragma: no cover
        utilities.send_backtrace('COL???')
        return None

    annotations = Annotations()
    with open(filename, 'r', encoding='utf-8') as f:
        for line in f:
            if not line:
                continue # pragma: no cover
            annotations.add(json.loads(line))
    cols = []
    for user, col_name, color, left, top, height, scale_y in annotations.states:
        if not user or not col_name:
            continue
        col = table.columns.from_title(col_name)
        cols.append((col, color, left, top, height, scale_y))

    background = PdfFileReader(os.path.join(dirname, 'A.pdf'))

    img = PIL.Image.open(os.path.join(dirname, 'A.png'))
    height = int(1000 * img.height / len(background.pages) / img.width)
    del img
    # Assume A4 sheet
    scale_h = 21 / 2.54 * 72 / 1000
    scale_v1000 = 29.7 / 2.54 * 72
    scale_v = scale_v1000 / 1000

    zip_fildes, zip_name = tempfile.mkstemp()
    zf = zipfile.ZipFile(os.fdopen(zip_fildes, "wb"),
                         mode="w", compression=zipfile.ZIP_DEFLATED)
    try:
        for line_id in server.the_path[:-1]:
            output = PdfFileWriter()
            line = table.lines[line_id]
            for i, page in enumerate(background.pages):
                foreground = io.BytesIO()
                my_canvas = canvas.Canvas(foreground)
                for col, color, left, top, rect_h, scale_y in cols:
                    if int(top//height) != i:
                        continue # Not on the good page
                    top = top % height
                    top += rect_h * scale_y * 0.85 # Compensate base line
                    font_size = scale_h * rect_h * scale_y
                    coord_x = scale_h * left
                    coord_y = scale_v * (height - top)
                    coord_y = scale_v1000 * (1 - top/height)
                    my_canvas.setFont("Helvetica", font_size)
                    for text in str(line[col.data_col].value).split('\n'):
                        my_canvas.drawString(coord_x, coord_y, text)
                        coord_y -= font_size * 1.1
                my_canvas.showPage()
                my_canvas.save()
                foreground.seek(0)
                output.addBlankPage(width=page.mediaBox.getWidth(),
                                    height=page.mediaBox.getHeight()
                                   )
                new_page = output.getPage(i)
                new_page.mergePage(page)
                new_page.mergePage(PdfFileReader(foreground).getPage(0))

            with io.BytesIO() as merged:
                output.write(merged)
                merged.seek(0)
                zf.writestr(f'TOMUSS/{line[0].value}#{line[1].value.title()}_{line[2].value}.pdf', merged.read())
        zf.close()
        with open(zip_name, "rb") as file:
            while True:
                data = file.read(65536)
                if not data:
                    break
                else:
                    server.the_file.write(data)
    finally:
        os.unlink(zip_name)
        del zf

plugin.Plugin('annotator_form', '/{Y}/{S}/{U}/annotator_form/{?}/{*}',
              function=annotator_form, launch_thread=True,
              mimetype='application/zip',
             )
