"""
Libtcod consoles are a strictly tile-based representation of text and color.
To render a console you need a tileset and a window to render to.
See :ref:`getting-started` for info on how to set those up.
"""

import warnings
from typing import Any, Optional, Tuple, Union  # noqa: F401

import numpy as np
from typing_extensions import Literal

import tcod._internal
import tcod.constants
from tcod._internal import deprecate
from tcod.loader import ffi, lib


def _fmt(string: str) -> bytes:
    """Return a string that escapes 'C printf' side effects."""
    return string.encode("utf-8").replace(b"%", b"%%")


_root_console = None


class Console:
    """A console object containing a grid of characters with
    foreground/background colors.

    `width` and `height` are the size of the console (in tiles.)

    `order` determines how the axes of NumPy array attributes are arraigned.
    `order="F"` will swap the first two axes which allows for more intuitive
    `[x, y]` indexing.

    With `buffer` the console can be initialized from another array. The
    `buffer` should be compatible with the `width`, `height`, and `order`
    given; and should also have a dtype compatible with :any:`Console.DTYPE`.

    .. versionchanged:: 4.3
        Added `order` parameter.

    .. versionchanged:: 8.5
        Added `buffer`, `copy`, and default parameters.
        Arrays are initialized as if the :any:`clear` method was called.

    .. versionchanged:: 10.0
        `DTYPE` changed, `buffer` now requires colors with an alpha channel.

    Attributes:
        console_c: A python-cffi "TCOD_Console*" object.
        DTYPE:
            A class attribute which provides a dtype compatible with this
            class.

            ``[("ch", np.intc), ("fg", "(4,)u1"), ("bg", "(4,)u1")]``

            Example::

                >>> buffer = np.zeros(
                ...     shape=(20, 3),
                ...     dtype=tcod.console.Console.DTYPE,
                ...     order="F",
                ... )
                >>> buffer["ch"] = ord(' ')
                >>> buffer["ch"][:, 1] = ord('x')
                >>> c = tcod.console.Console(20, 3, order="F", buffer=buffer)
                >>> print(c)
                <                    |
                |xxxxxxxxxxxxxxxxxxxx|
                |                    >

            .. versionadded:: 8.5

            .. versionchanged:: 10.0
                Added an alpha channel to the color types.
    """

    DTYPE = np.dtype([("ch", np.intc), ("fg", "(4,)u1"), ("bg", "(4,)u1")])

    # A structured arrays type with the added "fg_rgb" and "bg_rgb" fields.
    _DTYPE_RGB = np.dtype(
        {
            "names": ["ch", "fg", "bg"],
            "formats": [np.int32, "(3,)u1", "(3,)u1"],
            "offsets": [0, 4, 8],
            "itemsize": 12,
        }
    )

    def __init__(
        self,
        width: int,
        height: int,
        order: Union[Literal["C"], Literal["F"]] = "C",
        buffer: Optional[np.ndarray] = None,
    ):
        self._key_color = None  # type: Optional[Tuple[int, int, int]]
        self._order = tcod._internal.verify_order(order)
        if buffer is not None:
            if self._order == "F":
                buffer = buffer.transpose()
            self._tiles = np.ascontiguousarray(buffer, self.DTYPE)
        else:
            self._tiles = np.ndarray((height, width), dtype=self.DTYPE)

        # libtcod uses the root console for defaults.
        default_bg_blend = 0
        default_alignment = 0
        if lib.TCOD_ctx.root != ffi.NULL:
            default_bg_blend = lib.TCOD_ctx.root.bkgnd_flag
            default_alignment = lib.TCOD_ctx.root.alignment

        self._console_data = self.console_c = ffi.new(
            "struct TCOD_Console*",
            {
                "w": width,
                "h": height,
                "elements": width * height,
                "tiles": ffi.from_buffer(
                    "struct TCOD_ConsoleTile*", self._tiles
                ),
                "bkgnd_flag": default_bg_blend,
                "alignment": default_alignment,
                "fore": (255, 255, 255),
                "back": (0, 0, 0),
            },
        )

        if buffer is None:
            self.clear()

    @classmethod
    def _from_cdata(
        cls, cdata: Any, order: Union[Literal["C"], Literal["F"]] = "C"
    ) -> "Console":
        """Return a Console instance which wraps this `TCOD_Console*` object."""
        if isinstance(cdata, cls):
            return cdata
        self = object.__new__(cls)  # type: Console
        self.console_c = cdata
        self._init_setup_console_data(order)
        return self

    @classmethod
    def _get_root(
        cls, order: Optional[Union[Literal["C"], Literal["F"]]] = None
    ) -> "Console":
        """Return a root console singleton with valid buffers.

        This function will also update an already active root console.
        """
        global _root_console
        if _root_console is None:
            _root_console = object.__new__(cls)
        self = _root_console  # type: Console
        if order is not None:
            self._order = order
        self.console_c = ffi.NULL
        self._init_setup_console_data(self._order)
        return self

    def _init_setup_console_data(
        self, order: Union[Literal["C"], Literal["F"]] = "C"
    ) -> None:
        """Setup numpy arrays over libtcod data buffers."""
        global _root_console
        self._key_color = None
        if self.console_c == ffi.NULL:
            _root_console = self
            self._console_data = lib.TCOD_ctx.root
        else:
            self._console_data = ffi.cast(
                "struct TCOD_Console*", self.console_c
            )

        self._tiles = np.frombuffer(
            ffi.buffer(self._console_data.tiles[0 : self.width * self.height]),
            dtype=self.DTYPE,
        ).reshape((self.height, self.width))

        self._order = tcod._internal.verify_order(order)

    @property
    def width(self) -> int:
        """int: The width of this Console. (read-only)"""
        return lib.TCOD_console_get_width(self.console_c)  # type: ignore

    @property
    def height(self) -> int:
        """int: The height of this Console. (read-only)"""
        return lib.TCOD_console_get_height(self.console_c)  # type: ignore

    @property
    def bg(self) -> np.ndarray:
        """A uint8 array with the shape (height, width, 3).

        You can change the consoles background colors by using this array.

        Index this array with ``console.bg[i, j, channel]  # order='C'`` or
        ``console.bg[x, y, channel]  # order='F'``.

        """
        bg = self._tiles["bg"][..., :3]  # type: np.ndarray
        if self._order == "F":
            bg = bg.transpose(1, 0, 2)
        return bg

    @property
    def fg(self) -> np.ndarray:
        """A uint8 array with the shape (height, width, 3).

        You can change the consoles foreground colors by using this array.

        Index this array with ``console.fg[i, j, channel]  # order='C'`` or
        ``console.fg[x, y, channel]  # order='F'``.
        """
        fg = self._tiles["fg"][..., :3]  # type: np.ndarray
        if self._order == "F":
            fg = fg.transpose(1, 0, 2)
        return fg

    @property
    def ch(self) -> np.ndarray:
        """An integer array with the shape (height, width).

        You can change the consoles character codes by using this array.

        Index this array with ``console.ch[i, j]  # order='C'`` or
        ``console.ch[x, y]  # order='F'``.
        """
        return self._tiles["ch"].T if self._order == "F" else self._tiles["ch"]

    @property
    def tiles(self) -> np.ndarray:
        """An array of this consoles raw tile data.

        This acts as a combination of the `ch`, `fg`, and `bg` attributes.
        Colors include an alpha channel but how alpha works is currently
        undefined.

        Example:
            >>> con = tcod.console.Console(10, 2)
            >>> con.tiles[0, 0] = (
            ...     ord("X"),
            ...     (*tcod.white, 255),
            ...     (*tcod.black, 255),
            ... )
            >>> con.tiles[0, 0]
            (88, [255, 255, 255, 255], [  0,   0,   0, 255])

        .. versionadded:: 10.0
        """
        return self._tiles.T if self._order == "F" else self._tiles

    @property  # type: ignore
    @deprecate("This attribute has been renamed to `tiles`.")
    def buffer(self) -> np.ndarray:
        """An array of this consoles raw tile data.

        .. versionadded:: 11.4

        .. deprecated:: 11.8
            Use :any:`Console.tiles` instead.
        """
        return self._tiles.T if self._order == "F" else self._tiles

    @property
    def tiles_rgb(self) -> np.ndarray:
        """An array of this consoles tile data without the alpha channel.

        The dtype of this array is effectively:
        ``[("ch", np.intc), ("fg", "(3,)u1"), ("bg", "(3,)u1")]``

        Example:
            >>> con = tcod.console.Console(10, 2)
            >>> con.tiles_rgb[0, 0] = ord("@"), tcod.yellow, tcod.black
            >>> con.tiles_rgb[0, 0]
            (64, [255, 255,   0], [0, 0, 0])
            >>> con.tiles_rgb["bg"] = tcod.blue
            >>> con.tiles_rgb[0, 0]
            (64, [255, 255,   0], [  0,   0, 255])

        .. versionadded:: 11.8
        """
        return self.tiles.view(self._DTYPE_RGB)

    @property  # type: ignore
    @deprecate("This attribute has been renamed to `tiles_rgb`.")
    def tiles2(self) -> np.ndarray:
        """This name is deprecated in favour of :any:`tiles_rgb`.

        .. versionadded:: 11.3

        .. deprecated:: 11.8
            Use :any:`Console.tiles_rgb` instead.
        """
        return self.tiles_rgb

    @property
    def default_bg(self) -> Tuple[int, int, int]:
        """Tuple[int, int, int]: The default background color."""
        color = self._console_data.back
        return color.r, color.g, color.b

    @default_bg.setter  # type: ignore
    @deprecate("Console defaults have been deprecated.")
    def default_bg(self, color: Tuple[int, int, int]) -> None:
        self._console_data.back = color

    @property
    def default_fg(self) -> Tuple[int, int, int]:
        """Tuple[int, int, int]: The default foreground color."""
        color = self._console_data.fore
        return color.r, color.g, color.b

    @default_fg.setter  # type: ignore
    @deprecate("Console defaults have been deprecated.")
    def default_fg(self, color: Tuple[int, int, int]) -> None:
        self._console_data.fore = color

    @property
    def default_bg_blend(self) -> int:
        """int: The default blending mode."""
        return self._console_data.bkgnd_flag  # type: ignore

    @default_bg_blend.setter  # type: ignore
    @deprecate("Console defaults have been deprecated.")
    def default_bg_blend(self, value: int) -> None:
        self._console_data.bkgnd_flag = value

    @property
    def default_alignment(self) -> int:
        """int: The default text alignment."""
        return self._console_data.alignment  # type: ignore

    @default_alignment.setter  # type: ignore
    @deprecate("Console defaults have been deprecated.")
    def default_alignment(self, value: int) -> None:
        self._console_data.alignment = value

    def __clear_warning(self, name: str, value: Tuple[int, int, int]) -> None:
        """Raise a warning for bad default values during calls to clear."""
        warnings.warn(
            "Clearing with the console default values is deprecated.\n"
            "Add %s=%r to this call." % (name, value),
            DeprecationWarning,
            stacklevel=3,
        )

    def clear(
        self,
        ch: int = ord(" "),
        fg: Tuple[int, int, int] = ...,  # type: ignore
        bg: Tuple[int, int, int] = ...,  # type: ignore
    ) -> None:
        """Reset all values in this console to a single value.

        `ch` is the character to clear the console with.  Defaults to the space
        character.

        `fg` and `bg` are the colors to clear the console with.  Defaults to
        white-on-black if the console defaults are untouched.

        .. note::
            If `fg`/`bg` are not set, they will default to
            :any:`default_fg`/:any:`default_bg`.
            However, default values other than white-on-back are deprecated.

        .. versionchanged:: 8.5
            Added the `ch`, `fg`, and `bg` parameters.
            Non-white-on-black default values are deprecated.
        """
        if fg is ...:  # type: ignore
            fg = self.default_fg
            if fg != (255, 255, 255):
                self.__clear_warning("fg", fg)
        if bg is ...:  # type: ignore
            bg = self.default_bg
            if bg != (0, 0, 0):
                self.__clear_warning("bg", bg)
        self._tiles[...] = ch, (*fg, 255), (*bg, 255)

    def put_char(
        self,
        x: int,
        y: int,
        ch: int,
        bg_blend: int = tcod.constants.BKGND_DEFAULT,
    ) -> None:
        """Draw the character c at x,y using the default colors and a blend mode.

        Args:
            x (int): The x coordinate from the left.
            y (int): The y coordinate from the top.
            ch (int): Character code to draw.  Must be in integer form.
            bg_blend (int): Blending mode to use, defaults to BKGND_DEFAULT.
        """
        lib.TCOD_console_put_char(self.console_c, x, y, ch, bg_blend)

    __ALIGNMENT_LOOKUP = {0: "tcod.LEFT", 1: "tcod.RIGHT", 2: "tcod.CENTER"}

    __BG_BLEND_LOOKUP = {
        0: "tcod.BKGND_NONE",
        1: "tcod.BKGND_SET",
        2: "tcod.BKGND_MULTIPLY",
        3: "tcod.BKGND_LIGHTEN",
        4: "tcod.BKGND_DARKEN",
        5: "tcod.BKGND_SCREEN",
        6: "tcod.BKGND_COLOR_DODGE",
        7: "tcod.BKGND_COLOR_BURN",
        8: "tcod.BKGND_ADD",
        9: "tcod.BKGND_ADDA",
        10: "tcod.BKGND_BURN",
        11: "tcod.BKGND_OVERLAY",
        12: "tcod.BKGND_ALPH",
        13: "tcod.BKGND_DEFAULT",
    }

    def __deprecate_defaults(
        self,
        new_func: str,
        bg_blend: Any,
        alignment: Any = ...,
        clear: Any = ...,
    ) -> None:
        """Return the parameters needed to recreate the current default state."""
        if not __debug__:
            return

        fg = self.default_fg  # type: Any
        bg = self.default_bg  # type: Any
        if bg_blend == tcod.constants.BKGND_NONE:
            bg = None
        if bg_blend == tcod.constants.BKGND_DEFAULT:
            bg_blend = self.default_bg_blend
        else:
            bg_blend = None
        if bg_blend == tcod.constants.BKGND_NONE:
            bg = None
            bg_blend = None
        if bg_blend == tcod.constants.BKGND_SET:
            bg_blend = None
        if alignment is None:
            alignment = self.default_alignment
            if alignment == tcod.constants.LEFT:
                alignment = None
        else:
            alignment = None
        if clear is not ...:
            fg = None
        params = []
        if clear is True:
            params.append('ch=ord(" ")')
        if clear is False:
            params.append("ch=0")
        if fg is not None:
            params.append("fg=%s" % (fg,))
        if bg is not None:
            params.append("bg=%s" % (bg,))
        if bg_blend is not None:
            params.append("bg_blend=%s" % (self.__BG_BLEND_LOOKUP[bg_blend],))
        if alignment is not None:
            params.append(
                "alignment=%s" % (self.__ALIGNMENT_LOOKUP[alignment],)
            )
        param_str = ", ".join(params)
        if not param_str:
            param_str = "."
        else:
            param_str = " and add the following parameters:\n%s" % (param_str,)
        warnings.warn(
            "Console functions using default values have been deprecated.\n"
            "Replace this method with `Console.%s`%s" % (new_func, param_str),
            DeprecationWarning,
            stacklevel=3,
        )

    def print_(
        self,
        x: int,
        y: int,
        string: str,
        bg_blend: int = tcod.constants.BKGND_DEFAULT,
        alignment: Optional[int] = None,
    ) -> None:
        """Print a color formatted string on a console.

        Args:
            x (int): The x coordinate from the left.
            y (int): The y coordinate from the top.
            string (str): A Unicode string optionally using color codes.
            bg_blend (int): Blending mode to use, defaults to BKGND_DEFAULT.
            alignment (Optional[int]): Text alignment.

        .. deprecated:: 8.5
            Console methods which depend on console defaults have been
            deprecated.
            Use :any:`Console.print` instead, calling this function will print
            a warning detailing which default values need to be made explicit.
        """
        self.__deprecate_defaults("print", bg_blend, alignment)
        alignment = self.default_alignment if alignment is None else alignment
        lib.TCOD_console_printf_ex(
            self.console_c, x, y, bg_blend, alignment, _fmt(string)
        )

    def print_rect(
        self,
        x: int,
        y: int,
        width: int,
        height: int,
        string: str,
        bg_blend: int = tcod.constants.BKGND_DEFAULT,
        alignment: Optional[int] = None,
    ) -> int:
        """Print a string constrained to a rectangle.

        If h > 0 and the bottom of the rectangle is reached,
        the string is truncated. If h = 0,
        the string is only truncated if it reaches the bottom of the console.

        Args:
            x (int): The x coordinate from the left.
            y (int): The y coordinate from the top.
            width (int): Maximum width to render the text.
            height (int): Maximum lines to render the text.
            string (str): A Unicode string.
            bg_blend (int): Background blending flag.
            alignment (Optional[int]): Alignment flag.

        Returns:
            int: The number of lines of text once word-wrapped.

        .. deprecated:: 8.5
            Console methods which depend on console defaults have been
            deprecated.
            Use :any:`Console.print_box` instead, calling this function will
            print a warning detailing which default values need to be made
            explicit.
        """
        self.__deprecate_defaults("print_box", bg_blend, alignment)
        alignment = self.default_alignment if alignment is None else alignment
        return int(
            lib.TCOD_console_printf_rect_ex(
                self.console_c,
                x,
                y,
                width,
                height,
                bg_blend,
                alignment,
                _fmt(string),
            )
        )

    def get_height_rect(
        self, x: int, y: int, width: int, height: int, string: str
    ) -> int:
        """Return the height of this text word-wrapped into this rectangle.

        Args:
            x (int): The x coordinate from the left.
            y (int): The y coordinate from the top.
            width (int): Maximum width to render the text.
            height (int): Maximum lines to render the text.
            string (str): A Unicode string.

        Returns:
            int: The number of lines of text once word-wrapped.
        """
        string_ = string.encode("utf-8")
        return int(
            lib.TCOD_console_get_height_rect_n(
                self.console_c, x, y, width, height, len(string_), string_
            )
        )

    def rect(
        self,
        x: int,
        y: int,
        width: int,
        height: int,
        clear: bool,
        bg_blend: int = tcod.constants.BKGND_DEFAULT,
    ) -> None:
        """Draw a the background color on a rect optionally clearing the text.

        If `clear` is True the affected tiles are changed to space character.

        Args:
            x (int): The x coordinate from the left.
            y (int): The y coordinate from the top.
            width (int): Maximum width to render the text.
            height (int): Maximum lines to render the text.
            clear (bool): If True all text in the affected area will be
                          removed.
            bg_blend (int): Background blending flag.

        .. deprecated:: 8.5
            Console methods which depend on console defaults have been
            deprecated.
            Use :any:`Console.draw_rect` instead, calling this function will
            print a warning detailing which default values need to be made
            explicit.
        """
        self.__deprecate_defaults("draw_rect", bg_blend, clear=bool(clear))
        lib.TCOD_console_rect(
            self.console_c, x, y, width, height, clear, bg_blend
        )

    def hline(
        self,
        x: int,
        y: int,
        width: int,
        bg_blend: int = tcod.constants.BKGND_DEFAULT,
    ) -> None:
        """Draw a horizontal line on the console.

        This always uses ord('─'), the horizontal line character.

        Args:
            x (int): The x coordinate from the left.
            y (int): The y coordinate from the top.
            width (int): The horizontal length of this line.
            bg_blend (int): The background blending flag.

        .. deprecated:: 8.5
            Console methods which depend on console defaults have been
            deprecated.
            Use :any:`Console.draw_rect` instead, calling this function will
            print a warning detailing which default values need to be made
            explicit.
        """
        self.__deprecate_defaults("draw_rect", bg_blend)
        lib.TCOD_console_hline(self.console_c, x, y, width, bg_blend)

    def vline(
        self,
        x: int,
        y: int,
        height: int,
        bg_blend: int = tcod.constants.BKGND_DEFAULT,
    ) -> None:
        """Draw a vertical line on the console.

        This always uses ord('│'), the vertical line character.

        Args:
            x (int): The x coordinate from the left.
            y (int): The y coordinate from the top.
            height (int): The horozontal length of this line.
            bg_blend (int): The background blending flag.

        .. deprecated:: 8.5
            Console methods which depend on console defaults have been
            deprecated.
            Use :any:`Console.draw_rect` instead, calling this function will
            print a warning detailing which default values need to be made
            explicit.
        """
        self.__deprecate_defaults("draw_rect", bg_blend)
        lib.TCOD_console_vline(self.console_c, x, y, height, bg_blend)

    def print_frame(
        self,
        x: int,
        y: int,
        width: int,
        height: int,
        string: str = "",
        clear: bool = True,
        bg_blend: int = tcod.constants.BKGND_DEFAULT,
    ) -> None:
        """Draw a framed rectangle with optional text.

        This uses the default background color and blend mode to fill the
        rectangle and the default foreground to draw the outline.

        `string` will be printed on the inside of the rectangle, word-wrapped.
        If `string` is empty then no title will be drawn.

        Args:
            x (int): The x coordinate from the left.
            y (int): The y coordinate from the top.
            width (int): The width if the frame.
            height (int): The height of the frame.
            string (str): A Unicode string to print.
            clear (bool): If True all text in the affected area will be
                          removed.
            bg_blend (int): The background blending flag.

        .. versionchanged:: 8.2
            Now supports Unicode strings.

        .. deprecated:: 8.5
            Console methods which depend on console defaults have been
            deprecated.
            Use :any:`Console.draw_frame` instead, calling this function will
            print a warning detailing which default values need to be made
            explicit.
        """
        self.__deprecate_defaults("draw_frame", bg_blend)
        string = _fmt(string) if string else ffi.NULL
        lib.TCOD_console_printf_frame(
            self.console_c, x, y, width, height, clear, bg_blend, string
        )

    def blit(
        self,
        dest: "Console",
        dest_x: int = 0,
        dest_y: int = 0,
        src_x: int = 0,
        src_y: int = 0,
        width: int = 0,
        height: int = 0,
        fg_alpha: float = 1.0,
        bg_alpha: float = 1.0,
        key_color: Optional[Tuple[int, int, int]] = None,
    ) -> None:
        """Blit from this console onto the ``dest`` console.

        Args:
            dest (Console): The destination console to blit onto.
            dest_x (int): Leftmost coordinate of the destination console.
            dest_y (int): Topmost coordinate of the destination console.
            src_x (int): X coordinate from this console to blit, from the left.
            src_y (int): Y coordinate from this console to blit, from the top.
            width (int): The width of the region to blit.

                If this is 0 the maximum possible width will be used.
            height (int): The height of the region to blit.

                If this is 0 the maximum possible height will be used.
            fg_alpha (float): Foreground color alpha vaule.
            bg_alpha (float): Background color alpha vaule.
            key_color (Optional[Tuple[int, int, int]]):
                None, or a (red, green, blue) tuple with values of 0-255.

        .. versionchanged:: 4.0
            Parameters were rearraged and made optional.

            Previously they were:
            `(x, y, width, height, dest, dest_x, dest_y, *)`

        .. versionchanged:: 11.6
            Now supports per-cell alpha transparency.

            Use :any:`Console.buffer` to set tile alpha before blit.
        """
        # The old syntax is easy to detect and correct.
        if hasattr(src_y, "console_c"):
            (src_x, src_y, width, height, dest, dest_x, dest_y,) = (
                dest,  # type: ignore
                dest_x,
                dest_y,
                src_x,
                src_y,  # type: ignore
                width,
                height,
            )
            warnings.warn(
                "Parameter names have been moved around, see documentation.",
                DeprecationWarning,
                stacklevel=2,
            )

        key_color = key_color or self._key_color
        if key_color:
            key_color = ffi.new("TCOD_color_t*", key_color)
            lib.TCOD_console_blit_key_color(
                self.console_c,
                src_x,
                src_y,
                width,
                height,
                dest.console_c,
                dest_x,
                dest_y,
                fg_alpha,
                bg_alpha,
                key_color,
            )
        else:
            lib.TCOD_console_blit(
                self.console_c,
                src_x,
                src_y,
                width,
                height,
                dest.console_c,
                dest_x,
                dest_y,
                fg_alpha,
                bg_alpha,
            )

    @deprecate(
        "Pass the key color to Console.blit instead of calling this function."
    )
    def set_key_color(self, color: Optional[Tuple[int, int, int]]) -> None:
        """Set a consoles blit transparent color.

        `color` is the (r, g, b) color, or None to disable key color.

        .. deprecated:: 8.5
            Pass the key color to :any:`Console.blit` instead of calling this
            function.
        """
        self._key_color = color

    def __enter__(self) -> "Console":
        """Returns this console in a managed context.

        When the root console is used as a context, the graphical window will
        close once the context is left as if :any:`tcod.console_delete` was
        called on it.

        This is useful for some Python IDE's like IDLE, where the window would
        not be closed on its own otherwise.

        .. seealso::
            :any:`tcod.console_init_root`
        """
        if self.console_c != ffi.NULL:
            raise NotImplementedError("Only the root console has a context.")
        return self

    def close(self) -> None:
        """Close the active window managed by libtcod.

        This must only be called on the root console, which is returned from
        :any:`tcod.console_init_root`.

        .. versionadded:: 11.11
        """
        if self.console_c != ffi.NULL:
            raise NotImplementedError(
                "Only the root console can be used to close libtcod's window."
            )
        lib.TCOD_console_delete(self.console_c)

    def __exit__(self, *args: Any) -> None:
        """Closes the graphical window on exit.

        Some tcod functions may have undefined behavior after this point.
        """
        self.close()

    def __bool__(self) -> bool:
        """Returns False if this is the root console.

        This mimics libtcodpy behavior.
        """
        return bool(self.console_c != ffi.NULL)

    def __getstate__(self) -> Any:
        state = self.__dict__.copy()
        del state["console_c"]
        state["_console_data"] = {
            "w": self.width,
            "h": self.height,
            "bkgnd_flag": self.default_bg_blend,
            "alignment": self.default_alignment,
            "fore": self.default_fg,
            "back": self.default_bg,
        }
        if self.console_c == ffi.NULL:
            state["_tiles"] = np.copy(self._tiles)
        return state

    def __setstate__(self, state: Any) -> None:
        self._key_color = None
        if "_tiles" not in state:
            tiles = np.ndarray((self.height, self.width), dtype=self.DTYPE)
            tiles["ch"] = state["_ch"]
            tiles["fg"][..., :3] = state["_fg"]
            tiles["fg"][..., 3] = 255
            tiles["bg"][..., :3] = state["_bg"]
            tiles["bg"][..., 3] = 255
            state["_tiles"] = tiles
            del state["_ch"]
            del state["_fg"]
            del state["_bg"]

        self.__dict__.update(state)
        self._console_data["tiles"] = ffi.from_buffer(
            "struct TCOD_ConsoleTile*", self._tiles
        )
        self._console_data = self.console_c = ffi.new(
            "struct TCOD_Console*", self._console_data
        )

    def __repr__(self) -> str:
        """Return a string representation of this console."""
        return (
            "tcod.console.Console(width=%i, height=%i, "
            "order=%r,buffer=\n%r)"
            % (self.width, self.height, self._order, self.buffer)
        )

    def __str__(self) -> str:
        """Return a simplified representation of this consoles contents."""
        return "<%s>" % "|\n|".join(
            "".join(chr(c) for c in line) for line in self._tiles["ch"]
        )

    def _pythonic_index(self, x: int, y: int) -> Tuple[int, int]:
        if __debug__ and (x < 0 or y < 0):
            warnings.warn(
                "How negative indexes are handled my change in the future.",
                PendingDeprecationWarning,
                stacklevel=3,
            )
        if x < 0:
            x += self.width
        if y < 0:
            y += self.height
        return x, y

    def print(
        self,
        x: int,
        y: int,
        string: str,
        fg: Optional[Tuple[int, int, int]] = None,
        bg: Optional[Tuple[int, int, int]] = None,
        bg_blend: int = tcod.constants.BKGND_SET,
        alignment: int = tcod.constants.LEFT,
    ) -> None:
        """Print a string on a console with manual line breaks.

        `x` and `y` are the starting tile, with ``0,0`` as the upper-left
        corner of the console.  You can use negative numbers if you want to
        start printing relative to the bottom-right corner, but this behavior
        may change in future versions.

        `string` is a Unicode string which may include color control
        characters.  Strings which are too long will be truncated until the
        next newline character ``"\\n"``.

        `fg` and `bg` are the foreground text color and background tile color
        respectfully.  This is a 3-item tuple with (r, g, b) color values from
        0 to 255.  These parameters can also be set to `None` to leave the
        colors unchanged.

        `bg_blend` is the blend type used by libtcod.

        `alignment` can be `tcod.LEFT`, `tcod.CENTER`, or `tcod.RIGHT`.

        .. versionadded:: 8.5

        .. versionchanged:: 9.0
            `fg` and `bg` now default to `None` instead of white-on-black.
        """
        x, y = self._pythonic_index(x, y)
        string_ = string.encode("utf-8")  # type: bytes
        lib.TCOD_console_printn(
            self.console_c,
            x,
            y,
            len(string_),
            string_,
            (fg,) if fg is not None else ffi.NULL,
            (bg,) if bg is not None else ffi.NULL,
            bg_blend,
            alignment,
        )

    def print_box(
        self,
        x: int,
        y: int,
        width: int,
        height: int,
        string: str,
        fg: Optional[Tuple[int, int, int]] = None,
        bg: Optional[Tuple[int, int, int]] = None,
        bg_blend: int = tcod.constants.BKGND_SET,
        alignment: int = tcod.constants.LEFT,
    ) -> int:
        """Print a string constrained to a rectangle and return the height.

        `x` and `y` are the starting tile, with ``0,0`` as the upper-left
        corner of the console.  You can use negative numbers if you want to
        start printing relative to the bottom-right corner, but this behavior
        may change in future versions.

        `width` and `height` determine the bounds of the rectangle, the text
        will automatically be word-wrapped to fit within these bounds.

        `string` is a Unicode string which may include color control
        characters.

        `fg` and `bg` are the foreground text color and background tile color
        respectfully.  This is a 3-item tuple with (r, g, b) color values from
        0 to 255.  These parameters can also be set to `None` to leave the
        colors unchanged.

        `bg_blend` is the blend type used by libtcod.

        `alignment` can be `tcod.LEFT`, `tcod.CENTER`, or `tcod.RIGHT`.

        Returns the actual height of the printed area.

        .. versionadded:: 8.5

        .. versionchanged:: 9.0
            `fg` and `bg` now default to `None` instead of white-on-black.
        """
        x, y = self._pythonic_index(x, y)
        string_ = string.encode("utf-8")  # type: bytes
        return int(
            lib.TCOD_console_printn_rect(
                self.console_c,
                x,
                y,
                width,
                height,
                len(string_),
                string_,
                (fg,) if fg is not None else ffi.NULL,
                (bg,) if bg is not None else ffi.NULL,
                bg_blend,
                alignment,
            )
        )

    def draw_frame(
        self,
        x: int,
        y: int,
        width: int,
        height: int,
        title: str = "",
        clear: bool = True,
        fg: Optional[Tuple[int, int, int]] = None,
        bg: Optional[Tuple[int, int, int]] = None,
        bg_blend: int = tcod.constants.BKGND_SET,
    ) -> None:
        """Draw a framed rectangle with an optional title.

        `x` and `y` are the starting tile, with ``0,0`` as the upper-left
        corner of the console.  You can use negative numbers if you want to
        start printing relative to the bottom-right corner, but this behavior
        may change in future versions.

        `width` and `height` determine the size of the frame.

        `title` is a Unicode string.  The title is drawn with `bg` as the text
        color and `fg` as the background.

        If `clear` is True than the region inside of the frame will be cleared.

        `fg` and `bg` are the foreground and background colors for the frame
        border.  This is a 3-item tuple with (r, g, b) color values from
        0 to 255.  These parameters can also be set to `None` to leave the
        colors unchanged.

        `bg_blend` is the blend type used by libtcod.

        .. versionadded:: 8.5

        .. versionchanged:: 9.0
            `fg` and `bg` now default to `None` instead of white-on-black.
        """
        x, y = self._pythonic_index(x, y)
        title_ = title.encode("utf-8")  # type: bytes
        lib.TCOD_console_printn_frame(
            self.console_c,
            x,
            y,
            width,
            height,
            len(title_),
            title_,
            (fg,) if fg is not None else ffi.NULL,
            (bg,) if bg is not None else ffi.NULL,
            bg_blend,
            clear,
        )

    def draw_rect(
        self,
        x: int,
        y: int,
        width: int,
        height: int,
        ch: int,
        fg: Optional[Tuple[int, int, int]] = None,
        bg: Optional[Tuple[int, int, int]] = None,
        bg_blend: int = tcod.constants.BKGND_SET,
    ) -> None:
        """Draw characters and colors over a rectangular region.

        `x` and `y` are the starting tile, with ``0,0`` as the upper-left
        corner of the console.  You can use negative numbers if you want to
        start printing relative to the bottom-right corner, but this behavior
        may change in future versions.

        `width` and `height` determine the size of the rectangle.

        `ch` is a Unicode integer.  You can use 0 to leave the current
        characters unchanged.

        `fg` and `bg` are the foreground text color and background tile color
        respectfully.  This is a 3-item tuple with (r, g, b) color values from
        0 to 255.  These parameters can also be set to `None` to leave the
        colors unchanged.

        `bg_blend` is the blend type used by libtcod.

        .. versionadded:: 8.5

        .. versionchanged:: 9.0
            `fg` and `bg` now default to `None` instead of white-on-black.
        """
        x, y = self._pythonic_index(x, y)
        lib.TCOD_console_draw_rect_rgb(
            self.console_c,
            x,
            y,
            width,
            height,
            ch,
            (fg,) if fg is not None else ffi.NULL,
            (bg,) if bg is not None else ffi.NULL,
            bg_blend,
        )

    def draw_semigraphics(self, pixels: Any, x: int = 0, y: int = 0) -> None:
        """Draw a block of 2x2 semi-graphics into this console.

        `pixels` is an Image or an array-like object.  It will be down-sampled
        into 2x2 blocks when drawn.  Array-like objects must be in the shape of
        `(height, width, RGB)` and should have a `dtype` of `numpy.uint8`.

        `x` and `y` is the upper-left tile position to start drawing.

        .. versionadded:: 11.4
        """
        image = tcod._internal._asimage(pixels)
        lib.TCOD_image_blit_2x(
            image.image_c, self.console_c, x, y, 0, 0, -1, -1
        )


