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

# Copyright © 2018 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.

from __future__ import unicode_literals
from builtins import str

from pprint import pformat

from collections import namedtuple
from datetime import datetime

from matrix.globals import W, OPTIONS
from matrix.plugin_options import RedactType

from matrix.colors import Formatted
from matrix.utils import (
    strip_matrix_server, color_for_tags, server_ts_to_weechat,
    sender_to_nick_and_color, add_event_tags, sanitize_id, sanitize_ts,
    sanitize_text, shorten_sender, add_user_to_nicklist, get_prefix_for_level,
    sanitize_power_level, string_strikethrough,
    line_pointer_and_tags_from_event, sender_to_prefix_and_color)

PowerLevel = namedtuple('PowerLevel', ['user', 'level'])


class MatrixRoom:

    def __init__(self, room_id):
        # type: (str) -> None
        # yapf: disable
        self.room_id = room_id        # type: str
        self.alias = room_id          # type: str
        self.topic = ""               # type: str
        self.topic_author = ""        # type: str
        self.topic_date = None        # type: datetime.datetime
        self.prev_batch = ""          # type: str
        self.users = dict()           # type: Dict[str, MatrixUser]
        self.encrypted = False        # type: bool
        self.backlog_pending = False  # type: bool
        # yapf: enable


class MatrixUser:

    def __init__(self, name, display_name):
        # yapf: disable
        self.name = name                  # type: str
        self.display_name = display_name  # type: str
        self.power_level = 0              # type: int
        self.nick_color = ""              # type: str
        self.prefix = ""                  # type: str
        # yapf: enable


def matrix_create_room_buffer(server, room_id):
    # type: (MatrixServer, str) -> None
    buf = W.buffer_new(room_id, "room_input_cb", server.name, "room_close_cb",
                       server.name)

    W.buffer_set(buf, "localvar_set_type", 'channel')
    W.buffer_set(buf, "type", 'formatted')

    W.buffer_set(buf, "localvar_set_channel", room_id)

    W.buffer_set(buf, "localvar_set_nick", server.user)

    W.buffer_set(buf, "localvar_set_server", server.name)

    short_name = strip_matrix_server(room_id)
    W.buffer_set(buf, "short_name", short_name)

    W.nicklist_add_group(buf, '', "000|o", "weechat.color.nicklist_group", 1)
    W.nicklist_add_group(buf, '', "001|h", "weechat.color.nicklist_group", 1)
    W.nicklist_add_group(buf, '', "002|v", "weechat.color.nicklist_group", 1)
    W.nicklist_add_group(buf, '', "999|...", "weechat.color.nicklist_group", 1)

    W.buffer_set(buf, "nicklist", "1")
    W.buffer_set(buf, "nicklist_display_groups", "0")

    # TODO make this configurable
    W.buffer_set(buf, "highlight_tags_restrict", "matrix_message")

    server.buffers[room_id] = buf
    server.rooms[room_id] = MatrixRoom(room_id)


