"""RequestHandler."""

import bisect
import difflib
import log

from users_text_manager import AUTHORITY
from users_text_manager import UserInfo


class JSON_TOKEN:  # pylint:disable=W0232
    """Enumeration the Ttken strings for json object."""
    BYE = 'bye'  # Resets the user and do nothong.
    CURSORS = 'cursors'  # other users' cursor position
    DIFF = 'diff'  # Difference between this time and last time.
    ERROR = 'error'  # error string
    IDENTITY = 'identity'  # identity of myself
    INIT = 'init'  # initialize connect flag
    MODE = 'mode'  # vim mode.
    NICKNAME = 'nickname'  # nick name of the user.
    OTHERS = 'others'  # other users info.


def apply_patch(orig_lines, patch_info):
    """Applies a patch.

    Args:
        orig_lines: Original lines of the text.
        patch_info: A list of replacing information.

    Return:
        A list of text.
    """
    new_lines, done_len = [], 0
    for beg, end, lines in patch_info:
        new_lines += orig_lines[done_len : beg]
        new_lines += lines
        done_len = end
    return new_lines + orig_lines[done_len : ]


def _squash_patch(patch_info):
    """Squash list of replacing information to a smaller mount of information.

    Args:
        patch_info: Information of patches.

    Return:
        A list of replacing information.
    """
    ret, index = [], 0
    while index < len(patch_info):
        lines = patch_info[index][2]
        index2 = index + 1
        while index2 < len(patch_info) and \
              patch_info[index2 - 1][1] >= patch_info[index2][0]:
            lines += patch_info[index2][2]
            index2 += 1
        ret.append((patch_info[index][0], patch_info[index2 - 1][1], lines))
        index = index2
    return ret


def gen_patch(orig_lines, new_lines):
    """Creates a patch from two lines text.

    Args:
        orig_lines: Original lines of the text.
        new_lines: New lines of the text.

    Return:
        A list of replacing information.
    """
    diff_result = list(difflib.Differ().compare(orig_lines, new_lines))
    orig_index, ret = 0, []
    for line in diff_result:
        if line.startswith('  '):
            orig_index += 1
        elif line.startswith('+ '):
            ret.append((orig_index, orig_index, [line[2 : ]]))
        elif line.startswith('- '):
            ret.append((orig_index, orig_index + 1, []))
            orig_index += 1
    return _squash_patch(ret)


class _CursorTransformer(object):
    """Transformer for format of the cursor position.

    In vim, it use (row, col) to represent an cursor's position.
    In UsersTextManager, it use numerical notation (byte distance between the
    first byte in the text).

    Attributes:
        _sum_len: Sum of each row.
    """
    def __init__(self):
        """Constructor."""
        self._sum_len = [0]

    def update_lines(self, lines):
        """Update lines of text.

        Args:
            lines: Lines of text.
        """
        self._sum_len = [0]
        for line in lines:
            delta = len(line) + 1  # "+ 1" is for newline char
            self._sum_len.append(self._sum_len[-1] + delta)

    def rcs_to_nums(self, rcs):
        """Transform row-col format's cursor position to numerical type.

        Args:
            lines: List of line text.
            rcs: List of tuple of row-col format cursor postions

        Return:
            A list of numerical cursor positions.
        """
        ret = []
        for rc in rcs:
            base = self._sum_len[min(rc[0], len(self._sum_len) - 1)]
            ret.append(min(base + rc[1], self._sum_len[-1]))
        return ret


    def nums_to_rcs(self, nums):
        """Transform numerical format's cursor position to row-col format.

        Args:
            lines: List of line text.
            nums: A list of numerical cursor positions.

        Return:
            List of tuple of row-col format cursor postions
        """
        ans, rmin = {}, 0
        for num in sorted(nums):
            row = bisect.bisect(self._sum_len, num, lo=rmin) - 1
            col = (num - self._sum_len[row]) if row < len(self._sum_len) else 0
            rmin = row + 1
            ans[num] = (row, col)
        return [ans[num] for num in nums]


