# -*- coding: utf-8 -*-
"""
This module implements renderers that play content to the screen.
"""
from __future__ import division
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals
from abc import abstractmethod
from builtins import range
import json

from asciimatics.renderers.base import DynamicRenderer
from asciimatics.screen import Screen
from asciimatics.parsers import AnsiTerminalParser, Parser


class AbstractScreenPlayer(DynamicRenderer):
    """
    Abstract renderer to play terminal text with support for ANSI control codes.
    """

    def __init__(self, height, width):
        """
        :param height: required height of the renderer.
        :param width: required width of the renderer.
        """
        super(AbstractScreenPlayer, self).__init__(height, width, clear=False)
        self._parser = AnsiTerminalParser()
        self._current_colours = [Screen.COLOUR_WHITE, Screen.A_NORMAL, Screen.COLOUR_BLACK]
        self._show_cursor = False
        self._cursor_x = 0
        self._cursor_y = 0
        self._save_cursor_x = 0
        self._save_cursor_y = 0
        self._counter = 0
        self._next = 0
        self._buffer = None
        self._parser.reset("", self._current_colours)
        self._clear()

    @abstractmethod
    def _render_now(self):
        pass

    def _play_content(self, text):
        """
        Process new raw text.

        :param text: thebraw text to be processed.
        """
        lines = text.split("\n")
        for i, line in enumerate(lines):
            self._parser.append(line)
            for _, command, params in self._parser.parse():
                # logging.debug("Command: {} {}".format(command, params))
                if command == Parser.DISPLAY_TEXT:
                    # Just display the text...  allowing for line wrapping.
                    if self._cursor_x + len(params) >= self._canvas.width:
                        part_1 = params[:self._canvas.width - self._cursor_x]
                        part_2 = params[self._canvas.width - self._cursor_x:]
                        self._print_at(part_1, self._cursor_x, self._cursor_y)
                        self._print_at(part_2, 0, self._cursor_y + 1)
                        self._cursor_x = len(part_2)
                        self._cursor_y += 1
                        if self._cursor_y - self._canvas.start_line >= self._canvas.height:
                            self._canvas.scroll()
                    else:
                        self._print_at(params, self._cursor_x, self._cursor_y)
                        self._cursor_x += len(params)
                elif command == Parser.CHANGE_COLOURS:
                    # Change current text colours.
                    self._current_colours = params
                elif command == Parser.NEXT_TAB:
                    # Move to next tab stop - hard-coded to default of 8 characters.
                    self._cursor_x = (self._cursor_x // 8) * 8 + 8
                elif command == Parser.MOVE_RELATIVE:
                    # Move cursor relative to current position.
                    self._cursor_x += params[0]
                    self._cursor_y += params[1]
                    if self._cursor_y < self._canvas.start_line:
                        self._canvas.scroll(self._cursor_y - self._canvas.start_line)
                elif command == Parser.MOVE_ABSOLUTE:
                    # Move cursor relative to specified absolute position.
                    if params[0] is not None:
                        self._cursor_x = params[0]
                    if params[1] is not None:
                        self._cursor_y = params[1] + self._canvas.start_line
                elif command == Parser.DELETE_LINE:
                    # Delete some/all of the current line.
                    if params == 0:
                        self._print_at(
                            " " * (self._canvas.width - self._cursor_x), self._cursor_x, self._cursor_y)
                    elif params == 1:
                        self._print_at(" " * self._cursor_x, 0, self._cursor_y)
                    elif params == 2:
                        self._print_at(" " * self._canvas.width, 0, self._cursor_y)
                elif command == Parser.DELETE_CHARS:
                    # Delete n characters under the cursor.
                    for x in range(self._cursor_x, self._canvas.width):
                        if x + params < self._canvas.width:
                            cell = self._canvas.get_from(x + params, self._cursor_y)
                        else:
                            cell = (ord(" "),
                                    self._current_colours[0],
                                    self._current_colours[1],
                                    self._current_colours[2])
                        self._canvas.print_at(
                            chr(cell[0]), x, self._cursor_y, colour=cell[1], attr=cell[2], bg=cell[3])
                elif command == Parser.SHOW_CURSOR:
                    # Show/hide the cursor.
                    self._show_cursor = params
                elif command == Parser.SAVE_CURSOR:
                    # Save the cursor position.
                    self._save_cursor_x = self._cursor_x
                    self._save_cursor_y = self._cursor_y
                elif command == Parser.RESTORE_CURSOR:
                    # Restore the cursor position.
                    self._cursor_x = self._save_cursor_x
                    self._cursor_y = self._save_cursor_y
                elif command == Parser.CLEAR_SCREEN:
                    # Clear the screen.
                    self._canvas.clear_buffer(
                        self._current_colours[0], self._current_colours[1], self._current_colours[2])
                    self._cursor_x = 0
                    self._cursor_y = self._canvas.start_line
            # Move to next line, scrolling buffer as needed.
            if i != len(lines) - 1:
                self._cursor_x = 0
                self._cursor_y += 1
                if self._cursor_y - self._canvas.start_line >= self._canvas.height:
                    self._canvas.scroll()

    def _print_at(self, text, x, y):
        """
        Helper function to simplify use of the renderer.
        """
        self._canvas.print_at(
            text,
            x, y,
            colour=self._current_colours[0], attr=self._current_colours[1], bg=self._current_colours[2])


class AnsiArtPlayer(AbstractScreenPlayer):
    """
    Renderer to play ANSI art text files.

    In order to tidy up files, this must be used as a context manager (i.e. using `with`).
    """

    def __init__(self, filename, height=25, width=80, encoding="cp437", strip=False, rate=2):
        """
        :param filename: the file containingi the ANSI art.
        :param height: required height of the renderer.
        :param width: required width of the renderer.
        :param encoding: text encoding ofnthe file.
        :param strip: whether to strip CRLF from the file content.
        :param rate: number of lines to render on each update.
        """
        super(AnsiArtPlayer, self).__init__(height, width)
        self._file = open(filename, "rb")
        self._strip = strip
        self._rate = rate
        self._encoding = encoding

    def __enter__(self):
        """
        Create context for use as a context manager.
        """
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """
        Clear up the resources for this context.
        """
        if self._file:
            self._file.close()

    def _render_now(self):
        count = 0
        line = None
        while count < self._rate and line != "":
            line = self._file.readline().decode(self._encoding)
            count += 1
            if self._strip:
                line = line.rstrip("\r\n")
            self._play_content(line)
        return self._plain_image, self._colour_map


class AsciinemaPlayer(AbstractScreenPlayer):
    """
    Renderer to play terminal recordings created by asciinema.

    This only supports the version 2 file format.  Use the max_delay setting to speed up human
    interactions (i.e. to reduce delays from typing).

    In order to tidy up files, this must be used as a context manager (i.e. using `with`).
    """

    def __init__(self, filename, height=None, width=None, max_delay=None):
        """
        :param filename: the file containingi the ANSI art.
        :param height: required height of the renderer.
        :param width: required width of the renderer.
        :param max_delay: maximum time interval (in secs) to wait between frame updates.
        """
        # Open the file and check it looks plausibly like a supported format.
        self._file = open(filename)
        header = json.loads(self._file.readline())
        if header["version"] != 2:
            raise RuntimeError("Unsupported file format")

        # Use file details if not overriden by constructor params.
        height = height if height else header["height"]
        width = width if width else header["width"]

        # Construct the full player now we have all the details.
        super(AsciinemaPlayer, self).__init__(height, width)
        self._max_delay = max_delay

    def __enter__(self):
        """
        Create context for use as a context manager.
        """
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        """
        Clear up the resources for this context.
        """
        if self._file:
            self._file.close()

    def _render_now(self):
        self._counter += 0.05
        if self._counter >= self._next:
            if self._buffer:
                self._play_content(self._buffer)
                self._buffer = None
            while True:
                try:
                    self._next, _, self._buffer = json.loads(self._file.readline())
                    if self._next > self._counter:
                        # Speed up playback if requested.
                        if self._max_delay and self._next - self._counter > self._max_delay:
                            self._counter = self._next - self._max_delay
                        break
                    self._play_content(self._buffer)
                except ValueError:
                    # Python 3 raises a subclass of this error, so will also be caught.
                    break

        return self._plain_image, self._colour_map
