#!/bin/env python3
# -*- coding: utf-8 -*-
#    TOMUSS: The Online Multi User Simple Spreadsheet
#    Copyright (C) 2015 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

import os
import subprocess
import html
import json
import re
from .. import data
from .. import utilities
from .. import plugin
from .. import document
from .. import ticket
from .. import configuration
from . import text

def container_path(column):
    return os.path.join('UPLOAD', str(column.table.year),
                        column.table.semester, column.table.ue,
                        column.the_id)

configuration.container_path = container_path

def length(stream):
    stream.seek(0, 2)
    size = stream.tell()
    stream.seek(0)
    return size

class Upload(text.Text):
    type_type = 'data'
    attributes_visible = ('rounding', 'weight', 'upload_max', 'upload_zip',
                          'groupcolumn', 'import_zip', 'url_title')
    ondoubleclick = 'upload_double_click'
    formatte = 'function(v, column) { if ( column.rounding === "" && v.toFixed ) return v.toFixed(3) ; else return column.do_rounding(v) ; }'
    formatte_suivi = 'upload_format_suivi'
    human_priority = 20
    tip_cell = "TIP_cell_Upload"

class HackClamd(bytearray): # pragma: no cover
    """Pyclamd does not allow to use an open file"""
    def __init__(self, stream):
        self.stream = stream
        self.length = length(stream)
    def __getitem__(self, item):
        if item.stop is None:
            # pyclamd 0.3.9
            return self
        if self.length == 0:
            return b''
        start = item.start or 0
        x = self.stream.read(min(self.length, item.stop - start))
        self.length -= len(x)
        return x
    def __len__(self):
        return self.length

def check_virus(data): # pragma: no cover
    utilities.warn("SCAN: INIT")
    try:
        import pyclamd
        try:
            if configuration.clamav_server == '127.0.0.1':
                pc = pyclamd.ClamdUnixSocket()
            else:
                pc = pyclamd.ClamdNetworkSocket(host=configuration.clamav_server,
                                                port=configuration.clamav_port)
        except NameError:
            # Fix a pyclamd bug
            pyclamd.__dict__["__builtins__"]["basestring"] = str
            pc = pyclamd.ClamdUnixSocket()
    except:
        utilities.send_backtrace("", "CAN'T CONNECT TO CLAMAV")
        return None # Not installed or not running
    utilities.warn("SCAN: START")
    try:
        if isinstance(data, bytes):
            res = pc.scan_stream(data)
        else:
            res = pc.scan_stream(HackClamd(data))
    except:
        utilities.send_backtrace("", "SCAN STREAM FAIL")
        return None # Bug
        
    utilities.warn("SCAN: STOP %s" % res)
    if res:
        return repr(res)
    else:
        return ''