class RequestHandler(object):
    """Handles all kinds of request.

    Attributes:
        _users_text_manager: An instance of UsersTextManager.
        _cursor_transformer: An instance of _CursorTransformer.
    """
    def __init__(self, users_text_manager):
        """Constructor.

        Args:
            users_text_manager: An instance of UsersTextManager.
        """
        super(RequestHandler, self).__init__()
        self._users_text_manager = users_text_manager
        self._cursor_transformer = _CursorTransformer()

    def handle(self, request):
        """Handles the request and returns the response.

        Args:
            request: The request.

        Return
            The respsonse.
        """
        if JSON_TOKEN.IDENTITY not in request:
            return {JSON_TOKEN.ERROR : 'Bad request.'}
        identity = request[JSON_TOKEN.IDENTITY]
        if identity not in self._users_text_manager.get_users_info():
            return {JSON_TOKEN.ERROR: 'Invalid identity.'}
        for handler in [self._try_handle_leave,
                        self._try_handle_sync]:
            response = handler(identity, request)
            if response is not None:
                break
        else:
            return {JSON_TOKEN.ERROR: 'Bad request.'}
        return response

    def _try_handle_leave(self, identity, request):
        """Trying to handle the leaving operation if it is.

        Args:
            identity: The identity of that user.
            request: The request from that user.
        """
        if JSON_TOKEN.BYE in request:
            self._users_text_manager.reset_user(identity)
            return {}

    def _try_handle_sync(self, identity, request):
        """Trying to handle the sync request if it is, otherwise return None.

        Args:
            identity: The identity of that user.
            request: The request from that user.
        """
        if all(key in request for key in [JSON_TOKEN.INIT, JSON_TOKEN.DIFF,
                                          JSON_TOKEN.MODE, JSON_TOKEN.CURSORS]):
            log.info('handle sync-request from %r\n' % identity)
            self._check_init(identity, request)
            self._check_authority(identity, request)
            lines = apply_patch(
                self._users_text_manager.get_user_text(identity).split('\n'),
                request[JSON_TOKEN.DIFF])
            self._cursor_transformer.update_lines(lines)
            cursors = dict(zip(request[JSON_TOKEN.CURSORS].keys(),
                               self._cursor_transformer.rcs_to_nums(
                                   request[JSON_TOKEN.CURSORS].values())))
            new_user_info, new_text = self._users_text_manager.update_user_text(
                identity,
                UserInfo(mode=request[JSON_TOKEN.MODE], cursors=cursors),
                '\n'.join(lines))
            return self._pack_sync_response(
                identity, new_user_info, new_text.split('\n'), lines)

    def _pack_sync_response(self, identity, user_info, lines, old_lines):
        """Packs the response for the sync request by the result from manager.

        Args:
            identity: Identity of that user.
            user_info: Informations of that user.
            lines: New lines of text.
            old_lines: Old lines of text.

        Return:
            The response json object.
        """
        self._cursor_transformer.update_lines(lines)
        return {
            JSON_TOKEN.DIFF : gen_patch(old_lines, lines),
            JSON_TOKEN.CURSORS : dict(zip(
                user_info.cursors.keys(),
                self._cursor_transformer.nums_to_rcs(
                    user_info.cursors.values()))),
            JSON_TOKEN.MODE : user_info.mode,
            JSON_TOKEN.OTHERS : self._pack_sync_others_response(identity)
        }

    def _pack_sync_others_response(self, identity):
        """Packs the response information for other users.

        Args:
            identity: Identity of that user.

        Return:
            The response json object.
        """
        return [
            {
                JSON_TOKEN.NICKNAME : other.nick_name,
                JSON_TOKEN.MODE : other.mode,
                JSON_TOKEN.CURSORS: dict(zip(
                    other.cursors.keys(),
                    self._cursor_transformer.nums_to_rcs(
                        other.cursors.values())))
            } for iden, other in self._users_text_manager.get_users_info(
                without=[identity], must_online=True).items()
        ]

    def _check_init(self, identity, request):
        """Checks whether that user should be initialize or not.

        If yes, it will reset that user and update the request.

        Args:
            identity: The identity of that user.
            request: The request from that user.
        """
        if request[JSON_TOKEN.INIT]:
            log.info('Init the user %r\n' % identity)
            self._users_text_manager.reset_user(identity)
            request[JSON_TOKEN.DIFF] = []
            for mark in request.get(JSON_TOKEN.CURSORS, []):
                request[JSON_TOKEN.CURSORS][mark] = (0, 0)

    def _check_authority(self, identity, request):
        """Checks the authroity and updates the request.

        If the user is not writeable, it will modify the request to let it looks
        like that the user did nothing.

        Args:
            identity: The identity of that user.
            request: The request from that user.
        """
        auth = self._users_text_manager.get_users_info()[identity].authority
        if auth < AUTHORITY.READWRITE:
            request[JSON_TOKEN.DIFF] = []