"""
This module defines common screen output function.  For more details, see
http://asciimatics.readthedocs.io/en/latest/io.html
"""
import os
import signal
import struct
import sys
import time
from abc import ABCMeta, abstractmethod
from functools import update_wrapper, partial
from itertools import zip_longest
from locale import getlocale, getdefaultlocale
from logging import getLogger
from math import sqrt

from wcwidth import wcwidth, wcswidth

from asciimatics.event import KeyboardEvent, MouseEvent
from asciimatics.exceptions import ResizeScreenError, StopApplication, NextScene
from asciimatics.utilities import _DotDict
from asciimatics import constants

logger = getLogger(__name__)

# Looks like pywin32 is missing some Windows constants
ENABLE_EXTENDED_FLAGS = 0x0080
ENABLE_QUICK_EDIT_MODE = 0x0040


class _DoubleBuffer():
    """
    Pure python Screen buffering.
    """

    def __init__(self, height, width):
        """
        :param height: Height of the buffer to create.
        :param width: Width of the buffer to create.
        """
        super().__init__()
        self._height = height
        self._width = width
        self._double_buffer = None
        line = [(" ", Screen.COLOUR_WHITE, 0, 0, 1) for _ in range(self._width)]
        self._screen_buffer = [line[:] for _ in range(self._height)]
        self.clear(Screen.COLOUR_WHITE, 0, 0)

    def clear(self, fg, attr, bg, x=0, y=0, w=None, h=None):
        """
        Clear a box in the double-buffer.

        This does not clear the screen buffer and so the next call to deltas will still show all changes.
        Default box is the whole screen buffer.

        :param fg: The foreground colour to use for the new buffer.
        :param attr: The attribute value to use for the new buffer.
        :param bg: The background colour to use for the new buffer.
        :param x: Optional X coordinate for top left of box.
        :param y: Optional Y coordinate for top left of box.
        :param w: Optional width of the box.
        :param h: Optional height of the box.
        """
        width = self._width if w is None else w
        height = self._height if h is None else h
        width = max(0, min(self._width - x, width))
        height = max(0, min(self._height - y, height))
        line = [(" ", fg, attr, bg, 1) for _ in range(width)]
        if x == 0 and y == 0 and w is None and h is None:
            self._double_buffer = [line[:] for _ in range(height)]
        else:
            for i in range(y, y + height):
                self._double_buffer[i][x:x + w] = line[:]

    def invalidate(self):
        """
        Invalidate the screen buffer to force a full refresh.
        """
        line = [(None, None, None, None, 1) for _ in range(self._width)]
        self._screen_buffer = [line[:] for _ in range(self._height)]

    def get(self, x, y):
        """
        Get the cell value from the specified location

        :param x: The column (x coord) of the character.
        :param y: The row (y coord) of the character.

        :return: A 5-tuple of (unicode, foreground, attributes, background, width).
        """
        return self._double_buffer[y][x]

    def set(self, x, y, value):
        """
        Set the cell value from the specified location

        :param x: The column (x coord) of the character.
        :param y: The row (y coord) of the character.
        :param value: A 5-tuple of (unicode, foreground, attributes, background, width).
        """
        self._double_buffer[y][x] = value

    def deltas(self, start, height):
        """
        Return a list-like (i.e. iterable) object of (y, x) tuples
        """
        for y in range(start, min(start + height, self._height)):
            for x in range(self._width):
                old_cell = self._screen_buffer[y][x]
                new_cell = self._double_buffer[y][x]
                if old_cell != new_cell:
                    yield y, x

    def scroll(self, lines):
        """
        Scroll the window up or down.

        :param lines: Number of lines to scroll.  Negative numbers move the buffer up.
        """
        line = [(" ", Screen.COLOUR_WHITE, 0, 0, 1) for _ in range(self._width)]
        if lines > 0:
            # Limit to buffer size - this will just invalidate all the data
            lines = min(lines, self._height)
            for y in range(0, self._height - lines):
                self._double_buffer[y] = self._double_buffer[y + lines]
                self._screen_buffer[y] = self._screen_buffer[y + lines]
            for y in range(self._height - lines, self._height):
                self._double_buffer[y] = line[:]
                self._screen_buffer[y] = line[:]
        else:
            # Limit to buffer size - this will just invalidate all the data
            lines = max(lines, -self._height)
            for y in range(self._height - 1, -lines - 1, -1):
                self._double_buffer[y] = self._double_buffer[y + lines]
                self._screen_buffer[y] = self._screen_buffer[y + lines]
            for y in range(0, -lines):
                self._double_buffer[y] = line[:]
                self._screen_buffer[y] = line[:]

    def block_transfer(self, buffer, x, y):
        """
        Copy a buffer entirely to this double buffer.

        :param buffer: The double buffer to copy
        :param x: The X origin for where to place it in this buffer
        :param y: The Y origin for where to place it in this buffer
        """
        # Just copy the double-buffer cells - the real screen will sync on refresh.
        block_min_x = max(0, x)
        block_max_x = min(x + buffer.width, self._width)

        # Check for trivial non-overlap
        if block_min_x > block_max_x:
            return

        # Copy the available section
        for by in range(0, self._height):
            if y <= by < y + buffer.height:
                self._double_buffer[by][block_min_x:block_max_x] = buffer.slice(
                    block_min_x - x, by - y, block_max_x - block_min_x)

    def slice(self, x, y, width):
        """
        Provide a slice of data from the buffer at the specified location

        :param x: The X origin
        :param y: The Y origin
        :param width: The width of slice required
        :return: The slice of tuples from the current double-buffer
        """
        return self._double_buffer[y][x:x + width]

    def sync(self):
        """
        Synchronize the screen buffer with the double buffer.
        """
        # We're copying an array of tuples, so only need to copy the 2-D array (as the tuples are immutable).
        # This is way faster than a deep copy (which is INCREDIBLY slow).
        self._screen_buffer = [row[:] for row in self._double_buffer]

    @property
    def height(self):
        """
        The height of this buffer.
        """
        return self._height

    @property
    def width(self):
        """
        The width of this buffer.
        """
        return self._width

    @property
    def plain_image(self):
        return ["".join(x[0] for x in self.slice(0, y, self.width)) for y in range(self.height)]

    @property
    def colour_map(self):
        return [[x[1:4] for x in self.slice(0, y, self.width)] for y in range(self.height)]


