# -*- coding: utf-8 -*-

# Copyright © 2018, 2019 Damir Jelić <poljar@termina.org.uk>
#
# Permission to use, copy, modify, and/or distribute this software for
# any purpose with or without fee is hereby granted, provided that the
# above copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""Module implementing upload functionality."""

from __future__ import unicode_literals

import attr
import time
import json
from typing import Dict, Any
from uuid import uuid1, UUID
from enum import Enum

try:
    from json.decoder import JSONDecodeError
except ImportError:
    JSONDecodeError = ValueError  # type: ignore

from .globals import SCRIPT_NAME, SERVERS, W, UPLOADS
from .utf import utf8_decode
from .message_renderer import Render
from matrix import globals as G
from nio import Api


class UploadState(Enum):
    created = 0
    active = 1
    finished = 2
    error = 3
    aborted = 4


@attr.s
class Proxy(object):
    ptr = attr.ib(type=str)

    @property
    def name(self):
        return W.infolist_string(self.ptr, "name")

    @property
    def address(self):
        return W.infolist_string(self.ptr, "address")

    @property
    def type(self):
        return W.infolist_string(self.ptr, "type_string")

    @property
    def port(self):
        return str(W.infolist_integer(self.ptr, "port"))

    @property
    def user(self):
        return W.infolist_string(self.ptr, "username")

    @property
    def password(self):
        return W.infolist_string(self.ptr, "password")


@attr.s
class Upload(object):
    """Class representing an upload to a matrix server."""

    server_name = attr.ib(type=str)
    server_address = attr.ib(type=str)
    access_token = attr.ib(type=str)
    room_id = attr.ib(type=str)
    filepath = attr.ib(type=str)
    encrypt = attr.ib(type=bool, default=False)
    file_keys = attr.ib(type=Dict, default=None)

    done = 0
    total = 0

    uuid = None
    buffer = None
    upload_hook = None
    content_uri = None
    file_name = None
    mimetype = "?"
    state = UploadState.created

    def __attrs_post_init__(self):
        self.uuid = uuid1()
        self.buffer = ""

        server = SERVERS[self.server_name]

        proxy_name = server.config.proxy
        proxy = None
        proxies_list = None

        if proxy_name:
            proxies_list = W.infolist_get("proxy", "", proxy_name)
            if proxies_list:
                W.infolist_next(proxies_list)
                proxy = Proxy(proxies_list)

        process_args = {
            "arg1": self.filepath,
            "arg2": self.server_address,
            "arg3": self.access_token,
            "buffer_flush": "1",
        }

        arg_count = 3

        if self.encrypt:
            arg_count += 1
            process_args["arg{}".format(arg_count)] = "--encrypt"

        if not server.config.ssl_verify:
            arg_count += 1
            process_args["arg{}".format(arg_count)] = "--insecure"

        if proxy:
            arg_count += 1
            process_args["arg{}".format(arg_count)] = "--proxy-type"
            arg_count += 1
            process_args["arg{}".format(arg_count)] = proxy.type

            arg_count += 1
            process_args["arg{}".format(arg_count)] = "--proxy-address"
            arg_count += 1
            process_args["arg{}".format(arg_count)] = proxy.address

            arg_count += 1
            process_args["arg{}".format(arg_count)] = "--proxy-port"
            arg_count += 1
            process_args["arg{}".format(arg_count)] = proxy.port

            if proxy.user:
                arg_count += 1
                process_args["arg{}".format(arg_count)] = "--proxy-user"
                arg_count += 1
                process_args["arg{}".format(arg_count)] = proxy.user

            if proxy.password:
                arg_count += 1
                process_args["arg{}".format(arg_count)] = "--proxy-password"
                arg_count += 1
                process_args["arg{}".format(arg_count)] = proxy.password

        self.upload_hook = W.hook_process_hashtable(
            "matrix_upload",
            process_args,
            0,
            "upload_cb",
            str(self.uuid)
        )

        if proxies_list:
            W.infolist_free(proxies_list)

    def abort(self):
        pass

    @property
    def msgtype(self):
        # type: () -> str
        assert self.mimetype
        return Api.mimetype_to_msgtype(self.mimetype)

    @property
    def content(self):
        # type: () -> Dict[Any, Any]
        assert self.content_uri

        if self.encrypt:
            content = {
                "body": self.file_name,
                "msgtype": self.msgtype,
                "file": self.file_keys,
            }
            content["file"]["url"] = self.content_uri
            content["file"]["mimetype"] = self.mimetype

            # TODO thumbnail if it's an image

            return content

        return {
            "msgtype": self.msgtype,
            "body": self.file_name,
            "url": self.content_uri,
        }

    @property
    def render(self):
        # type: () -> str
        assert self.content_uri

        if self.encrypt:
            return Render.encrypted_media(
                self.content_uri,
                self.file_name,
                self.file_keys["key"]["k"],
                self.file_keys["hashes"]["sha256"],
                self.file_keys["iv"],
            )

        return Render.media(self.content_uri, self.file_name)