class RoomInfo():

    def __init__(self, room_id, prev_batch, membership_events, events):
        # type: (str, str, List[Any], List[Any]) -> None
        self.room_id = room_id
        self.prev_batch = prev_batch
        self.membership_events = membership_events
        self.events = events

    @staticmethod
    def _message_from_event(event):
        # The transaction id will only be present for events that are send out
        # from this client, since we print out our own messages as soon as we
        # get a receive confirmation from the server we don't care about our
        # own messages in a sync event. More info under:
        # https://github.com/matrix-org/matrix-doc/blob/master/api/client-server/definitions/event.yaml#L53
        if "transaction_id" in event["unsigned"]:
            return None

        if "redacted_by" in event["unsigned"]:
            return RoomRedactedMessageEvent.from_dict(event)

        return RoomMessageEvent.from_dict(event)

    @staticmethod
    def _membership_from_dict(event_dict):
        if (event_dict["content"]["membership"] not in [
                "invite", "join", "knock", "leave", "ban"
        ]):
            raise ValueError

        if event_dict["content"]["membership"] == "join":
            event = RoomMemberJoin.from_dict(event_dict)

            try:
                message = RoomMembershipMessage(
                    event.event_id, event.sender, event.age,
                    "has joined", "join")
                return event, message
            except AttributeError:
                return event, None

        elif event_dict["content"]["membership"] == "leave":
            event = RoomMemberLeave.from_dict(event_dict)

            try:
                msg = ("has left" if event.sender == event.leaving_user else
                       "has been kicked")
                message = RoomMembershipMessage(
                    event.event_id, event.leaving_user, event.timestamp, msg, "quit")
                return event, message
            except AttributeError:
                return event, None

        return None, None

    @staticmethod
    def _parse_events(parsed_dict):
        membership_events = []
        other_events = []

        try:
            for event in parsed_dict:
                if event["type"] == "m.room.message":
                    other_events.append(RoomInfo._message_from_event(event))
                elif event["type"] == "m.room.member":
                    m_event, msg = RoomInfo._membership_from_dict(event)
                    membership_events.append(m_event)
                    if msg:
                        other_events.append(msg)
                elif event["type"] == "m.room.power_levels":
                    other_events.append(RoomPowerLevels.from_dict(event))
                elif event["type"] == "m.room.topic":
                    other_events.append(RoomTopicEvent.from_dict(event))
                elif event["type"] == "m.room.redaction":
                    other_events.append(RoomRedactionEvent.from_dict(event))
                elif event["type"] == "m.room.name":
                    other_events.append(RoomNameEvent.from_dict(event))
                elif event["type"] == "m.room.encryption":
                    other_events.append(RoomEncryptionEvent.from_dict(event))
        except (ValueError, TypeError, KeyError) as error:
            message = ("{prefix}matrix: Error parsing "
                       "room event of type {type}: {error}").format(
                           prefix=W.prefix("error"),
                           type=event["type"],
                           error=pformat(error))
            W.prnt("", message)
            raise

        return (membership_events, other_events)

    @classmethod
    def from_dict(cls, room_id, parsed_dict):
        prev_batch = sanitize_id(parsed_dict['timeline']['prev_batch'])

        state_dict = parsed_dict['state']['events']
        timeline_dict = parsed_dict['timeline']['events']

        membership_events, other_events = RoomInfo._parse_events(state_dict)
        timeline_member_events, timeline_events = RoomInfo._parse_events(
            timeline_dict)

        membership_events.extend(timeline_member_events)
        other_events.extend(timeline_events)

        return cls(room_id, prev_batch, list(filter(None, membership_events)),
                   list(filter(None, other_events)))


class RoomEvent():

    def __init__(self, event_id, sender, timestamp):
        self.event_id = event_id
        self.sender = sender
        self.timestamp = timestamp


class RoomRedactedMessageEvent(RoomEvent):

    def __init__(self, event_id, sender, timestamp, censor, reason=None):
        self.censor = censor
        self.reason = reason
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event):
        event_id = sanitize_id(event["event_id"])
        sender = sanitize_id(event["sender"])
        timestamp = sanitize_ts(event["origin_server_ts"])

        censor = sanitize_id(event['unsigned']['redacted_because']['sender'])
        reason = None

        if 'reason' in event['unsigned']['redacted_because']['content']:
            reason = sanitize_text(
                event['unsigned']['redacted_because']['content']['reason'])

        return cls(event_id, sender, timestamp, censor, reason)

    def execute(self, server, room, buff, tags):
        nick, color_name = sender_to_nick_and_color(room, self.sender)
        color = color_for_tags(color_name)
        date = server_ts_to_weechat(self.timestamp)

        event_tags = add_event_tags(self.event_id, nick, color, tags)

        reason = (", reason: \"{reason}\"".format(reason=self.reason)
                  if self.reason else "")

        censor, _ = sender_to_nick_and_color(room, self.censor)

        msg = ("{del_color}<{log_color}Message redacted by: "
               "{censor}{log_color}{reason}{del_color}>{ncolor}").format(
                   del_color=W.color("chat_delimiters"),
                   ncolor=W.color("reset"),
                   log_color=W.color("logger.color.backlog_line"),
                   censor=censor,
                   reason=reason)

        event_tags.append("matrix_redacted")

        tags_string = ",".join(event_tags)

        data = "{author}\t{msg}".format(author=nick, msg=msg)

        W.prnt_date_tags(buff, date, tags_string, data)