class _AbstractCanvas(metaclass=ABCMeta):
    """
    Abstract class to handle screen buffering.
    """

    # Characters for anti-aliasing line drawing.
    _line_chars = " ''^.|/7.\\|Ywbd#"
    _uni_line_chars = " ▘▝▀▖▌▞▛▗▚▐▜▄▙▟█"

    #  Colour palette for 8/16 colour terminals
    _8_palette = [
        0x00, 0x00, 0x00,
        0x80, 0x00, 0x00,
        0x00, 0x80, 0x00,
        0x80, 0x80, 0x00,
        0x00, 0x00, 0x80,
        0x80, 0x00, 0x80,
        0x00, 0x80, 0x80,
        0xc0, 0xc0, 0xc0,
    ] + [0x00 for _ in range(248 * 3)]

    # Colour palette for 256 colour terminals
    _256_palette = [
        0x00, 0x00, 0x00,
        0x80, 0x00, 0x00,
        0x00, 0x80, 0x00,
        0x80, 0x80, 0x00,
        0x00, 0x00, 0x80,
        0x80, 0x00, 0x80,
        0x00, 0x80, 0x80,
        0xc0, 0xc0, 0xc0,
        0x80, 0x80, 0x80,
        0xff, 0x00, 0x00,
        0x00, 0xff, 0x00,
        0xff, 0xff, 0x00,
        0x00, 0x00, 0xff,
        0xff, 0x00, 0xff,
        0x00, 0xff, 0xff,
        0xff, 0xff, 0xff,
        0x00, 0x00, 0x00,
        0x00, 0x00, 0x5f,
        0x00, 0x00, 0x87,
        0x00, 0x00, 0xaf,
        0x00, 0x00, 0xd7,
        0x00, 0x00, 0xff,
        0x00, 0x5f, 0x00,
        0x00, 0x5f, 0x5f,
        0x00, 0x5f, 0x87,
        0x00, 0x5f, 0xaf,
        0x00, 0x5f, 0xd7,
        0x00, 0x5f, 0xff,
        0x00, 0x87, 0x00,
        0x00, 0x87, 0x5f,
        0x00, 0x87, 0x87,
        0x00, 0x87, 0xaf,
        0x00, 0x87, 0xd7,
        0x00, 0x87, 0xff,
        0x00, 0xaf, 0x00,
        0x00, 0xaf, 0x5f,
        0x00, 0xaf, 0x87,
        0x00, 0xaf, 0xaf,
        0x00, 0xaf, 0xd7,
        0x00, 0xaf, 0xff,
        0x00, 0xd7, 0x00,
        0x00, 0xd7, 0x5f,
        0x00, 0xd7, 0x87,
        0x00, 0xd7, 0xaf,
        0x00, 0xd7, 0xd7,
        0x00, 0xd7, 0xff,
        0x00, 0xff, 0x00,
        0x00, 0xff, 0x5f,
        0x00, 0xff, 0x87,
        0x00, 0xff, 0xaf,
        0x00, 0xff, 0xd7,
        0x00, 0xff, 0xff,
        0x5f, 0x00, 0x00,
        0x5f, 0x00, 0x5f,
        0x5f, 0x00, 0x87,
        0x5f, 0x00, 0xaf,
        0x5f, 0x00, 0xd7,
        0x5f, 0x00, 0xff,
        0x5f, 0x5f, 0x00,
        0x5f, 0x5f, 0x5f,
        0x5f, 0x5f, 0x87,
        0x5f, 0x5f, 0xaf,
        0x5f, 0x5f, 0xd7,
        0x5f, 0x5f, 0xff,
        0x5f, 0x87, 0x00,
        0x5f, 0x87, 0x5f,
        0x5f, 0x87, 0x87,
        0x5f, 0x87, 0xaf,
        0x5f, 0x87, 0xd7,
        0x5f, 0x87, 0xff,
        0x5f, 0xaf, 0x00,
        0x5f, 0xaf, 0x5f,
        0x5f, 0xaf, 0x87,
        0x5f, 0xaf, 0xaf,
        0x5f, 0xaf, 0xd7,
        0x5f, 0xaf, 0xff,
        0x5f, 0xd7, 0x00,
        0x5f, 0xd7, 0x5f,
        0x5f, 0xd7, 0x87,
        0x5f, 0xd7, 0xaf,
        0x5f, 0xd7, 0xd7,
        0x5f, 0xd7, 0xff,
        0x5f, 0xff, 0x00,
        0x5f, 0xff, 0x5f,
        0x5f, 0xff, 0x87,
        0x5f, 0xff, 0xaf,
        0x5f, 0xff, 0xd7,
        0x5f, 0xff, 0xff,
        0x87, 0x00, 0x00,
        0x87, 0x00, 0x5f,
        0x87, 0x00, 0x87,
        0x87, 0x00, 0xaf,
        0x87, 0x00, 0xd7,
        0x87, 0x00, 0xff,
        0x87, 0x5f, 0x00,
        0x87, 0x5f, 0x5f,
        0x87, 0x5f, 0x87,
        0x87, 0x5f, 0xaf,
        0x87, 0x5f, 0xd7,
        0x87, 0x5f, 0xff,
        0x87, 0x87, 0x00,
        0x87, 0x87, 0x5f,
        0x87, 0x87, 0x87,
        0x87, 0x87, 0xaf,
        0x87, 0x87, 0xd7,
        0x87, 0x87, 0xff,
        0x87, 0xaf, 0x00,
        0x87, 0xaf, 0x5f,
        0x87, 0xaf, 0x87,
        0x87, 0xaf, 0xaf,
        0x87, 0xaf, 0xd7,
        0x87, 0xaf, 0xff,
        0x87, 0xd7, 0x00,
        0x87, 0xd7, 0x5f,
        0x87, 0xd7, 0x87,
        0x87, 0xd7, 0xaf,
        0x87, 0xd7, 0xd7,
        0x87, 0xd7, 0xff,
        0x87, 0xff, 0x00,
        0x87, 0xff, 0x5f,
        0x87, 0xff, 0x87,
        0x87, 0xff, 0xaf,
        0x87, 0xff, 0xd7,
        0x87, 0xff, 0xff,
        0xaf, 0x00, 0x00,
        0xaf, 0x00, 0x5f,
        0xaf, 0x00, 0x87,
        0xaf, 0x00, 0xaf,
        0xaf, 0x00, 0xd7,
        0xaf, 0x00, 0xff,
        0xaf, 0x5f, 0x00,
        0xaf, 0x5f, 0x5f,
        0xaf, 0x5f, 0x87,
        0xaf, 0x5f, 0xaf,
        0xaf, 0x5f, 0xd7,
        0xaf, 0x5f, 0xff,
        0xaf, 0x87, 0x00,
        0xaf, 0x87, 0x5f,
        0xaf, 0x87, 0x87,
        0xaf, 0x87, 0xaf,
        0xaf, 0x87, 0xd7,
        0xaf, 0x87, 0xff,
        0xaf, 0xaf, 0x00,
        0xaf, 0xaf, 0x5f,
        0xaf, 0xaf, 0x87,
        0xaf, 0xaf, 0xaf,
        0xaf, 0xaf, 0xd7,
        0xaf, 0xaf, 0xff,
        0xaf, 0xd7, 0x00,
        0xaf, 0xd7, 0x5f,
        0xaf, 0xd7, 0x87,
        0xaf, 0xd7, 0xaf,
        0xaf, 0xd7, 0xd7,
        0xaf, 0xd7, 0xff,
        0xaf, 0xff, 0x00,
        0xaf, 0xff, 0x5f,
        0xaf, 0xff, 0x87,
        0xaf, 0xff, 0xaf,
        0xaf, 0xff, 0xd7,
        0xaf, 0xff, 0xff,
        0xd7, 0x00, 0x00,
        0xd7, 0x00, 0x5f,
        0xd7, 0x00, 0x87,
        0xd7, 0x00, 0xaf,
        0xd7, 0x00, 0xd7,
        0xd7, 0x00, 0xff,
        0xd7, 0x5f, 0x00,
        0xd7, 0x5f, 0x5f,
        0xd7, 0x5f, 0x87,
        0xd7, 0x5f, 0xaf,
        0xd7, 0x5f, 0xd7,
        0xd7, 0x5f, 0xff,
        0xd7, 0x87, 0x00,
        0xd7, 0x87, 0x5f,
        0xd7, 0x87, 0x87,
        0xd7, 0x87, 0xaf,
        0xd7, 0x87, 0xd7,
        0xd7, 0x87, 0xff,
        0xd7, 0xaf, 0x00,
        0xd7, 0xaf, 0x5f,
        0xd7, 0xaf, 0x87,
        0xd7, 0xaf, 0xaf,
        0xd7, 0xaf, 0xd7,
        0xd7, 0xaf, 0xff,
        0xd7, 0xd7, 0x00,
        0xd7, 0xd7, 0x5f,
        0xd7, 0xd7, 0x87,
        0xd7, 0xd7, 0xaf,
        0xd7, 0xd7, 0xd7,
        0xd7, 0xd7, 0xff,
        0xd7, 0xff, 0x00,
        0xd7, 0xff, 0x5f,
        0xd7, 0xff, 0x87,
        0xd7, 0xff, 0xaf,
        0xd7, 0xff, 0xd7,
        0xd7, 0xff, 0xff,
        0xff, 0x00, 0x00,
        0xff, 0x00, 0x5f,
        0xff, 0x00, 0x87,
        0xff, 0x00, 0xaf,
        0xff, 0x00, 0xd7,
        0xff, 0x00, 0xff,
        0xff, 0x5f, 0x00,
        0xff, 0x5f, 0x5f,
        0xff, 0x5f, 0x87,
        0xff, 0x5f, 0xaf,
        0xff, 0x5f, 0xd7,
        0xff, 0x5f, 0xff,
        0xff, 0x87, 0x00,
        0xff, 0x87, 0x5f,
        0xff, 0x87, 0x87,
        0xff, 0x87, 0xaf,
        0xff, 0x87, 0xd7,
        0xff, 0x87, 0xff,
        0xff, 0xaf, 0x00,
        0xff, 0xaf, 0x5f,
        0xff, 0xaf, 0x87,
        0xff, 0xaf, 0xaf,
        0xff, 0xaf, 0xd7,
        0xff, 0xaf, 0xff,
        0xff, 0xd7, 0x00,
        0xff, 0xd7, 0x5f,
        0xff, 0xd7, 0x87,
        0xff, 0xd7, 0xaf,
        0xff, 0xd7, 0xd7,
        0xff, 0xd7, 0xff,
        0xff, 0xff, 0x00,
        0xff, 0xff, 0x5f,
        0xff, 0xff, 0x87,
        0xff, 0xff, 0xaf,
        0xff, 0xff, 0xd7,
        0xff, 0xff, 0xff,
        0x08, 0x08, 0x08,
        0x12, 0x12, 0x12,
        0x1c, 0x1c, 0x1c,
        0x26, 0x26, 0x26,
        0x30, 0x30, 0x30,
        0x3a, 0x3a, 0x3a,
        0x44, 0x44, 0x44,
        0x4e, 0x4e, 0x4e,
        0x58, 0x58, 0x58,
        0x62, 0x62, 0x62,
        0x6c, 0x6c, 0x6c,
        0x76, 0x76, 0x76,
        0x80, 0x80, 0x80,
        0x8a, 0x8a, 0x8a,
        0x94, 0x94, 0x94,
        0x9e, 0x9e, 0x9e,
        0xa8, 0xa8, 0xa8,
        0xb2, 0xb2, 0xb2,
        0xbc, 0xbc, 0xbc,
        0xc6, 0xc6, 0xc6,
        0xd0, 0xd0, 0xd0,
        0xda, 0xda, 0xda,
        0xe4, 0xe4, 0xe4,
        0xee, 0xee, 0xee,
    ]

    def __init__(self, height, width, buffer_height, colours, unicode_aware):
        """
        :param height: The buffer height for this object.
        :param width: The buffer width for this object.
        :param buffer_height: The buffer height for this object.
        :param colours: Number of colours for this object.
        :param unicode_aware: Force use of unicode options for this object.
        """
        super().__init__()

        # Can we handle unicode environments?
        self._unicode_aware = unicode_aware

        # Create screen buffers.
        self.height = height
        self.width = width
        self.colours = colours
        self._buffer_height = height if buffer_height is None else buffer_height
        self._buffer = None
        self._start_line = 0
        self._x = 0
        self._y = 0

        # dictionary cache for colour blending
        self._blends = {}

        # Reset the screen ready to go...
        self.reset()

    def clear_buffer(self, fg, attr, bg, x=0, y=0, w=None, h=None):
        """
        Clear a box in the current double-buffer used by this object.

        This is the recommended way to clear parts, or all, ofthe Screen without causing flicker
        as it will only become visible at the next refresh.  Defaults to the whole buffer if no
        box is specified.

        :param fg: The foreground colour to use for the new buffer.
        :param attr: The attribute value to use for the new buffer.
        :param bg: The background colour to use for the new buffer.
        :param x: Optional X coordinate for top left of box.
        :param y: Optional Y coordinate for top left of box.
        :param w: Optional width of the box.
        :param h: Optional height of the box.
        """
        self._buffer.clear(fg, attr, bg, x, y, w, h)

    def reset(self):
        """
        Reset the internal buffers for the abstract canvas.
        """
        # Reset our screen buffer
        self._start_line = 0
        self._x = self._y = None
        self._buffer = _DoubleBuffer(self._buffer_height, self.width)
        self._reset()

    def scroll(self, lines=1):
        """
        Scroll the abstract canvas up one line.

        :param lines: The number of lines to scroll.  Defaults to down by one.
        """
        self._buffer.scroll(lines)
        self._start_line += lines

    def scroll_to(self, line):
        """
        Scroll the abstract canvas to make a specific line.

        :param line: The line to scroll to.
        """
        self._buffer.scroll(line - self._start_line)
        self._start_line = line

    @abstractmethod
    def _reset(self):
        """
        Internal implementation required to reset underlying drawing interface.
        """

    @abstractmethod
    def refresh(self):
        """
        Refresh this object - this will draw to the underlying display interface.
        """

    def get_from(self, x, y):
        """
        Get the character at the specified location.

        :param x: The column (x coord) of the character.
        :param y: The row (y coord) of the character.

        :return: A 4-tuple of (ascii code, foreground, attributes, background)
                 for the character at the location.
        """
        # Convert to buffer coordinates
        y -= self._start_line
        if y < 0 or y >= self._buffer_height or x < 0 or x >= self.width:
            return None
        cell = self._buffer.get(x, y)
        return ord(cell[0]), cell[1], cell[2], cell[3]

    def print_at(self, text, x, y, colour=7, attr=0, bg=0, transparent=False):
        """
        Print the text at the specified location using the specified colour and attributes.

        :param text: The (single line) text to be printed.
        :param x: The column (x coord) for the start of the text.
        :param y: The line (y coord) for the start of the text.
        :param colour: The colour of the text to be displayed.
        :param attr: The cell attribute of the text to be displayed.
        :param bg: The background colour of the text to be displayed.
        :param transparent: Whether to print spaces or not, thus giving a
            transparent effect.

        The colours and attributes are the COLOUR_xxx and A_yyy constants
        defined in the Screen class.
        """
        # Convert to the logically visible window that our double-buffer provides
        y -= self._start_line

        # Trim text to the buffer vertically.  Don't trim horizontally as we don't know whether any
        # of these characters are dual-width yet.  Handle it on the fly below...
        if y < 0 or y >= self._buffer_height or x > self.width:
            return

        text = str(text)
        if len(text) > 0:
            if self._unicode_aware:
                j = 0
                for i, c in enumerate(text):
                    # Handle under-run and overrun of double-width glyphs now.
                    #
                    # Note that wcwidth uses significant resources, so only call when we have a
                    # unicode aware application.  The rest of the time assume ASCII.
                    width = wcwidth(c) if self._unicode_aware and ord(c) >= 256 else 1
                    if x + i + j < 0:
                        x += (width - 1)
                        continue
                    if x + i + j + width > self.width:
                        return

                    # Handle modifier glyphs - just delete them for now.
                    if width == 0:
                        j -= 1
                        continue

                    # Now handle the update.
                    if c != " " or not transparent:
                        # Fix up orphaned double-width glyphs that we've just bisected.
                        if x + i + j - 1 >= 0 and self._buffer.get(x + i + j - 1, y)[4] == 2:
                            self._buffer.set(x + i + j - 1, y, ("x", 0, 0, 0, 1))

                        self._buffer.set(x + i + j, y, (c, colour, attr, bg, width))
                        if width == 2:
                            j += 1
                            if x + i + j < self.width:
                                self._buffer.set(x + i + j, y, (c, colour, attr, bg, 0))

                        # Now fix up any glyphs we may have bisected the other way.
                        if x + i + j + 1 < self.width and self._buffer.get(x + i + j + 1, y)[4] == 0:
                            self._buffer.set(x + i + j + 1, y, ("x", 0, 0, 0, 1))
            else:
                # Optimized version that ignores double-width characters
                if x < 0:
                    text = text[-x:]
                    x = 0
                if x + len(text) > self.width:
                    text = text[:self.width - x]
                if not transparent:
                    self._buffer.set(slice(x, x + len(text)), y, [(c, colour, attr, bg, 1) for c in text])
                else:
                    for i, c in enumerate(text):
                        if c != " ":
                            self._buffer.set(x + i, y, (c, colour, attr, bg, 1))

    def block_transfer(self, buffer, x, y):
        """
        Copy a buffer to the screen double buffer at a specified location.

        :param buffer: The double buffer to copy
        :param x: The X origin for where to place it in the Screen
        :param y: The Y origin for where to place it in the Screen
        """
        self._buffer.block_transfer(buffer, x, y)

    @property
    def start_line(self):
        """
        :return: The start line of the top of the canvas.
        """
        return self._start_line

    @property
    def unicode_aware(self):
        """
        :return: Whether unicode input/output is supported or not.
        """
        return self._unicode_aware

    @property
    def dimensions(self):
        """
        :return: The full dimensions of the canvas as a (height, width) tuple.
        """
        return self.height, self.width

    @property
    def palette(self):
        """
        :return: A palette compatible with the PIL.
        """
        if self.colours < 256:
            # Use the ANSI colour set.
            return self._8_palette
        else:
            return self._256_palette

    def centre(self, text, y, colour=7, attr=0, colour_map=None):
        """
        Centre the text on the specified line (y) using the optional colour and attributes.

        :param text: The (single line) text to be printed.
        :param y: The line (y coord) for the start of the text.
        :param colour: The colour of the text to be displayed.
        :param attr: The cell attribute of the text to be displayed.
        :param colour_map: Colour/attribute list for multi-colour text.

        The colours and attributes are the COLOUR_xxx and A_yyy constants
        defined in the Screen class.
        """
        if self._unicode_aware:
            x = (self.width - wcswidth(text)) // 2
        else:
            x = (self.width - len(text)) // 2
        self.paint(text, x, y, colour, attr, colour_map=colour_map)

    def paint(self, text, x, y, colour=7, attr=0, bg=0, transparent=False,
              colour_map=None):
        """
        Paint multi-colour text at the defined location.

        :param text: The (single line) text to be printed.
        :param x: The column (x coord) for the start of the text.
        :param y: The line (y coord) for the start of the text.
        :param colour: The default colour of the text to be displayed.
        :param attr: The default cell attribute of the text to be displayed.
        :param bg: The default background colour of the text to be displayed.
        :param transparent: Whether to print spaces or not, thus giving a
            transparent effect.
        :param colour_map: Colour/attribute list for multi-colour text.

        The colours and attributes are the COLOUR_xxx and A_yyy constants
        defined in the Screen class.
        colour_map is a list of tuples (foreground, attribute, background) that
        must be the same length as the passed in text (or None if no mapping is
        required).
        """
        if colour_map is None:
            self.print_at(text, x, y, colour, attr, bg, transparent)
        else:
            offset = next_offset = 0
            current = ""
            for c, m in zip_longest(str(text), colour_map):
                if m:
                    if len(current) > 0:
                        self.print_at(current, x + offset, y, colour, attr, bg, transparent)
                        offset = next_offset
                        current = ""
                    if len(m) > 0 and m[0] is not None:
                        colour = m[0]
                    if len(m) > 1 and m[1] is not None:
                        attr = m[1]
                    if len(m) > 2 and m[2] is not None:
                        bg = m[2]
                if c:
                    current += c
                    next_offset += wcwidth(c) if ord(c) >= 256 else 1
            if len(current) > 0:
                self.print_at(current, x + offset, y, colour, attr, bg, transparent)

    def _blend(self, new, old, ratio):
        """
        Blend the new colour with the old according to the ratio.

        :param new: The new colour (or None if not required).
        :param old: The old colour.
        :param ratio: The ratio to blend new and old
        :returns: the new colour index to use for the required blend.
        """
        # Don't bother blending if none is required.
        if new is None:
            return old

        # Check colour blend cache for a quick answer.
        key = (min(new, old), max(new, old))
        if key in self._blends:
            return self._blends[key]

        # No quick answer - do it the long way...  First lookup the RGB values
        # for both colours and blend.
        (r1, g1, b1) = self.palette[new * 3:new * 3 + 3]
        (r2, g2, b2) = self.palette[old * 3:old * 3 + 3]

        # Helper function to blend RGB values.
        def f(c1, c2):
            return ((c1 * ratio) + (c2 * (100 - ratio))) // 100

        r = f(r1, r2)
        g = f(g1, g2)
        b = f(b1, b2)

        # Now do the reverse lookup...
        nearest = (256 ** 2) * 3
        match = 0
        for c in range(self.colours):
            (rc, gc, bc) = self.palette[c * 3:c * 3 + 3]
            diff = sqrt(((rc - r) * 0.3) ** 2 + ((gc - g) * 0.59) ** 2 +
                        ((bc - b) * 0.11) ** 2)
            if diff < nearest:
                nearest = diff
                match = c

        # Save off the answer and return it
        self._blends[key] = match
        return match

    def highlight(self, x, y, w, h, fg=None, bg=None, blend=100):
        """
        Highlight a specified section of the screen.

        :param x: The column (x coord) for the start of the highlight.
        :param y: The line (y coord) for the start of the highlight.
        :param w: The width of the highlight (in characters).
        :param h: The height of the highlight (in characters).
        :param fg: The foreground colour of the highlight.
        :param bg: The background colour of the highlight.
        :param blend: How much (as a percentage) to take of the new colour
            when blending.

        The colours and attributes are the COLOUR_xxx and A_yyy constants
        defined in the Screen class.  If fg or bg are None that means don't
        change the foreground/background as appropriate.
        """
        # Convert to buffer coordinates
        y -= self._start_line
        for i in range(w):
            if x + i >= self.width or x + i < 0:
                continue

            for j in range(h):
                if y + j >= self._buffer_height or y + j < 0:
                    continue

                old = self._buffer.get(x + i, y + j)
                new_bg = self._blend(bg, old[3], blend)
                new_fg = self._blend(fg, old[1], blend)
                self._buffer.set(x + i, y + j, (old[0], new_fg, old[2], new_bg, old[4]))

    def is_visible(self, x, y):
        """
        Return whether the specified location is on the visible screen.

        :param x: The column (x coord) for the location to check.
        :param y: The line (y coord) for the location to check.
        """
        return ((0 <= x < self.width) and
                (self._start_line <= y < self._start_line + self.height))

    def move(self, x, y):
        """
        Move the drawing cursor to the specified position.

        :param x: The column (x coord) for the location to check.
        :param y: The line (y coord) for the location to check.
        """
        self._x = int(round(x * 2, 0))
        self._y = int(round(y * 2, 0))

    def draw(self, x, y, char=None, colour=7, bg=0, thin=False):
        """
        Draw a line from drawing cursor to the specified position.

        This uses a modified Bressenham algorithm, interpolating twice as many points to
        render down to anti-aliased characters when no character is specified,
        or uses standard algorithm plotting with the specified character.

        :param x: The column (x coord) for the location to check.
        :param y: The line (y coord) for the location to check.
        :param char: Optional character to use to draw the line.
        :param colour: Optional colour for plotting the line.
        :param bg: Optional background colour for plotting the line.
        :param thin: Optional width of anti-aliased line.
        """
        # Decide what type of line drawing to use.
        line_chars = (self._uni_line_chars if self._unicode_aware else
                      self._line_chars)

        # Define line end points.
        x0 = self._x
        y0 = self._y
        x1 = int(round(x * 2, 0))
        y1 = int(round(y * 2, 0))

        # Remember last point for next line.
        self._x = x1
        self._y = y1

        # Don't bother drawing anything if we're guaranteed to be off-screen
        # pylint: disable-next=too-many-boolean-expressions
        if ((x0 < 0 and x1 < 0) or (x0 >= self.width * 2 and x1 >= self.width * 2) or
                (y0 < 0 and y1 < 0) or (y0 >= self.height * 2 and y1 >= self.height * 2)):
            return

        dx = abs(x1 - x0)
        dy = abs(y1 - y0)

        sx = -1 if x0 > x1 else 1
        sy = -1 if y0 > y1 else 1

        def _get_start_char(cx, cy):
            needle = self.get_from(cx, cy)
            if needle is not None:
                letter, cfg, _, cbg = needle
                if colour == cfg and bg == cbg and chr(letter) in line_chars:
                    return line_chars.find(chr(letter))
            return 0

        def _fast_fill(start_x, end_x, iy):
            next_char = -1
            for ix in range(start_x, end_x):
                if ix % 2 == 0 or next_char == -1:
                    next_char = _get_start_char(ix // 2, iy // 2)
                next_char |= 2 ** abs(ix % 2) * 4 ** (iy % 2)
                if ix % 2 == 1:
                    self.print_at(line_chars[next_char], ix // 2, iy // 2, colour, bg=bg)
            if end_x % 2 == 1:
                self.print_at(line_chars[next_char], end_x // 2, iy // 2, colour, bg=bg)

        def _draw_on_x(ix, iy):
            err = dx
            px = ix - 2
            py = iy - 2
            next_char = 0
            while ix != x1:
                if ix < px or ix - px >= 2 or iy < py or iy - py >= 2:
                    px = ix & ~1
                    py = iy & ~1
                    next_char = _get_start_char(px // 2, py // 2)
                next_char |= 2 ** abs(ix % 2) * 4 ** (iy % 2)
                err -= 2 * dy
                if err < 0:
                    iy += sy
                    err += 2 * dx
                ix += sx

                if char is None:
                    self.print_at(line_chars[next_char],
                                  px // 2, py // 2, colour, bg=bg)
                else:
                    self.print_at(char, px // 2, py // 2, colour, bg=bg)

        def _draw_on_y(ix, iy):
            err = dy
            px = ix - 2
            py = iy - 2
            next_char = 0
            while iy != y1:
                if ix < px or ix - px >= 2 or iy < py or iy - py >= 2:
                    px = ix & ~1
                    py = iy & ~1
                    next_char = _get_start_char(px // 2, py // 2)
                next_char |= 2 ** abs(ix % 2) * 4 ** (iy % 2)
                err -= 2 * dx
                if err < 0:
                    ix += sx
                    err += 2 * dy
                iy += sy

                if char is None:
                    self.print_at(line_chars[next_char],
                                  px // 2, py // 2, colour, bg=bg)
                else:
                    self.print_at(char, px // 2, py // 2, colour, bg=bg)

        if dy == 0 and thin and char is None:
            # Fast-path for polygon filling
            _fast_fill(min(x0, x1), max(x0, x1), y0)
        elif dx > dy:
            _draw_on_x(x0, y0)
            if not thin:
                _draw_on_x(x0, y0 + 1)
        else:
            _draw_on_y(x0, y0)
            if not thin:
                _draw_on_y(x0 + 1, y0)

    def fill_polygon(self, polygons, colour=7, bg=0):
        """
        Draw a filled polygon.

        This function uses the scan line algorithm to create the polygon.  See
        https://www.cs.uic.edu/~jbell/CourseNotes/ComputerGraphics/PolygonFilling.html for details.

        :param polygons: A list of polygons (which are each a list of (x,y) coordinates for the
            points of the polygon) - i.e. nested list of 2-tuples.
        :param colour: The foreground colour to use for the polygon
        :param bg: The background colour to use for the polygon
        """
        def _add_edge(a, b):
            # Ignore horizontal lines - they are redundant
            if a[1] == b[1]:
                return

            # Ignore any edges that do not intersect the visible raster lines at all.
            if (a[1] < 0 and b[1] < 0) or (a[1] >= self.height and b[1] >= self.height):
                return

            # Save off the edge, always starting at the lowest value of y.
            new_edge = _DotDict()
            if a[1] < b[1]:
                new_edge.min_y = a[1]
                new_edge.max_y = b[1]
                new_edge.x = a[0]
                new_edge.dx = (b[0] - a[0]) / (b[1] - a[1]) / 2
            else:
                new_edge.min_y = b[1]
                new_edge.max_y = a[1]
                new_edge.x = b[0]
                new_edge.dx = (a[0] - b[0]) / (a[1] - b[1]) / 2
            edges.append(new_edge)

        # Create a table of all the edges in the polygon, sorted on smallest x.
        logger.debug("Processing polygon: %s", polygons)
        min_y = self.height
        max_y = -1
        edges = []
        last = None
        for polygon in polygons:
            # Ignore lines and polygons.
            if len(polygon) <= 2:
                continue

            # Ignore any polygons completely off the screen
            x, y = zip(*polygon)
            p_min_x = min(x)
            p_max_x = max(x)
            p_min_y = min(y)
            p_max_y = max(y)
            if p_max_x < 0 or p_min_x >= self.width or p_max_y < 0 or p_min_y > self.height:
                continue

            # Build up the edge list, maintaining bounding coordinates on the Y axis.
            min_y = min(p_min_y, min_y)
            max_y = max(p_max_y, max_y)
            for i, point in enumerate(polygon):
                if i != 0:
                    _add_edge(last, point)
                last = point
            _add_edge(polygon[0], polygon[-1])
            edges = sorted(edges, key=lambda e: e.x)

        # Check we still have something to do:
        if len(edges) == 0:
            return

        # Re-base all edges to visible Y coordinates of the screen.
        for edge in edges:
            if edge.min_y < 0:
                edge.x -= int(edge.min_y * 2) * edge.dx
                edge.min_y = 0
        min_y = max(0, min_y)
        max_y = min(max_y - min_y, self.height)

        logger.debug("Resulting edges: %s", edges)

        # Render each line in the bounding rectangle.
        for y in [min_y + (i / 2) for i in range(0, int(max_y) * 2)]:
            # Create a list of live edges (for drawing this raster line) and edges for next
            # iteration of the raster.
            live_edges = []
            new_edges = []
            for edge in edges:
                if edge.min_y <= y <= edge.max_y:
                    live_edges.append(edge)
                if y < edge.max_y:
                    new_edges.append(edge)

            # Draw the portions of the line that are inside the polygon.
            count = 0
            last_x = 0
            for edge in live_edges:
                # Draw the next segment
                if 0 <= y < self.height:
                    if edge.max_y != y:
                        count += 1
                        if count % 2 == 1:
                            last_x = edge.x
                        else:
                            # Don't bother drawing lines entirely off the screen.
                            if not ((last_x < 0 and edge.x < 0) or
                                    (last_x >= self.width and edge.x >= self.width)):
                                # Clip raster to screen width.
                                self.move(max(0, last_x), y)
                                self.draw(
                                    min(edge.x, self.width), y, colour=colour, bg=bg, thin=True)

                # Update the x location for this active edge.
                edge.x += edge.dx

            # Rely on the fact that we have the same dicts in both live_edges and new_edges, so
            # we just need to resort new_edges for the next iteration.
            edges = sorted(new_edges, key=lambda e: e.x)


class TemporaryCanvas(_AbstractCanvas):
    """
    A TemporaryCanvas is an object that can only be used to draw to a buffer.

    This class is desigend purely for use by dynamic renderers and so ignores some features of
    a full Canvas - most notably the screen related fhnction (e.g. the screen buffer and related
    properties).
    """

    def __init__(self, height, width):
        """
        :param height: The height of the screen buffer to be used.
        :param width: The width of the screen buffer to be used.
        """
        # Colours and unicode rendering are up to the user.  Pick defaults that won't limit them.
        super().__init__(height, width, None, 256, True)

    @property
    def plain_image(self):
        return self._buffer.plain_image

    @property
    def colour_map(self):
        return self._buffer.colour_map

    def refresh(self):
        pass

    def _reset(self):
        pass


class Canvas(_AbstractCanvas):
    """
    A Canvas is an object that can be used to draw to the screen. It maintains
    its own buffer that will be flushed to the screen when `refresh()` is
    called.
    """

    def __init__(self, screen, height, width, x=None, y=None):
        """
        :param screen: The underlying Screen that will be drawn to on refresh.
        :param height: The height of the screen buffer to be used.
        :param width: The width of the screen buffer to be used.
        :param x: The x position for the top left corner of the Canvas.
        :param y: The y position for the top left corner of the Canvas.

        If either of the x or y positions is not set, the Canvas will default
        to centring within the current Screen for that location.
        """
        # Save off the screen details.
        super().__init__(
            height, width, None, screen.colours, screen.unicode_aware)
        self._screen = screen
        self._dx = (screen.width - width) // 2 if x is None else x
        self._dy = (screen.height - height) // 2 if y is None else y

    def refresh(self):
        """
        Flush the canvas content to the underlying screen.
        """
        self._screen.block_transfer(self._buffer, self._dx, self._dy)

    def _reset(self):
        # Nothing needed for a Canvas
        pass

    @property
    def origin(self):
        """
        The location of top left corner of the canvas on the Screen.

        :returns: A tuple (x, y) of the location
        """
        return self._dx, self._dy


class Screen(_AbstractCanvas, metaclass=ABCMeta):
    """
    Class to track basic state of the screen.  This constructs the necessary
    resources to allow us to do the ASCII animations.

    This is an abstract class that will build the correct concrete class for
    you when you call :py:meth:`.wrapper`.  If needed, you can use the
    :py:meth:`~.Screen.open` and :py:meth:`~.Screen.close` methods for finer
    grained control of the construction and tidy up.

    Note that you need to define the required height for your screen buffer.
    This is important if you plan on using any Effects that will scroll the
    screen vertically (e.g. Scroll).  It must be big enough to handle the
    full scrolling of your selected Effect.
    """

    # Text attributes for use when printing to the Screen.
    A_BOLD = constants.A_BOLD
    A_NORMAL = constants.A_NORMAL
    A_REVERSE = constants.A_REVERSE
    A_UNDERLINE = constants.A_UNDERLINE

    # Text colours for use when printing to the Screen.
    COLOUR_DEFAULT = constants.COLOUR_DEFAULT
    COLOUR_BLACK = constants.COLOUR_BLACK
    COLOUR_RED = constants.COLOUR_RED
    COLOUR_GREEN = constants.COLOUR_GREEN
    COLOUR_YELLOW = constants.COLOUR_YELLOW
    COLOUR_BLUE = constants.COLOUR_BLUE
    COLOUR_MAGENTA = constants.COLOUR_MAGENTA
    COLOUR_CYAN = constants.COLOUR_CYAN
    COLOUR_WHITE = constants.COLOUR_WHITE

    # Standard extended key codes.
    KEY_ESCAPE = -1
    KEY_F1 = -2
    KEY_F2 = -3
    KEY_F3 = -4
    KEY_F4 = -5
    KEY_F5 = -6
    KEY_F6 = -7
    KEY_F7 = -8
    KEY_F8 = -9
    KEY_F9 = -10
    KEY_F10 = -11
    KEY_F11 = -12
    KEY_F12 = -13
    KEY_F13 = -14
    KEY_F14 = -15
    KEY_F15 = -16
    KEY_F16 = -17
    KEY_F17 = -18
    KEY_F18 = -19
    KEY_F19 = -20
    KEY_F20 = -21
    KEY_F21 = -22
    KEY_F22 = -23
    KEY_F23 = -24
    KEY_F24 = -25
    KEY_PRINT_SCREEN = -100
    KEY_INSERT = -101
    KEY_DELETE = -102
    KEY_HOME = -200
    KEY_END = -201
    KEY_LEFT = -203
    KEY_UP = -204
    KEY_RIGHT = -205
    KEY_DOWN = -206
    KEY_PAGE_UP = -207
    KEY_PAGE_DOWN = -208
    KEY_BACK = -300
    KEY_TAB = -301
    KEY_BACK_TAB = -302
    KEY_NUMPAD0 = -400
    KEY_NUMPAD1 = -401
    KEY_NUMPAD2 = -402
    KEY_NUMPAD3 = -403
    KEY_NUMPAD4 = -404
    KEY_NUMPAD5 = -405
    KEY_NUMPAD6 = -406
    KEY_NUMPAD7 = -407
    KEY_NUMPAD8 = -408
    KEY_NUMPAD9 = -409
    KEY_MULTIPLY = -410
    KEY_ADD = -411
    KEY_SUBTRACT = -412
    KEY_DECIMAL = -413
    KEY_DIVIDE = -414
    KEY_CAPS_LOCK = -500
    KEY_NUM_LOCK = -501
    KEY_SCROLL_LOCK = -502
    KEY_SHIFT = -600
    KEY_CONTROL = -601
    KEY_MENU = -602

    def __init__(self, height, width, buffer_height, unicode_aware):
        """
        Don't call this constructor directly.
        """
        super().__init__(
            height, width, buffer_height, 0, unicode_aware)

        # Initialize base class variables - e.g. those used for drawing.
        self.height = height
        self.width = width
        self._last_start_line = 0

        # Set up internal state for colours - used by children to determine
        # changes to text colour when refreshing the screen.
        self._colour = 0
        self._attr = 0
        self._bg = 0

        # tracking of current cursor position - used in screen refresh.
        self._cur_x = 0
        self._cur_y = 0

        # Control variables for playing out a set of Scenes.
        self._scenes = []
        self._scene_index = 0
        self._frame = 0
        self._idle_frame_count = 0
        self._forced_update = False
        self._unhandled_input = self._unhandled_event_default

    @classmethod
    def open(cls, height=None, catch_interrupt=False, unicode_aware=None):
        """
        Construct a new Screen for any platform.  This will just create the
        correct Screen object for your environment.  See :py:meth:`.wrapper` for
        a function to create and tidy up once you've finished with the Screen.

        :param height: The buffer height for this window (for testing only).
        :param catch_interrupt: Whether to catch and prevent keyboard
            interrupts.  Defaults to False to maintain backwards compatibility.
        :param unicode_aware: Whether the application can use unicode or not.
            If None, try to detect from the environment if UTF-8 is enabled.
        """
        if sys.platform == "win32":
            # Clone the standard output buffer so that we can do whatever we
            # need for the application, but restore the buffer at the end.
            # Note that we need to resize the clone to ensure that it is the
            # same size as the original in some versions of Windows.
            old_out = win32console.PyConsoleScreenBufferType(
                win32file.CreateFile("CONOUT$",
                                     win32file.GENERIC_READ | win32file.GENERIC_WRITE,
                                     win32file.FILE_SHARE_WRITE,
                                     None,
                                     win32file.OPEN_ALWAYS,
                                     0,
                                     None))
            try:
                info = old_out.GetConsoleScreenBufferInfo()
            except pywintypes.error:
                info = None
            win_out = win32console.CreateConsoleScreenBuffer()
            if info:
                win_out.SetConsoleScreenBufferSize(info['Size'])
            else:
                win_out.SetStdHandle(win32console.STD_OUTPUT_HANDLE)
            win_out.SetConsoleActiveScreenBuffer()

            # Get the standard input buffer.
            win_in = win32console.PyConsoleScreenBufferType(
                win32file.CreateFile("CONIN$",
                                     win32file.GENERIC_READ | win32file.GENERIC_WRITE,
                                     win32file.FILE_SHARE_READ,
                                     None,
                                     win32file.OPEN_ALWAYS,
                                     0,
                                     None))
            win_in.SetStdHandle(win32console.STD_INPUT_HANDLE)

            # Hide the cursor.
            win_out.SetConsoleCursorInfo(1, 0)

            # Disable scrolling
            out_mode = win_out.GetConsoleMode()
            win_out.SetConsoleMode(
                out_mode & ~ win32console.ENABLE_WRAP_AT_EOL_OUTPUT)

            # Enable mouse input, disable quick-edit mode and disable ctrl-c
            # if needed.
            in_mode = win_in.GetConsoleMode()
            new_mode = (in_mode | win32console.ENABLE_MOUSE_INPUT |
                        ENABLE_EXTENDED_FLAGS)
            new_mode &= ~ENABLE_QUICK_EDIT_MODE
            if catch_interrupt:
                # Ignore ctrl-c handlers if specified.
                new_mode &= ~win32console.ENABLE_PROCESSED_INPUT
            win_in.SetConsoleMode(new_mode)

            screen = _WindowsScreen(win_out, win_in, height, old_out, in_mode,
                                    unicode_aware=unicode_aware)
        else:
            # Reproduce curses.wrapper()
            stdscr = curses.initscr()
            curses.noecho()
            # Shouldn't fail on real systems.  This code is for running tests in CI pipelines.
            try:
                curses.cbreak()
            except curses.error:
                pass
            stdscr.keypad(1)

            # Fed up with linters complaining about original curses code - trying to be a bit better...
            # noinspection PyBroadException
            # pylint: disable=broad-except
            try:
                curses.start_color()
            except Exception as e:
                logger.debug(e)
            screen = _CursesScreen(stdscr, height,
                                   catch_interrupt=catch_interrupt,
                                   unicode_aware=unicode_aware)

        return screen

    @abstractmethod
    def close(self, restore=True):
        """
        Close down this Screen and tidy up the environment as required.

        :param restore: whether to restore the environment or not.
        """

    @classmethod
    def wrapper(cls, func, height=None, catch_interrupt=False, arguments=None,
                unicode_aware=None):
        """
        Construct a new Screen for any platform.  This will initialize the
        Screen, call the specified function and then tidy up the system as
        required when the function exits.

        :param func: The function to call once the Screen has been created.
        :param height: The buffer height for this Screen (only for test purposes).
        :param catch_interrupt: Whether to catch and prevent keyboard
            interrupts.  Defaults to False to maintain backwards compatibility.
        :param arguments: Optional arguments list to pass to func (after the
            Screen object).
        :param unicode_aware: Whether the application can use unicode or not.
            If None, try to detect from the environment if UTF-8 is enabled.
        """
        screen = Screen.open(height,
                             catch_interrupt=catch_interrupt,
                             unicode_aware=unicode_aware)
        restore = True
        try:
            try:
                if arguments:
                    return func(screen, *arguments)
                else:
                    return func(screen)
            except ResizeScreenError:
                restore = False
                raise
        finally:
            screen.close(restore)

    def _reset(self):
        """
        Reset the Screen.
        """
        self._last_start_line = 0
        self._colour = None
        self._attr = None
        self._bg = None
        self._cur_x = None
        self._cur_y = None

    def refresh(self):
        """
        Refresh the screen.
        """
        # Scroll the screen now - we've already sorted the double-buffer to reflect this change.
        if self._last_start_line != self._start_line:
            self._scroll(self._start_line - self._last_start_line)
            self._last_start_line = self._start_line

        # Now draw any deltas to the scrolled screen.  Note that CJK character sets sometimes
        # use double-width characters, so don't try to draw the next 2nd char (of 0 width).
        for y, x in self._buffer.deltas(0, self.height):
            new_cell = self._buffer.get(x, y)
            if new_cell[4] > 0:
                self._change_colours(new_cell[1], new_cell[2], new_cell[3])
                self._print_at(new_cell[0], x, y, new_cell[4])

        # Resynch for next refresh.
        self._buffer.sync()

    def clear(self):
        """
        Clear the Screen of all content.

        Note that this will instantly clear the Screen and reset all buffers to the default state,
        without waiting for you to call :py:meth:`~.Screen.refresh`.  It is designed for use once
        at the start of your application to reset all buffers and the screen to a known state.

        If you want to clear parts, or all, of the Screen inside your application without any
        flicker, use :py:meth:`~.Screen.clear_buffer` instead.
        """
        # Clear the actual terminal
        self.reset()
        self._change_colours(Screen.COLOUR_WHITE, 0, 0)
        self._clear()

    def get_key(self):
        """
        Check for a key without waiting.  This method is deprecated.  Use
        :py:meth:`.get_event` instead.
        """
        event = self.get_event()
        if event and isinstance(event, KeyboardEvent):
            return event.key_code
        return None

    @abstractmethod
    def get_event(self):
        """
        Check for any events (e.g. key-press or mouse movement) without waiting.

        :returns: A :py:obj:`.Event` object if anything was detected, otherwise
                  it returns None.
        """

    @staticmethod
    def ctrl(char):
        """
        Calculate the control code for a given key.  For example, this converts
        "a" to 1 (which is the code for ctrl-a).

        :param char: The key to convert to a control code.
        :return: The control code as an integer or None if unknown.
        """
        # Convert string to int... assuming any non-integer is a string.
        # TODO: Consider asserting a more rigorous test without falling back to past basestring.
        if not isinstance(char, int):
            char = ord(char.upper())

        # Only deal with the characters between '@' and '_'
        return char & 0x1f if 64 <= char <= 95 else None

    @abstractmethod
    def has_resized(self):
        """
        Check whether the screen has been re-sized.

        :returns: True when the screen has been re-sized since the last check.
        """

    def getch(self, x, y):
        """
        Get the character at a specified location.  This method is deprecated.
        Use :py:meth:`.get_from` instead.

        :param x: The x coordinate.
        :param y: The y coordinate.
        """
        return self.get_from(x, y)

    def putch(self, text, x, y, colour=7, attr=0, bg=0, transparent=False):
        """
        Print text at the specified location.  This method is deprecated.  Use
        :py:meth:`.print_at` instead.

        :param text: The (single line) text to be printed.
        :param x: The column (x coord) for the start of the text.
        :param y: The line (y coord) for the start of the text.
        :param colour: The colour of the text to be displayed.
        :param attr: The cell attribute of the text to be displayed.
        :param bg: The background colour of the text to be displayed.
        :param transparent: Whether to print spaces or not, thus giving a
            transparent effect.
        """
        self.print_at(text, x, y, colour, attr, bg, transparent)

    @staticmethod
    def _unhandled_event_default(event):
        """
        Default unhandled event handler for handling simple scene navigation.
        """
        if isinstance(event, KeyboardEvent):
            c = event.key_code
            if c in (ord("X"), ord("x"), ord("Q"), ord("q")):
                raise StopApplication("User terminated app")
            if c in (ord(" "), ord("\n"), ord("\r")):
                raise NextScene()

    def play(self, scenes, stop_on_resize=False, unhandled_input=None,
             start_scene=None, repeat=True, allow_int=False):
        """
        Play a set of scenes.

        This is effectively a helper function to wrap :py:meth:`.set_scenes` and
        :py:meth:`.draw_next_frame` to simplify animation for most applications.

        :param scenes: a list of :py:obj:`.Scene` objects to play.
        :param stop_on_resize: Whether to stop when the screen is resized.
            Default is to carry on regardless - which will typically result
            in an error. This is largely done for back-compatibility.
        :param unhandled_input: Function to call for any input not handled
            by the Scenes/Effects being played.  Defaults to a function that
            closes the application on "Q" or "X" being pressed.
        :param start_scene: The old Scene to start from.  This must have name
            that matches the name of one of the Scenes passed in.
        :param repeat: Whether to repeat the Scenes once it has reached the end.
            Defaults to True.
        :param allow_int: Allow input to interrupt frame rate delay.

        :raises ResizeScreenError: if the screen is resized (and allowed by
            stop_on_resize).

        The unhandled input function just takes one parameter - the input
        event that was not handled.
        """
        # Initialise the Screen for animation.
        self.set_scenes(
            scenes, unhandled_input=unhandled_input, start_scene=start_scene)

        # Mainline loop for animations
        try:
            while True:
                a = time.time()
                self.draw_next_frame(repeat=repeat)
                if self.has_resized():
                    if stop_on_resize:
                        self._scenes[self._scene_index].exit()
                        raise ResizeScreenError("Screen resized",
                                                self._scenes[self._scene_index])
                b = time.time()
                if b - a < 0.05:
                    # Just in case time has jumped (e.g. time change), ensure we only delay for 0.05s
                    pause = min(0.05, a + 0.05 - b)
                    if allow_int:
                        self.wait_for_input(pause)
                    else:
                        time.sleep(pause)
        except StopApplication:
            # Time to stop  - just exit the function.
            return

    def set_scenes(self, scenes, unhandled_input=None, start_scene=None):
        """
        Remember a set of scenes to be played.  This must be called before
        using :py:meth:`.draw_next_frame`.

        :param scenes: a list of :py:obj:`.Scene` objects to play.
        :param unhandled_input: Function to call for any input not handled
            by the Scenes/Effects being played.  Defaults to a function that
            closes the application on "Q" or "X" being pressed.
        :param start_scene: The old Scene to start from.  This must have name
            that matches the name of one of the Scenes passed in.

        :raises ResizeScreenError: if the screen is resized (and allowed by
            stop_on_resize).

        The unhandled input function just takes one parameter - the input
        event that was not handled.
        """
        # Save off the scenes now.
        self._scenes = scenes

        # Set up default unhandled input handler if needed.
        if unhandled_input is None:
            # Check that none of the Effects is incompatible with the default
            # handler.
            safe = True
            for scene in self._scenes:
                for effect in scene.effects:
                    safe &= effect.safe_to_default_unhandled_input
            if safe:
                unhandled_input = self._unhandled_event_default
        self._unhandled_input = unhandled_input

        # Find the starting scene.  Default to first if no match.
        self._scene_index = 0
        if start_scene is not None:
            for i, scene in enumerate(scenes):
                if scene.name == start_scene.name:
                    self._scene_index = i
                    break

        # Reset the Scene - this allows the original scene to pick up old
        # values on resizing.
        self._scenes[self._scene_index].reset(
            old_scene=start_scene, screen=self)

        # Reset other internal state for the animation
        self._frame = 0
        self._idle_frame_count = 0
        self._forced_update = False
        self.clear()

    def draw_next_frame(self, repeat=True):
        """
        Draw the next frame in the currently configured Scenes. You must call
        :py:meth:`.set_scenes` before using this for the first time.

        :param repeat: Whether to repeat the Scenes once it has reached the end.
            Defaults to True.

        :raises StopApplication: if the application should be terminated.
        """
        scene = self._scenes[self._scene_index]
        try:
            # Check for an event now and remember for refresh reasons.
            event = self.get_event()
            got_event = event is not None

            # Now process all the input events
            while event is not None:
                event = scene.process_event(event)
                if event is not None and self._unhandled_input is not None:
                    self._unhandled_input(event)
                event = self.get_event()

            # Only bother with a refresh if there was an event to process or
            # we have to refresh due to the refresh limit required for an
            # Effect.
            self._frame += 1
            self._idle_frame_count -= 1
            if got_event or self._idle_frame_count <= 0 or self._forced_update:
                self._forced_update = False
                self._idle_frame_count = 1000000
                for effect in scene.effects:
                    # Update the effect and delete if needed.
                    effect.update(self._frame)
                    if effect.delete_count is not None:
                        effect.delete_count -= 1
                        if effect.delete_count <= 0:
                            scene.remove_effect(effect)

                    # Sort out when we next _need_ to do a refresh.
                    if effect.frame_update_count > 0:
                        self._idle_frame_count = min(self._idle_frame_count,
                                                     effect.frame_update_count)
                self.refresh()

            if 0 < scene.duration <= self._frame:
                raise NextScene()
        except NextScene as e:
            # Tidy up the current scene.
            scene.exit()

            # Find the specified next Scene
            if e.name is None:
                # Just allow next iteration of loop
                self._scene_index += 1
                if self._scene_index >= len(self._scenes):
                    if repeat:
                        self._scene_index = 0
                    else:
                        raise StopApplication("Repeat disabled") from e
            else:
                # Find the required scene.
                for i, scene in enumerate(self._scenes):
                    if scene.name == e.name:
                        self._scene_index = i
                        break
                else:
                    raise RuntimeError(f"Could not find Scene: '{e.name}'") from e

            # Reset the screen if needed.
            scene = self._scenes[self._scene_index]
            scene.reset()
            self._frame = 0
            self._idle_frame_count = 0
            if scene.clear:
                self.clear()

    @property
    def current_scene(self):
        """
        :return: The scene currently being rendered. To be used in conjunction
                 with :py:meth:`.draw_next_frame`.
        """
        return self._scenes[self._scene_index]

    def force_update(self, full_refresh=False):
        """
        Force the Screen to redraw the current Scene on the next call to
        draw_next_frame, overriding the frame_update_count value for all the
        Effects.

        :param full_refresh: if True force the whole screen to redraw.
        """
        self._forced_update = True
        if full_refresh:
            self._buffer.invalidate()

    @abstractmethod
    def _change_colours(self, colour, attr, bg):
        """
        Change current colour if required.

        :param colour: New colour to use.
        :param attr: New attributes to use.
        :param bg: New background colour to use.
        """

    @abstractmethod
    def wait_for_input(self, timeout):
        """
        Wait until there is some input or the timeout is hit.

        :param timeout: Time to wait for input in seconds (floating point).
        """

    @abstractmethod
    def _print_at(self, text, x, y, width):
        """
        Print string at the required location.

        :param text: The text string to print.
        :param x: The x coordinate
        :param y: The Y coordinate
        :param width: The width of the character (for dual-width glyphs in CJK languages).
        """

    @abstractmethod
    def _clear(self):
        """
        Clear the window.
        """

    @abstractmethod
    def _scroll(self, lines):
        """
        Scroll the window up or down.

        :param lines: Number of lines to scroll.  Negative numbers scroll down.
        """

    @abstractmethod
    def set_title(self, title):
        """
        Set the title for this terminal/console session.  This will typically
        change the text displayed in the window title bar.

        :param title: The title to be set.
        """


class ManagedScreen():
    """
    Decorator and class to create a managed Screen. It can be used in
    two ways. If used as a method decorator it will create and open a new Screen,
    pass the screen to the method as a keyword argument, and close the
    screen when the method has completed. If used with the with statement
    the class will create and open a new Screen, return the screen for
    using in the block, and close the screen when the statement ends.
    Note that any arguments are in this class so that you can use it
    as a decorator or using the with statment. No arguments are required
    to use.
    """

    def __init__(self, func=lambda: None):
        """
        :param func: The function to call once the Screen has been created.
        """
        update_wrapper(self, func)
        self.func = func
        self.screen = None

    def __get__(self, obj, objtype):
        """
        Class decorator method, so we can use the class in a with statement.

        See https://stackoverflow.com/a/3296318/4994021 for details.
        """
        return partial(self.__call__, obj)

    def __call__(self, *args, **kwargs):
        screen = Screen.open()
        kwargs["screen"] = screen
        output = self.func(*args, **kwargs)
        screen.close()
        return output

    def __enter__(self):
        """
        Method used for with statement
        """
        self.screen = Screen.open()
        return self.screen

    def __exit__(self, etype, value, traceback):
        """
        Method used for with statement
        """
        self.screen.close()


if sys.platform == "win32":
    import win32con
    import win32console
    import win32event
    import win32file
    import pywintypes

    class _WindowsScreen(Screen):
        """
        Windows screen implementation.
        """

        # Virtual key code mapping.
        _KEY_MAP = {
            win32con.VK_ESCAPE: Screen.KEY_ESCAPE,
            win32con.VK_F1: Screen.KEY_F1,
            win32con.VK_F2: Screen.KEY_F2,
            win32con.VK_F3: Screen.KEY_F3,
            win32con.VK_F4: Screen.KEY_F4,
            win32con.VK_F5: Screen.KEY_F5,
            win32con.VK_F6: Screen.KEY_F6,
            win32con.VK_F7: Screen.KEY_F7,
            win32con.VK_F8: Screen.KEY_F8,
            win32con.VK_F9: Screen.KEY_F9,
            win32con.VK_F10: Screen.KEY_F10,
            win32con.VK_F11: Screen.KEY_F11,
            win32con.VK_F12: Screen.KEY_F12,
            win32con.VK_F13: Screen.KEY_F13,
            win32con.VK_F14: Screen.KEY_F14,
            win32con.VK_F15: Screen.KEY_F15,
            win32con.VK_F16: Screen.KEY_F16,
            win32con.VK_F17: Screen.KEY_F17,
            win32con.VK_F18: Screen.KEY_F18,
            win32con.VK_F19: Screen.KEY_F19,
            win32con.VK_F20: Screen.KEY_F20,
            win32con.VK_F21: Screen.KEY_F21,
            win32con.VK_F22: Screen.KEY_F22,
            win32con.VK_F23: Screen.KEY_F23,
            win32con.VK_F24: Screen.KEY_F24,
            win32con.VK_PRINT: Screen.KEY_PRINT_SCREEN,
            win32con.VK_INSERT: Screen.KEY_INSERT,
            win32con.VK_DELETE: Screen.KEY_DELETE,
            win32con.VK_HOME: Screen.KEY_HOME,
            win32con.VK_END: Screen.KEY_END,
            win32con.VK_LEFT: Screen.KEY_LEFT,
            win32con.VK_UP: Screen.KEY_UP,
            win32con.VK_RIGHT: Screen.KEY_RIGHT,
            win32con.VK_DOWN: Screen.KEY_DOWN,
            win32con.VK_PRIOR: Screen.KEY_PAGE_UP,
            win32con.VK_NEXT: Screen.KEY_PAGE_DOWN,
            win32con.VK_BACK: Screen.KEY_BACK,
            win32con.VK_TAB: Screen.KEY_TAB
        }

        _EXTRA_KEY_MAP = {
            win32con.VK_NUMPAD0: Screen.KEY_NUMPAD0,
            win32con.VK_NUMPAD1: Screen.KEY_NUMPAD1,
            win32con.VK_NUMPAD2: Screen.KEY_NUMPAD2,
            win32con.VK_NUMPAD3: Screen.KEY_NUMPAD3,
            win32con.VK_NUMPAD4: Screen.KEY_NUMPAD4,
            win32con.VK_NUMPAD5: Screen.KEY_NUMPAD5,
            win32con.VK_NUMPAD6: Screen.KEY_NUMPAD6,
            win32con.VK_NUMPAD7: Screen.KEY_NUMPAD7,
            win32con.VK_NUMPAD8: Screen.KEY_NUMPAD8,
            win32con.VK_NUMPAD9: Screen.KEY_NUMPAD9,
            win32con.VK_MULTIPLY: Screen.KEY_MULTIPLY,
            win32con.VK_ADD: Screen.KEY_ADD,
            win32con.VK_SUBTRACT: Screen.KEY_SUBTRACT,
            win32con.VK_DECIMAL: Screen.KEY_DECIMAL,
            win32con.VK_DIVIDE: Screen.KEY_DIVIDE,
            win32con.VK_CAPITAL: Screen.KEY_CAPS_LOCK,
            win32con.VK_NUMLOCK: Screen.KEY_NUM_LOCK,
            win32con.VK_SCROLL: Screen.KEY_SCROLL_LOCK,
            win32con.VK_SHIFT: Screen.KEY_SHIFT,
            win32con.VK_CONTROL: Screen.KEY_CONTROL,
            win32con.VK_MENU: Screen.KEY_MENU,
        }

        # Foreground colour lookup table.
        _COLOURS = {
            Screen.COLOUR_DEFAULT: (win32console.FOREGROUND_RED |
                                    win32console.FOREGROUND_GREEN |
                                    win32console.FOREGROUND_BLUE),
            Screen.COLOUR_BLACK: 0,
            Screen.COLOUR_RED: win32console.FOREGROUND_RED,
            Screen.COLOUR_GREEN: win32console.FOREGROUND_GREEN,
            Screen.COLOUR_YELLOW: (win32console.FOREGROUND_RED |
                                   win32console.FOREGROUND_GREEN),
            Screen.COLOUR_BLUE: win32console.FOREGROUND_BLUE,
            Screen.COLOUR_MAGENTA: (win32console.FOREGROUND_RED |
                                    win32console.FOREGROUND_BLUE),
            Screen.COLOUR_CYAN: (win32console.FOREGROUND_BLUE |
                                 win32console.FOREGROUND_GREEN),
            Screen.COLOUR_WHITE: (win32console.FOREGROUND_RED |
                                  win32console.FOREGROUND_GREEN |
                                  win32console.FOREGROUND_BLUE)
        }

        # Background colour lookup table.
        _BG_COLOURS = {
            Screen.COLOUR_DEFAULT: 0,
            Screen.COLOUR_BLACK: 0,
            Screen.COLOUR_RED: win32console.BACKGROUND_RED,
            Screen.COLOUR_GREEN: win32console.BACKGROUND_GREEN,
            Screen.COLOUR_YELLOW: (win32console.BACKGROUND_RED |
                                   win32console.BACKGROUND_GREEN),
            Screen.COLOUR_BLUE: win32console.BACKGROUND_BLUE,
            Screen.COLOUR_MAGENTA: (win32console.BACKGROUND_RED |
                                    win32console.BACKGROUND_BLUE),
            Screen.COLOUR_CYAN: (win32console.BACKGROUND_BLUE |
                                 win32console.BACKGROUND_GREEN),
            Screen.COLOUR_WHITE: (win32console.BACKGROUND_RED |
                                  win32console.BACKGROUND_GREEN |
                                  win32console.BACKGROUND_BLUE)
        }

        # Attribute lookup table
        _ATTRIBUTES = {
            0: lambda x: x,
            Screen.A_BOLD: lambda x: x | win32console.FOREGROUND_INTENSITY,
            Screen.A_NORMAL: lambda x: x,
            # Windows console uses a bitmap where background is the top nibble,
            # so we can reverse by swapping nibbles.
            Screen.A_REVERSE: lambda x: ((x & 15) * 16) + ((x & 240) // 16),
            Screen.A_UNDERLINE: lambda x: x
        }

        def __init__(self, stdout, stdin, buffer_height, old_out, old_in,
                     unicode_aware=False):
            """
            :param stdout: The win32console PyConsoleScreenBufferType object for stdout.
            :param stdin: The win32console PyConsoleScreenBufferType object for stdin.
            :param buffer_height: The buffer height for this window (for testing only).
            :param old_out: The original win32console PyConsoleScreenBufferType object for stdout
                that should be restored on exit.
            :param old_in: The original stdin state that should be restored on exit.
            :param unicode_aware: Whether this Screen can use unicode or not.
            """
            # Save off the screen details and set up the scrolling pad.
            info = stdout.GetConsoleScreenBufferInfo()['Window']
            width = info.Right - info.Left + 1
            height = info.Bottom - info.Top + 1

            # Detect UTF-8 if needed and then construct the Screen.
            if unicode_aware is None:
                # According to MSDN, 65001 is the Windows UTF-8 code page.
                unicode_aware = win32console.GetConsoleCP() == 65001
            super().__init__(
                height, width, buffer_height, unicode_aware)

            # Save off the console details.
            self._stdout = stdout
            self._stdin = stdin
            self._last_width = width
            self._last_height = height
            self._old_out = old_out
            self._old_in = old_in

            # Windows is limited to the ANSI colour set.
            self.colours = 8

            # Opt for compatibility with Linux by default
            self._map_all = False

            # Set of keys currently pressed.
            self._keys = set()

        def close(self, restore=True):
            """
            Close down this Screen and tidy up the environment as required.

            :param restore: whether to restore the environment or not.
            """
            if restore:
                # Reset the original screen settings.
                self._old_out.SetConsoleActiveScreenBuffer()
                self._stdin.SetConsoleMode(self._old_in)

        def map_all_keys(self, state):
            """
            Switch on extended keyboard mapping for this Screen.

            :param state: Boolean flag where true means map all keys.

            Enabling this setting will allow Windows to tell you when any key
            is pressed, including metakeys (like shift and control) and whether
            the numeric keypad keys have been used.

            .. warning::

                Using this means your application will not be compatible across
                all platforms.
            """
            self._map_all = state

        def get_event(self):
            """
            Check for any event without waiting.
            """
            # Look for a new event and consume it if there is one.
            while len(self._stdin.PeekConsoleInput(1)) > 0:
                event = self._stdin.ReadConsoleInput(1)[0]
                if event.EventType == win32console.KEY_EVENT:
                    # Pasting unicode text appears to just generate key-up
                    # events (as if you had pressed the Alt keys plus the
                    # keypad code for the character), but the rest of the
                    # console input simply doesn't
                    # work with key up events - e.g. misses keyboard repeats.
                    #
                    # We therefore allow any key press (i.e. KeyDown) event and
                    # _any_ event that appears to have popped up from nowhere
                    # as long as the Alt key is present.
                    key_code = ord(event.Char)
                    logger.debug("Processing key: %x", key_code)
                    if (event.KeyDown or
                            (key_code > 0 and key_code not in self._keys and
                             event.VirtualKeyCode == win32con.VK_MENU)):
                        # Record any keys that were pressed.
                        if event.KeyDown:
                            self._keys.add(key_code)

                        # Translate keys into a KeyboardEvent object.
                        if event.VirtualKeyCode in self._KEY_MAP:
                            key_code = self._KEY_MAP[event.VirtualKeyCode]

                        # Sadly, we are limited to Linux terminal input and so
                        # can't return modifier states in a cross-platform way.
                        # If the user decided not to be cross-platform, so be
                        # it, otherwise map some standard bindings for extended
                        # keys.
                        if (self._map_all and
                                event.VirtualKeyCode in self._EXTRA_KEY_MAP):
                            key_code = self._EXTRA_KEY_MAP[event.VirtualKeyCode]
                        else:
                            if (event.VirtualKeyCode == win32con.VK_TAB and
                                    event.ControlKeyState &
                                    win32con.SHIFT_PRESSED):
                                key_code = Screen.KEY_BACK_TAB

                        # Don't return anything if we didn't have a valid
                        # mapping.
                        if key_code:
                            return KeyboardEvent(key_code)
                    else:
                        # Tidy up any key that was previously pressed.  At
                        # start-up, we may be mid-key, so can't assume this must
                        # always match up.
                        if key_code in self._keys:
                            self._keys.remove(key_code)

                elif event.EventType == win32console.MOUSE_EVENT:
                    # Translate into a MouseEvent object.
                    logger.debug("Processing mouse: %d, %d",
                                 event.MousePosition.X, event.MousePosition.Y)
                    button = 0
                    if event.EventFlags == 0:
                        # Button pressed - translate it.
                        if (event.ButtonState &
                                win32con.FROM_LEFT_1ST_BUTTON_PRESSED != 0):
                            button |= MouseEvent.LEFT_CLICK
                        if (event.ButtonState &
                                win32con.RIGHTMOST_BUTTON_PRESSED != 0):
                            button |= MouseEvent.RIGHT_CLICK
                    elif event.EventFlags & win32con.DOUBLE_CLICK != 0:
                        button |= MouseEvent.DOUBLE_CLICK

                    return MouseEvent(event.MousePosition.X,
                                      event.MousePosition.Y,
                                      button)

            # If we get here, we've fully processed the event queue and found
            # nothing interesting.
            return None

        def has_resized(self):
            """
            Check whether the screen has been re-sized.
            """
            # Get the current Window dimensions and check them against last
            # time.
            re_sized = False
            info = self._stdout.GetConsoleScreenBufferInfo()['Window']
            width = info.Right - info.Left + 1
            height = info.Bottom - info.Top + 1
            if width != self._last_width or height != self._last_height:
                re_sized = True
            return re_sized

        def _change_colours(self, colour, attr, bg):
            """
            Change current colour if required.

            :param colour: New colour to use.
            :param attr: New attributes to use.
            :param bg: New background colour to use.
            """
            # Change attribute first as this will reset colours when swapping
            # modes.
            if colour != self._colour or attr != self._attr or self._bg != bg:
                new_attr = self._ATTRIBUTES[attr](
                    self._COLOURS[colour] + self._BG_COLOURS[bg])
                self._stdout.SetConsoleTextAttribute(new_attr)
                self._attr = attr
                self._colour = colour
                self._bg = bg

        def _print_at(self, text, x, y, width):
            """
            Print string at the required location.

            :param text: The text string to print.
            :param x: The x coordinate
            :param y: The Y coordinate
            :param width: The width of the character (for dual-width glyphs in CJK languages).
            """
            # We can throw temporary errors on resizing, so catch and ignore
            # them on the assumption that we'll resize shortly.
            try:
                # Move the cursor if necessary
                if x != self._cur_x or y != self._cur_y:
                    self._stdout.SetConsoleCursorPosition(
                        win32console.PyCOORDType(x, y))

                # Print the text at the required location and update the current
                # position.
                self._stdout.WriteConsole(text)
                self._cur_x = x + width
                self._cur_y = y
            except pywintypes.error:
                pass

        def wait_for_input(self, timeout):
            """
            Wait until there is some input or the timeout is hit.

            :param timeout: Time to wait for input in seconds (floating point).
            """
            rc = win32event.WaitForSingleObject(self._stdin, int(timeout * 1000))
            if rc not in [0, 258]:
                raise RuntimeError(rc)

        def _scroll(self, lines):
            """
            Scroll the window up or down.

            :param lines: Number of lines to scroll.  Negative numbers scroll
                down.
            """
            # Scroll the visible screen up by one line
            info = self._stdout.GetConsoleScreenBufferInfo()['Window']
            rectangle = win32console.PySMALL_RECTType(
                info.Left, info.Top + lines, info.Right, info.Bottom)
            new_pos = win32console.PyCOORDType(0, info.Top)
            self._stdout.ScrollConsoleScreenBuffer(
                rectangle, None, new_pos, " ", 0)

        def _clear(self):
            """
            Clear the terminal.
            """
            info = self._stdout.GetConsoleScreenBufferInfo()['Window']
            width = info.Right - info.Left + 1
            height = info.Bottom - info.Top + 1
            box_size = width * height
            self._stdout.FillConsoleOutputAttribute(
                0, box_size, win32console.PyCOORDType(0, 0))
            self._stdout.FillConsoleOutputCharacter(
                " ", box_size, win32console.PyCOORDType(0, 0))
            self._stdout.SetConsoleCursorPosition(
                win32console.PyCOORDType(0, 0))

        def set_title(self, title):
            """
            Set the title for this terminal/console session.  This will
            typically change the text displayed in the window title bar.

            :param title: The title to be set.
            """
            win32console.SetConsoleTitle(title)

else:
    # UNIX compatible platform - use curses
    import curses
    import select
    import termios

    class _CursesScreen(Screen):
        """
        Curses screen implementation.
        """

        # Virtual key code mapping.
        _KEY_MAP = {
            27: Screen.KEY_ESCAPE,
            curses.KEY_F1: Screen.KEY_F1,
            curses.KEY_F2: Screen.KEY_F2,
            curses.KEY_F3: Screen.KEY_F3,
            curses.KEY_F4: Screen.KEY_F4,
            curses.KEY_F5: Screen.KEY_F5,
            curses.KEY_F6: Screen.KEY_F6,
            curses.KEY_F7: Screen.KEY_F7,
            curses.KEY_F8: Screen.KEY_F8,
            curses.KEY_F9: Screen.KEY_F9,
            curses.KEY_F10: Screen.KEY_F10,
            curses.KEY_F11: Screen.KEY_F11,
            curses.KEY_F12: Screen.KEY_F12,
            curses.KEY_F13: Screen.KEY_F13,
            curses.KEY_F14: Screen.KEY_F14,
            curses.KEY_F15: Screen.KEY_F15,
            curses.KEY_F16: Screen.KEY_F16,
            curses.KEY_F17: Screen.KEY_F17,
            curses.KEY_F18: Screen.KEY_F18,
            curses.KEY_F19: Screen.KEY_F19,
            curses.KEY_F20: Screen.KEY_F20,
            curses.KEY_F21: Screen.KEY_F21,
            curses.KEY_F22: Screen.KEY_F22,
            curses.KEY_F23: Screen.KEY_F23,
            curses.KEY_F24: Screen.KEY_F24,
            curses.KEY_PRINT: Screen.KEY_PRINT_SCREEN,
            curses.KEY_IC: Screen.KEY_INSERT,
            curses.KEY_DC: Screen.KEY_DELETE,
            curses.KEY_HOME: Screen.KEY_HOME,
            curses.KEY_END: Screen.KEY_END,
            curses.KEY_LEFT: Screen.KEY_LEFT,
            curses.KEY_UP: Screen.KEY_UP,
            curses.KEY_RIGHT: Screen.KEY_RIGHT,
            curses.KEY_DOWN: Screen.KEY_DOWN,
            curses.KEY_PPAGE: Screen.KEY_PAGE_UP,
            curses.KEY_NPAGE: Screen.KEY_PAGE_DOWN,
            curses.KEY_BACKSPACE: Screen.KEY_BACK,
            9: Screen.KEY_TAB,
            curses.KEY_BTAB: Screen.KEY_BACK_TAB
            # Terminals translate keypad keys, so no need for a special
            # mapping here.

            # Terminals don't transmit meta keys (like control, shift, etc), so
            # there's no translation for them either.
        }

        def __init__(self, win, height=None, catch_interrupt=False,
                     unicode_aware=False):
            """
            :param win: The window object as returned by the curses wrapper method.
            :param height: The height of the screen buffer to be used (for teesting only).
            :param catch_interrupt: Whether to catch SIGINT or not.
            :param unicode_aware: Whether this Screen can use unicode or not.
            """
            # Determine unicode support if needed.
            if unicode_aware is None:
                try:
                    encoding = getlocale()[1]
                    if not encoding:
                        encoding = getdefaultlocale()[1]
                except ValueError:
                    encoding = os.environ.get("LC_CTYPE")
                unicode_aware = (encoding is not None and
                                 encoding.lower() == "utf-8")

            # Save off the screen details.
            super().__init__(
                win.getmaxyx()[0], win.getmaxyx()[1], height, unicode_aware)
            self._screen = win
            self._screen.keypad(1)

            # Set up basic colour schemes.
            self.colours = curses.COLORS

            # Disable the cursor.
            curses.curs_set(0)

            # Non-blocking key checks.
            self._screen.nodelay(1)

            # Store previous handlers for restoration at close
            self._signal_state = _SignalState()

            # Set up signal handler for screen resizing.
            self._re_sized = False
            self._signal_state.set(signal.SIGWINCH, self._resize_handler)

            # Set up signal handler for job pause/resume.
            self._signal_state.set(signal.SIGCONT, self._continue_handler)

            # Catch SIGINTs and translated them to ctrl-c if needed.
            if catch_interrupt:
                # Ignore SIGINT (ctrl-c) and SIGTSTP (ctrl-z) signals.
                self._signal_state.set(signal.SIGINT, self._catch_interrupt)
                self._signal_state.set(signal.SIGTSTP, self._catch_interrupt)

            # Enable mouse events
            curses.mousemask(curses.ALL_MOUSE_EVENTS |
                             curses.REPORT_MOUSE_POSITION)

            # Lookup the necessary escape codes in the terminfo database.
            self._move_y_x = curses.tigetstr("cup")
            self._up_line = curses.tigetstr("ri").decode("utf-8")
            self._down_line = curses.tigetstr("ind").decode("utf-8")
            self._fg_color = curses.tigetstr("setaf")
            self._bg_color = curses.tigetstr("setab")
            self._default_colours = curses.tigetstr("op")
            if self._default_colours:
                self._default_colours = self._default_colours.decode("utf-8")
            self._clear_line = curses.tigetstr("el").decode("utf-8")
            if curses.tigetflag("hs"):
                self._start_title = curses.tigetstr("tsl").decode("utf-8")
                self._end_title = curses.tigetstr("fsl").decode("utf-8")
            else:
                self._start_title = self._end_title = None
            self._a_normal = curses.tigetstr("sgr0").decode("utf-8")
            self._a_bold = curses.tigetstr("bold").decode("utf-8")
            self._a_reverse = curses.tigetstr("rev").decode("utf-8")
            self._a_underline = curses.tigetstr("smul").decode("utf-8")
            self._clear_screen = curses.tigetstr("clear").decode("utf-8")

            # Look for a mismatch between the kernel terminal and the terminfo
            # database for backspace.  Fix up keyboard mappings if needed.
            try:
                kbs = curses.tigetstr("kbs").decode("utf-8")
                tbs = termios.tcgetattr(sys.stdin)[6][termios.VERASE]
                if tbs != kbs:
                    self._KEY_MAP[ord(tbs)] = Screen.KEY_BACK
            except termios.error:
                pass

            # Conversion from Screen attributes to curses equivalents.
            self._ATTRIBUTES = {
                Screen.A_BOLD: self._a_bold,
                Screen.A_NORMAL: self._a_normal,
                Screen.A_REVERSE: self._a_reverse,
                Screen.A_UNDERLINE: self._a_underline
            }

            # Byte stream processing for unicode input.
            self._bytes_to_read = 0
            self._bytes_to_return = b""

            # We'll actually break out into low-level output, so flush any
            # high level buffers now.
            self._screen.refresh()

        def close(self, restore=True):
            """
            Close down this Screen and tidy up the environment as required.

            :param restore: whether to restore the environment or not.
            """
            self._signal_state.restore()
            if restore:
                self._screen.keypad(0)
                curses.echo()
                # Shouldn't fail on real systems.  This code is for running tests in CI pipelines.
                try:
                    curses.nocbreak()
                except curses.error:
                    pass
                try:
                    curses.endwin()
                except curses.error:
                    pass

        @staticmethod
        def _safe_write(msg):
            """
            Safe write to screen - catches IOErrors on screen resize.

            :param msg: The message to write to the screen.
            """
            try:
                sys.stdout.write(msg)
            except OSError:
                # Screen resize can throw IOErrors.  These can be safely
                # ignored as the screen will be shortly reset anyway.
                pass

        def _resize_handler(self, *_):
            """
            Window resize signal handler.  We don't care about any of the
            parameters passed in beyond the object reference.
            """
            curses.endwin()
            curses.initscr()
            self._re_sized = True

        def _continue_handler(self, *_):
            """
            Job pause/resume signal handler.  We don't care about any of the
            parameters passed in beyond the object reference.
            """
            self.force_update(full_refresh=True)

        def _scroll(self, lines):
            """
            Scroll the window up or down.

            :param lines: Number of lines to scroll.  Negative numbers scroll
                down.
            """
            if lines < 0:
                start = curses.tparm(self._move_y_x, 0, 0).decode("utf-8")
                scroll = (self._up_line + self._clear_line) * -lines
            else:
                start = curses.tparm(self._move_y_x, self.height, 0).decode("utf-8")
                scroll = (self._down_line + self._clear_line) * lines
            self._safe_write(f"{start}{scroll}")

        def _clear(self):
            """
            Clear the Screen of all content.
            """
            self._safe_write(self._clear_screen)
            sys.stdout.flush()

        def refresh(self):
            """
            Refresh the screen.
            """
            super().refresh()
            try:
                sys.stdout.flush()
            except OSError:
                pass

        @staticmethod
        def _catch_interrupt(signal_no, frame):
            """
            SIGINT handler.  We ignore the signal and frame info passed in.
            """
            # Stop pep-8 shouting at me for unused params I can't control.
            del frame

            # The OS already caught the ctrl-c, so inject it now for the next
            # input.
            if signal_no == signal.SIGINT:
                curses.ungetch(3)
            elif signal_no == signal.SIGTSTP:
                curses.ungetch(26)

        def get_event(self):
            """
            Check for an event without waiting.
            """
            # Spin through notifications until we find something we want.
            key = 0
            while key != -1:
                # Get the next key
                key = self._screen.getch()

                if key == curses.KEY_RESIZE:
                    # Handle screen resize
                    self._re_sized = True
                elif key == curses.KEY_MOUSE:
                    # Handle a mouse event
                    _, x, y, _, bstate = curses.getmouse()
                    buttons = 0
                    # Some Linux modes only report clicks, so check for any
                    # button down or click events.
                    if (bstate & curses.BUTTON1_PRESSED != 0 or
                            bstate & curses.BUTTON1_CLICKED != 0):
                        buttons |= MouseEvent.LEFT_CLICK
                    if (bstate & curses.BUTTON3_PRESSED != 0 or
                            bstate & curses.BUTTON3_CLICKED != 0):
                        buttons |= MouseEvent.RIGHT_CLICK
                    if bstate & curses.BUTTON1_DOUBLE_CLICKED != 0:
                        buttons |= MouseEvent.DOUBLE_CLICK
                    return MouseEvent(x, y, buttons)
                elif key != -1:
                    # Handle any byte streams first
                    logger.debug("Processing key: %x", key)
                    if self._unicode_aware and key > 0:
                        # Start of unicode byte stream
                        if key & 0xC0 == 0xC0:
                            self._bytes_to_return = struct.pack(b"B", key)
                            self._bytes_to_read = bin(key)[2:].index("0") - 1
                            logger.debug("Byte stream: %d bytes left",
                                         self._bytes_to_read)
                            continue

                        # Process unicode bytestream if still expecting data.
                        if self._bytes_to_read > 0:
                            self._bytes_to_return += struct.pack(b"B", key)
                            self._bytes_to_read -= 1
                            if self._bytes_to_read > 0:
                                continue

                            # If we get here we have a key definition.
                            key = ord(self._bytes_to_return.decode("utf-8"))

                    # Handle a genuine key press.
                    logger.debug("Returning key: %x", key)

                    if self._bytes_to_return:
                        # UTF-8 character - resetting _bytes_to_return
                        self._bytes_to_return = b""
                        return KeyboardEvent(key)
                    if key in self._KEY_MAP:
                        return KeyboardEvent(self._KEY_MAP[key])
                    elif key != -1:
                        return KeyboardEvent(key)

            return None

        def has_resized(self):
            """
            Check whether the screen has been re-sized.
            """
            re_sized = self._re_sized
            self._re_sized = False
            return re_sized

        def _change_colours(self, colour, attr, bg):
            """
            Change current colour if required.

            :param colour: New colour to use.
            :param attr: New attributes to use.
            :param bg: New background colour to use.
            """
            # Change attribute first as this will reset colours when swapping
            # modes.
            if attr != self._attr:
                self._safe_write(self._a_normal)
                if attr != 0:
                    self._safe_write(self._ATTRIBUTES[attr])
                self._attr = attr
                self._colour = None
                self._bg = None

            # next check for default colours - which reset both fg and bg.
            if Screen.COLOUR_DEFAULT in (colour, bg):
                if self._default_colours:
                    self._safe_write(self._default_colours)
                    self._colour = colour if colour == Screen.COLOUR_DEFAULT else None
                    self._bg = bg if bg == Screen.COLOUR_DEFAULT else None
                else:
                    colour = Screen.COLOUR_WHITE if colour == Screen.COLOUR_DEFAULT else colour
                    bg = Screen.COLOUR_BLACK if bg == Screen.COLOUR_DEFAULT else bg

            # Now swap colours if required.
            if colour != self._colour:
                self._safe_write(curses.tparm(
                    self._fg_color, colour).decode("utf-8"))
                self._colour = colour
            if bg != self._bg:
                self._safe_write(curses.tparm(
                    self._bg_color, bg).decode("utf-8"))
                self._bg = bg

        def _print_at(self, text, x, y, width):
            """
            Print string at the required location.

            :param text: The text string to print.
            :param x: The x coordinate
            :param y: The Y coordinate
            :param width: The width of the character (for dual-width glyphs in CJK languages).
            """
            # Move the cursor if necessary
            cursor = ""
            if x != self._cur_x or y != self._cur_y:
                cursor = curses.tparm(self._move_y_x, y, x).decode("utf-8")

            # Print the text at the required location and update the current
            # position.
            try:
                self._safe_write(cursor + text)
            except UnicodeEncodeError:
                # This is probably a sign that the user has the wrong locale.
                # Try to soldier on anyway.
                self._safe_write(cursor + "?" * len(text))

            # Update cursor position for next time...
            self._cur_x = x + width
            self._cur_y = y

        def wait_for_input(self, timeout):
            """
            Wait until there is some input or the timeout is hit.

            :param timeout: Time to wait for input in seconds (floating point).
            """
            try:
                select.select([sys.stdin], [], [], timeout)
            except OSError:
                # Any error will almost certainly result in a a Screen.  Ignore.
                pass

        def set_title(self, title):
            """
            Set the title for this terminal/console session.  This will
            typically change the text displayed in the window title bar.

            :param title: The title to be set.
            """
            if self._start_line is not None:
                self._safe_write(f"{self._start_title}{title}{self._end_title}")

    class _SignalState():
        """
        Save previous user signal state while setting signals.

        Used for signal restoration when asciimatics no longer has control
        of the user program.
        """

        def __init__(self):
            self._old_signal_states = []

        def set(self, signalnum, handler):
            """
            Set signal handler and record their previous values.

            :param signalnum: The const/enum matching to the signal to be set.
            :param handler: The function/const to set the signal to
            """
            old_handler = signal.getsignal(signalnum)
            # Some environments may install a non-Python handler (which returns None at this point).
            # We can't reinstate these, so just reset the default handler in such cases.
            if old_handler is None:
                old_handler = signal.SIG_DFL
            self._old_signal_states.append((signalnum, old_handler))
            signal.signal(signalnum, handler)

        def restore(self):
            """
            Restore saved signals to their previous handles.
            """
            for signalnum, handler in self._old_signal_states:
                signal.signal(signalnum, handler)
            self._old_signal_states = []