@attr.s
class UploadsBuffer(object):
    """Weechat buffer showing the uploads for a server."""

    _ptr = ""           # type: str
    _selected_line = 0  # type: int
    uploads = UPLOADS

    def __attrs_post_init__(self):
        self._ptr = W.buffer_new(
            SCRIPT_NAME + ".uploads",
            "",
            "",
            "",
            "",
        )
        W.buffer_set(self._ptr, "type", "free")
        W.buffer_set(self._ptr, "title", "Upload list")
        W.buffer_set(self._ptr, "key_bind_meta2-A", "/uploads up")
        W.buffer_set(self._ptr, "key_bind_meta2-B", "/uploads down")
        W.buffer_set(self._ptr, "localvar_set_type", "uploads")

        self.render()

    def move_line_up(self):
        self._selected_line = max(self._selected_line - 1, 0)
        self.render()

    def move_line_down(self):
        self._selected_line = min(
            self._selected_line + 1,
            len(self.uploads) - 1
        )
        self.render()

    def display(self):
        """Display the buffer."""
        W.buffer_set(self._ptr, "display", "1")

    def render(self):
        """Render the new state of the upload buffer."""
        # This function is under the MIT license.
        # Copyright (c) 2016 Vladimir Ignatev
        def progress(count, total):
            bar_len = 60

            if total == 0:
                bar = '-' * bar_len
                return "[{}] {}%".format(bar, "?")

            filled_len = int(round(bar_len * count / float(total)))
            percents = round(100.0 * count / float(total), 1)
            bar = '=' * filled_len + '-' * (bar_len - filled_len)

            return "[{}] {}%".format(bar, percents)

        W.buffer_clear(self._ptr)
        header = "{}{}{}{}{}{}{}{}".format(
            W.color("green"),
            "Actions (letter+enter):",
            W.color("lightgreen"),
            "  [A] Accept",
            "  [C] Cancel",
            "  [R] Remove",
            "  [P] Purge finished",
            "  [Q] Close this buffer"
        )
        W.prnt_y(self._ptr, 0, header)

        for line_number, upload in enumerate(self.uploads.values()):
            line_color = "{},{}".format(
                "white" if line_number == self._selected_line else "default",
                "blue" if line_number == self._selected_line else "default",
            )
            first_line = ("%s%s %-24s %s%s%s %s (%s.%s)" % (
                          W.color(line_color),
                          "*** " if line_number == self._selected_line else "    ",
                          upload.room_id,
                          "\"",
                          upload.filepath,
                          "\"",
                          upload.mimetype,
                          SCRIPT_NAME,
                          upload.server_name,
                          ))
            W.prnt_y(self._ptr, (line_number * 2) + 2, first_line)

            status_color = "{},{}".format("green", "blue")
            status = "{}{}{}".format(
                W.color(status_color),
                upload.state.name,
                W.color(line_color)
            )

            second_line = ("{color}{prefix} {status} {progressbar} "
                           "{done} / {total}").format(
                color=W.color(line_color),
                prefix="*** " if line_number == self._selected_line else "    ",
                status=status,
                progressbar=progress(upload.done, upload.total),
                done=W.string_format_size(upload.done),
                total=W.string_format_size(upload.total))

            W.prnt_y(self._ptr, (line_number * 2) + 3, second_line)


def find_upload(uuid):
    return UPLOADS.get(uuid, None)


def handle_child_message(upload, message):
    if message["type"] == "progress":
        upload.done = message["data"]

    elif message["type"] == "status":
        if message["status"] == "started":
            upload.state = UploadState.active
            upload.total = message["total"]
            upload.mimetype = message["mimetype"]
            upload.file_name = message["file_name"]

        elif message["status"] == "done":
            upload.state = UploadState.finished
            upload.content_uri = message["url"]
            upload.file_keys = message.get("file_keys", None)

            server = SERVERS.get(upload.server_name, None)

            if not server:
                return

            server.room_send_upload(upload)

        elif message["status"] == "error":
            upload.state = UploadState.error

    if G.CONFIG.upload_buffer:
        G.CONFIG.upload_buffer.render()


@utf8_decode
def upload_cb(data, command, return_code, out, err):
    upload = find_upload(UUID(data))

    if not upload:
        return W.WEECHAT_RC_OK

    if return_code == W.WEECHAT_HOOK_PROCESS_ERROR:
        W.prnt("", "Error with command '%s'" % command)
        return W.WEECHAT_RC_OK

    if err != "":
        W.prnt("", "Error with command '%s'" % err)
        upload.state = UploadState.error

    if out != "":
        upload.buffer += out
        messages = upload.buffer.split("\n")
        upload.buffer = ""

        for m in messages:
            try:
                message = json.loads(m)
            except (JSONDecodeError, TypeError):
                upload.buffer += m
                continue

            handle_child_message(upload, message)

    return W.WEECHAT_RC_OK