class RoomMessageEvent(RoomEvent):

    @classmethod
    def from_dict(cls, event):
        if event['content']['msgtype'] == 'm.text':
            return RoomMessageText.from_dict(event)
        elif event['content']['msgtype'] == 'm.image':
            return RoomMessageMedia.from_dict(event)
        elif event['content']['msgtype'] == 'm.audio':
            return RoomMessageMedia.from_dict(event)
        elif event['content']['msgtype'] == 'm.file':
            return RoomMessageMedia.from_dict(event)
        elif event['content']['msgtype'] == 'm.video':
            return RoomMessageMedia.from_dict(event)
        elif event['content']['msgtype'] == 'm.emote':
            return RoomMessageEmote.from_dict(event)
        elif event['content']['msgtype'] == 'm.notice':
            return RoomMessageNotice.from_dict(event)
        return RoomMessageUnknown.from_dict(event)

    def _print_message(self, message, room, buff, tags):
        nick, color_name = sender_to_nick_and_color(room, self.sender)
        color = color_for_tags(color_name)

        event_tags = add_event_tags(self.event_id, nick, color, tags)

        tags_string = ",".join(event_tags)

        prefix, prefix_color = sender_to_prefix_and_color(room, self.sender)

        prefix_string = ("" if not prefix else "{}{}{}".format(
            W.color(prefix_color), prefix, W.color("reset")))

        data = "{prefix}{color}{author}{ncolor}\t{msg}".format(
            prefix=prefix_string,
            color=W.color(color_name),
            author=nick,
            ncolor=W.color("reset"),
            msg=message)

        date = server_ts_to_weechat(self.timestamp)
        W.prnt_date_tags(buff, date, tags_string, data)


class RoomMessageSimple(RoomMessageEvent):

    def __init__(self, event_id, sender, timestamp, message, message_type):
        self.message = message
        self.message_type = message_type
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event):
        event_id = sanitize_id(event["event_id"])
        sender = sanitize_id(event["sender"])
        timestamp = sanitize_ts(event["origin_server_ts"])

        message = sanitize_text(event["content"]["body"])
        message_type = sanitize_text(event["content"]["msgtype"])

        return cls(event_id, sender, timestamp, message, message_type)


class RoomMessageUnknown(RoomMessageSimple):

    def execute(self, server, room, buff, tags):
        msg = ("Unknown message of type {t}, body: {body}").format(
            t=self.message_type, body=self.message)

        self._print_message(msg, room, buff, tags)


class RoomMessageText(RoomMessageEvent):

    def __init__(self, event_id, sender, timestamp, message, formatted_message=None):
        self.message = message
        self.formatted_message = formatted_message
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event):
        event_id = sanitize_id(event["event_id"])
        sender = sanitize_id(event["sender"])
        timestamp = sanitize_ts(event["origin_server_ts"])

        msg = ""
        formatted_msg = None

        msg = sanitize_text(event['content']['body'])

        if ('format' in event['content'] and
                'formatted_body' in event['content']):
            if event['content']['format'] == "org.matrix.custom.html":
                formatted_msg = Formatted.from_html(
                    sanitize_text(event['content']['formatted_body']))

        return cls(event_id, sender, timestamp, msg, formatted_msg)

    def execute(self, server, room, buff, tags):
        msg = (self.formatted_message.to_weechat()
               if self.formatted_message else self.message)

        self._print_message(msg, room, buff, tags)