def copy_stream(instream, outstream, start=None, end=4000000000):
    if start is not None:
        instream.seek(start)
        n = end - start
        for _ in range(n // 4096):
            outstream.write(instream.read(4096))
        outstream.write(instream.read(n % 4096))
    else:
        n = 0
        while True:
            a = instream.read(4096)
            if a == b"":
                break
            n += len(a)
            outstream.write(a)
        outstream.close()
    return n

def nice_txt(txt):
    """Protect < > & but add formatting"""
    return re.sub(
        '(https?:[-._a-zA-Z0-9/]*)', r'<a target="_blank" href="\1">\1</a>',
        html.escape(txt).replace('&lt;br&gt;', '<br>'))

def save_file(server, page, column, lin_id, data, filename, store_png=False):
    # Search a student with yet a downloaded file
    def write(txt):
        try:
            server.the_file.write(txt)
        except BrokenPipeError: # pragma: no cover
            pass
    table = column.table
    line = table.lines.get(lin_id, None)
    cell = line[column.data_col]
    if not table.authorized(page.user_name, cell, column, line,
                            is_a_teacher=server.ticket.is_a_teacher): # pragma: no cover
        # XXX should be in the lock to be perfect.
        # but it must be before uploading the file
        write(server._("ERROR_value_not_modifiable") + '\n')
        return server._("ERROR_value_not_modifiable")
    path = container_path(column)
    write('<br>' + server._("MSG_upload_before") + '\n')
    err = check_virus(data)
    if err: # pragma: no cover
        write('<span style="background:#F00;color:#FFF">%s %s</span>\n'
              % (server._("MSG_virus_found"), html.escape(err)))
        return err
    if err is not None:
        write(server._("MSG_no_virus_found") + '\n') # pragma: no cover
    utilities.mkpath(path, create_init=False)
    file_path = os.path.join(path, lin_id)
    if store_png:
        file_path += '.png'
    if os.path.exists(file_path):
        os.rename(file_path, file_path + '~')

    if isinstance(data, bytes):
        f = open(file_path, "wb")
        n = len(data)
        f.write(data)
        f.close()
    else:
        data.seek(0) # because check_virus read it
        if store_png:
            if configuration.store_stream_in_png(data, file_path): # pragma: no cover
                write(server._("ERROR_PDF_required") + '\n')
                return server._("ERROR_PDF_required")
            else:
                write('PDF → PNG OK\n')
                n = length(data)
        else:
            f = open(file_path, "wb")
            n = copy_stream(data, f)
        data.close() # Free FieldStorage

    if n >= 0:
        write('<br><span>%s %s</span>\n' % (server._("MSG_upload_size"), n))

    magic = subprocess.check_output(["file", "--mime", file_path])
    magic = magic.decode("utf-8").split(": ", 1)[1].strip()
    write('<br><span>%s %s</span>\n'
          % (server._("MSG_upload_type"), html.escape(magic)))
    table.lock()
    try:
        old_value = cell.value
        old_comment = cell.comment
        if store_png:
            new_comment = filename.replace(' ', '_')
        else:
            new_comment = magic + ' ' + filename
        result = table.comment_change(page, column.the_id, lin_id, new_comment)
        if result != "ok.png":
            utilities.send_backtrace(str(result),
                                     "UPLOAD CELL COMMENT CHANGE FAIL") # pragma: no cover
        # force_update=True because the writable cell check can be: "#="
        table.cell_change(page, column.the_id, lin_id,
                          configuration.pre if store_png else n/1000.,
                          force_update=True)

        # Erase upload of the other students of the group:
        for a_lin_id, a_line in column.lines_of_the_group(line):
            if a_lin_id != lin_id and a_line[column.data_col].comment:
                table.comment_change(page, column.the_id, a_lin_id, '')
                if os.path.exists(os.path.join(path, a_lin_id)):
                    write("<p>%s %s %s" % (
                        server._("MSG_upload_replace"),
                        html.escape(a_line[1].value),
                        html.escape(a_line[2].value)))

    finally:
        table.unlock()
    cell = line[column.data_col] # XXX because of cell class change
    if cell.value == old_value and cell.comment == old_comment:
        write('<p style="background:#000;color: #FF0;padding:1em">%s</p>' %
              server._("MSG_upload_identical"))
    write('<!--OK--{}--OK-->'.format(utilities.js([n/1000., magic + ' ' + filename])))

    for col in table.columns:
        if col.type.name != 'Analyser':
            continue
        if column.title not in col.get_columns():
            continue
        result = configuration.Analysing(server, True, False, (lin_id,), col).get_result()
        if result == {}:
            continue # Not on upload
        if 'error' in result:
            server.the_file.write('<p>THERE IS A PROBLEM, CONTACT YOUR TEACHER')
            utilities.send_mail_in_background(
                configuration.User(column.author).mail,
                str(column) + ':' + col.type.name + ' BUG',
                result['error'])
            continue
        if lin_id not in result['students']: # pragma: no cover
            server.the_file.write('<p>THERE IS A BUG')
            continue
        grading = json.loads(result['students'][lin_id])
        if 'message_red' in grading:
            server.the_file.write(
                '''<style>@keyframes blink {
                    0% { background: #FF0 }
                    50% { background: #FF0 }
                    51% { background: #F88 }
                    100% { background: #F88 }
                    }</style>
                <div style="border: 4px solid #F00; animation: blink 1s linear infinite">'''
                + nice_txt(grading['message_red'])
                + '</div>')
        if 'message_green' in grading:
            server.the_file.write(
                '<div style="background:#8F8; border: 4px solid #0F0">'
                + nice_txt(grading['message_green'])
                + '</div>')

def upload_post(server):
    """Upload a file associated to a table cell"""
    data = server.uploaded
    if data is None or 'data' not in data: # pragma: no cover
        server.the_file.write(server._('MSG_bad_ticket'))
        return

    err = document.get_cell_from_table(server, ('Upload', 'Annotate'))
    if isinstance(err, str):
        server.the_file.write(err)
        server.close_connection_now()
        return
    table, page, column, lin_id = err
    if column.type.name == 'Annotate':
        annotate = utilities.literal_eval(str(column.annotate))
        if annotate == 1 or not annotate.get('images', ''):
            store_png = True
        else: # pragma: no cover
            server.close_connection_now()
            return
    else:
        store_png = False

    try:
        filename = data.getfirst("filename").replace("\\", "/").split("/")[-1]
        stream = data["data"].file

        server.the_file.write('<p><b>' + html.escape(filename) + '</b>\n')
        size = length(stream)

        if size > float(column.upload_max) * 1000:
            server.the_file.write('<p style="color:red">%s %d &gt; %d\n'
                                  % (server._("MSG_upload_fail_max"),
                                     size, float(column.upload_max)*1000))
            return
        err = save_file(server, page, column, lin_id, stream, filename, store_png)
        if not err:
            server.the_file.write('<p style="background:#8F8">'
                                  + server._("MSG_upload_stop"))
    finally:
        table.do_not_unload_remove('cell_change')

def upload_get_done(server, mime, file_path):
    byte_range = server.headers.get('Range', '')
    if byte_range.startswith('bytes='):
        byte_range = byte_range[6:] # pragma: no cover
    else:
        byte_range = ''
    try:
        f = open(file_path, "rb")
        mime = mime.replace("; ", ";").split(' ')[0].strip().lower()
        server.send_response(200)
        if '+xml;' in mime or 'html' in mime:
            mime = 'application/octet-stream' # pragma: no cover
        if '/' in mime:
            server.send_header('Content-Type', mime)
        size = os.path.getsize(file_path)
        if byte_range:  # pragma: no cover
            # server.send_header("Content-Length", len(body))
            start, end = byte_range.split('-')
            start = int(start)
            end = int(end) if end else size
            server.send_header("Content-Range", "bytes %d-%d/%d" % (start, end, size))
        else:
            start = 0
            end = size
        server.end_headers()
        copy_stream(f, server.the_file, start=start, end=end)
        f.close()
        return True
    except IOError:
        return

def upload_log(server, table, lin_id, column):
    """If a title.log column exists and is writable: write the access log into"""
    if not table.modifiable:
        return # pragma: no cover
    log_column = table.columns.from_title(column.title + '.log')
    if not log_column or log_column.locked:
        return
    if log_column.cell_writable:
        cell_writable = table.get_filter(log_column.cell_writable, log_column.type.name)
        if not cell_writable(table.lines[lin_id],
                             table.lines[lin_id][log_column.data_col],
                             data.no_user, True):
            return # pragma: no cover
    name = server.ticket.user_name + ' '
    nr_downloads = 1
    for value_date_author_type in table.lines[lin_id][log_column.data_col].history:
        if value_date_author_type[3] == 'V':
            if value_date_author_type[0].startswith(name):
                nr_downloads += 1
    name += str(nr_downloads)
    with table.the_lock:
        table.cell_change(table.get_nobody_page(), log_column.the_id, lin_id, name)

def upload_get(server, public=False):
    """Download a file associated to a table cell"""
    if public:
        server.ticket = ticket.Anonymous()
    if server.the_path[1].endswith('~'):
        old_version = "~"
        server.the_path[1] = server.the_path[1][:-1]
    else:
        old_version = ""
    err = document.get_cell_from_table_ro(server, ('Upload', 'Annotate'))
    if isinstance(err, str):
        server.send_response(200)
        server.send_header('Content-Type', 'text/plain; charset=utf-8')
        server.end_headers()
        server.the_file.write(err.encode("utf-8"))
        raise ValueError(err)
    table, column, lin_id = err
    path = container_path(column)
    if column.type.name == 'Annotate':
        suffix = '.png'
    else:
        suffix = ''
    line = table.lines[lin_id]
    lines_to_test = ((lin_id, line),) + tuple(column.lines_of_the_group(line))
    mime = "application/octet-stream"
    for a_lin_id, a_line in lines_to_test:
        if a_line[column.data_col].value == '':
            continue
        if old_version:
            comments = [infos[0]
                        for infos in a_line[column.data_col].history
                        if infos[3] == 'C'
                       ]
            if len(comments) == 1:
                # Twice the same mimetype
                mime = comments[0] # pragma: no cover
            else:
                mime = comments[-2]
        else:
            mime = a_line[column.data_col].comment
        if ';' not in mime:
            continue
        if upload_get_done(server, mime,
                           os.path.join(path, a_lin_id + suffix + old_version)):
            upload_log(server, table, lin_id, column)
            return
    # The uploaded file and the comment are not in the same cell
    for a_lin_id, a_line in lines_to_test:
        if a_line[column.data_col].value == '':
            continue
        if ';' in a_line[column.data_col].comment:
            continue # pragma: no cover
        # Assume the same MIME type found in the previous loop
        if upload_get_done(server, mime, os.path.join(path, a_lin_id + suffix)):
            upload_log(server, table, lin_id, column)
            return

    data = server._("MSG_upload_no_file")
    mime = "text/plain; charset=utf-8"
    server.send_response(200)
    server.send_header('Content-Type', mime)
    server.end_headers()
    server.the_file.write(data.encode("utf-8"))


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

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

plugin.Plugin('upload_get_public', '/{Y}/{S}/{U}/upload_get_public/{*}',
              function=lambda server: upload_get(server, public=True),
              documentation = "Download a file without login",
              launch_thread = True, mimetype=None,
              authenticated = False,
              priority = -10 # Before student_redirection
          )