def get_height_rect(width: int, string: str) -> int:
    """Return the number of lines which would be printed from these parameters.

    `width` is the width of the print boundary.

    `string` is a Unicode string which may include color control characters.

    .. versionadded:: 9.2
    """
    string_ = string.encode("utf-8")  # type: bytes
    return int(
        lib.TCOD_console_get_height_rect_wn(width, len(string_), string_)
    )


@deprecate("This function does not support contexts.")
def recommended_size() -> Tuple[int, int]:
    """Return the recommended size of a console for the current active window.

    The return is determined from the active tileset size and active window
    size.  This result should be used create an :any:`Console` instance.

    This function will raise RuntimeError if libtcod has not been initialized.

    .. versionadded:: 11.8

    .. seealso::
        :any:`tcod.console_init_root`
        :any:`tcod.console_flush`

    .. deprecated:: 11.13
        This function does not support contexts.
        Use :any:`Context.recommended_console_size` instead.
    """
    if not lib.TCOD_ctx.engine:
        raise RuntimeError("The libtcod engine was not initialized first.")
    window = lib.TCOD_sys_get_sdl_window()
    renderer = lib.TCOD_sys_get_sdl_renderer()
    with ffi.new("int[2]") as xy:
        if renderer:
            lib.SDL_GetRendererOutputSize(renderer, xy, xy + 1)
        else:  # Assume OpenGL if a renderer does not exist.
            lib.SDL_GL_GetDrawableSize(window, xy, xy + 1)
        w = max(1, xy[0] // lib.TCOD_ctx.tileset.tile_width)
        h = max(1, xy[1] // lib.TCOD_ctx.tileset.tile_height)
    return w, h