class RoomMessageEmote(RoomMessageSimple):

    def execute(self, server, room, buff, tags):
        nick, color_name = sender_to_nick_and_color(room, self.sender)
        color = color_for_tags(color_name)

        event_tags = add_event_tags(self.event_id, nick, color, tags)
        event_tags.append("matrix_action")

        tags_string = ",".join(event_tags)

        data = "{prefix}{nick_color}{author}{ncolor} {msg}".format(
            prefix=W.prefix("action"),
            nick_color=W.color(color_name),
            author=nick,
            ncolor=W.color("reset"),
            msg=self.message)

        date = server_ts_to_weechat(self.timestamp)
        W.prnt_date_tags(buff, date, tags_string, data)


class RoomMessageNotice(RoomMessageText):

    def execute(self, server, room, buff, tags):
        msg = "{color}{message}{ncolor}".format(
            color=W.color("irc.color.notice"),
            message=self.message,
            ncolor=W.color("reset"))

        self._print_message(msg, room, buff, tags)


class RoomMessageMedia(RoomMessageEvent):

    def __init__(self, event_id, sender, timestamp, url, description):
        self.url = url
        self.description = description
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event):
        event_id = sanitize_id(event["event_id"])
        sender = sanitize_id(event["sender"])
        timestamp = sanitize_ts(event["origin_server_ts"])

        mxc_url = sanitize_text(event['content']['url'])
        description = sanitize_text(event["content"]["body"])

        return cls(event_id, sender, timestamp, mxc_url, description)

    def execute(self, server, room, buff, tags):
        http_url = server.client.mxc_to_http(self.url)
        url = http_url if http_url else self.url

        description = (" ({})".format(self.description)
                       if self.description else "")

        msg = "{url}{desc}".format(url=url, desc=description)

        self._print_message(msg, room, buff, tags)


class RoomMembershipMessage(RoomEvent):
    def __init__(self, event_id, sender, timestamp, message, prefix):
        self.message = message
        self.prefix = prefix
        RoomEvent.__init__(self, event_id, sender, timestamp)

    def execute(self, server, room, buff, tags):
        nick, color_name = sender_to_nick_and_color(room, self.sender)
        event_tags = add_event_tags(self.event_id, nick, None, [])
        # TODO this should be configurable
        action_color = "red" if self.prefix == "quit" else "green"

        data = ("{prefix}{color}{author}{ncolor} "
                "{del_color}({host_color}{user_id}{del_color})"
                "{action_color} {message} "
                "{channel_color}{room}{ncolor}").format(
            prefix=W.prefix(self.prefix),
            color=W.color(color_name),
            author=nick,
            ncolor=W.color("reset"),
            del_color=W.color("chat_delimiters"),
            host_color=W.color("chat_host"),
            user_id=self.sender,
            action_color=W.color(action_color),
            message=self.message,
            channel_color=W.color("chat_channel"),
            room=room.alias)
        date = server_ts_to_weechat(self.timestamp)
        tags_string = ",".join(event_tags)

        W.prnt_date_tags(buff, date, tags_string, data)


class RoomMemberJoin(RoomEvent):

    def __init__(self, event_id, sender, timestamp, display_name):
        self.display_name = display_name
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event_dict):
        event_id = sanitize_id(event_dict["event_id"])
        sender = sanitize_id(event_dict["sender"])
        timestamp = sanitize_ts(event_dict["origin_server_ts"])
        display_name = None

        if event_dict["content"]:
            if "display_name" in event_dict["content"]:
                display_name = sanitize_text(
                    event_dict["content"]["displayname"])

        return cls(event_id, sender, timestamp, display_name)

    def execute(self, server, room, buff, tags):
        short_name = shorten_sender(self.sender)

        if self.sender in room.users:
            user = room.users[self.sender]
            if self.display_name:
                user.display_name = self.display_name
        else:
            user = MatrixUser(short_name, self.display_name)

        if not user.nick_color:
            if self.sender == server.user_id:
                highlight_words = [self.sender, user.name]

                if self.display_name:
                    highlight_words.append(self.display_name)

                user.nick_color = "weechat.color.chat_nick_self"
                W.buffer_set(buff, "highlight_words", ",".join(highlight_words))
            else:
                user.nick_color = W.info_get("nick_color_name", user.name)

        room.users[self.sender] = user

        nick_pointer = W.nicklist_search_nick(buff, "", self.sender)

        if not nick_pointer:
            add_user_to_nicklist(buff, self.sender, user)


class RoomMemberLeave(RoomEvent):

    def __init__(self, event_id, sender, leaving_user, timestamp):
        self.leaving_user = leaving_user
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event_dict):
        event_id = sanitize_id(event_dict["event_id"])
        sender = sanitize_id(event_dict["sender"])
        leaving_user = sanitize_id(event_dict["state_key"])
        timestamp = sanitize_ts(event_dict["origin_server_ts"])

        return cls(event_id, sender, leaving_user, timestamp)

    def execute(self, server, room, buff, tags):
        if self.leaving_user in room.users:
            nick_pointer = W.nicklist_search_nick(buff, "", self.leaving_user)

            if nick_pointer:
                W.nicklist_remove_nick(buff, nick_pointer)

            del room.users[self.leaving_user]


class RoomPowerLevels(RoomEvent):

    def __init__(self, event_id, sender, timestamp, power_levels):
        self.power_levels = power_levels
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event_dict):
        event_id = sanitize_id(event_dict["event_id"])
        sender = sanitize_id(event_dict["sender"])
        timestamp = sanitize_ts(event_dict["origin_server_ts"])
        power_levels = []

        for user, level in event_dict["content"]["users"].items():
            power_levels.append(
                PowerLevel(sanitize_id(user), sanitize_power_level(level)))

        return cls(event_id, sender, timestamp, power_levels)

    def _set_power_level(self, room, buff, power_level):
        user_id = power_level.user
        level = power_level.level

        if user_id not in room.users:
            return

        user = room.users[user_id]
        user.power_level = level
        user.prefix = get_prefix_for_level(level)

        nick_pointer = W.nicklist_search_nick(buff, "", user_id)

        if nick_pointer:
            W.nicklist_remove_nick(buff, nick_pointer)
            add_user_to_nicklist(buff, user_id, user)

    def execute(self, server, room, buff, tags):
        for level in self.power_levels:
            self._set_power_level(room, buff, level)


class RoomTopicEvent(RoomEvent):

    def __init__(self, event_id, sender, timestamp, topic):
        self.topic = topic
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event_dict):
        event_id = sanitize_id(event_dict["event_id"])
        sender = sanitize_id(event_dict["sender"])
        timestamp = sanitize_ts(event_dict["origin_server_ts"])

        topic = sanitize_text(event_dict["content"]["topic"])

        return cls(event_id, sender, timestamp, topic)

    def execute(self, server, room, buff, tags):
        topic = self.topic

        nick, color_name = sender_to_nick_and_color(room, self.sender)

        author = ("{nick_color}{user}{ncolor}").format(
            nick_color=W.color(color_name), user=nick, ncolor=W.color("reset"))

        # TODO print old topic if configured so
        message = ("{prefix}{nick} has changed "
                   "the topic for {chan_color}{room}{ncolor} "
                   "to \"{topic}\"").format(
                       prefix=W.prefix("network"),
                       nick=author,
                       chan_color=W.color("chat_channel"),
                       ncolor=W.color("reset"),
                       room=strip_matrix_server(room.alias),
                       topic=topic)

        tags = ["matrix_topic", "log3", "matrix_id_{}".format(self.event_id)]

        date = server_ts_to_weechat(self.timestamp)

        W.buffer_set(buff, "title", topic)
        W.prnt_date_tags(buff, date, ",".join(tags), message)

        room.topic = topic
        room.topic_author = self.sender
        room.topic_date = datetime.fromtimestamp(
            server_ts_to_weechat(self.timestamp))


class RoomRedactionEvent(RoomEvent):

    def __init__(self, event_id, sender, timestamp, redaction_id, reason=None):
        self.redaction_id = redaction_id
        self.reason = reason
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event_dict):
        event_id = sanitize_id(event_dict["event_id"])
        sender = sanitize_id(event_dict["sender"])
        timestamp = sanitize_ts(event_dict["origin_server_ts"])

        redaction_id = sanitize_id(event_dict["redacts"])

        reason = (sanitize_text(event_dict["content"]["reason"])
                  if "reason" in event_dict["content"] else None)

        return cls(event_id, sender, timestamp, redaction_id, reason)

    @staticmethod
    def already_redacted(tags):
        if "matrix_redacted" in tags:
            return True
        return False

    def _redact_line(self, data_pointer, tags, room, buff):
        hdata_line_data = W.hdata_get('line_data')

        message = W.hdata_string(hdata_line_data, data_pointer, 'message')
        censor, _ = sender_to_nick_and_color(room, self.sender)

        reason = ("" if not self.reason else
                  ", reason: \"{reason}\"".format(reason=self.reason))

        redaction_msg = ("{del_color}<{log_color}Message redacted by: "
                         "{censor}{log_color}{reason}{del_color}>"
                         "{ncolor}").format(
                             del_color=W.color("chat_delimiters"),
                             ncolor=W.color("reset"),
                             log_color=W.color("logger.color.backlog_line"),
                             censor=censor,
                             reason=reason)

        new_message = ""

        if OPTIONS.redaction_type == RedactType.STRIKETHROUGH:
            new_message = string_strikethrough(message)
        elif OPTIONS.redaction_type == RedactType.NOTICE:
            new_message = message
        elif OPTIONS.redaction_type == RedactType.DELETE:
            pass

        message = " ".join(s for s in [new_message, redaction_msg] if s)

        tags.append("matrix_redacted")

        new_data = {'tags_array': ','.join(tags), 'message': message}

        W.hdata_update(hdata_line_data, data_pointer, new_data)

    def execute(self, server, room, buff, tags):
        data_pointer, tags = line_pointer_and_tags_from_event(
            buff, self.redaction_id)

        if not data_pointer:
            return

        if RoomRedactionEvent.already_redacted(tags):
            return

        self._redact_line(data_pointer, tags, room, buff)


class RoomNameEvent(RoomEvent):

    def __init__(self, event_id, sender, timestamp, name):
        self.name = name
        RoomEvent.__init__(self, event_id, sender, timestamp)

    @classmethod
    def from_dict(cls, event_dict):
        event_id = sanitize_id(event_dict["event_id"])
        sender = sanitize_id(event_dict["sender"])
        timestamp = sanitize_ts(event_dict["origin_server_ts"])

        name = sanitize_id(event_dict['content']['name'])

        return cls(event_id, sender, timestamp, name)

    def execute(self, server, room, buff, tags):
        if not self.name:
            return

        room.alias = self.name
        W.buffer_set(buff, "name", self.name)
        W.buffer_set(buff, "short_name", self.name)
        W.buffer_set(buff, "localvar_set_channel", self.name)


class RoomEncryptionEvent(RoomEvent):

    @classmethod
    def from_dict(cls, event_dict):
        event_id = sanitize_id(event_dict["event_id"])
        sender = sanitize_id(event_dict["sender"])
        timestamp = sanitize_ts(event_dict["origin_server_ts"])

        return cls(event_id, sender, timestamp)

    def execute(self, server, room, buff, tags):
        room.encrypted = True

        message = ("{prefix}This room is encrypted, encryption is "
                   "currently unsuported. Message sending is disabled for "
                   "this room.").format(prefix=W.prefix("error"))

        W.prnt(buff, message)