"""
Functions for output (show or save) of graphics.
"""

import base64
import io
import os
import re
from pathlib import Path
import subprocess
import sys
from tempfile import NamedTemporaryFile
from typing import List

from PIL import Image as PILImageMod
from PIL.Image import Image as PILImage
from skia import Canvas, FILEWStream, Image, Rect, Surface, SVGCanvas, kPNG

from pytamaro.checks import check_graphic, check_type
from pytamaro.debug import add_debug_info
from pytamaro.graphic import Graphic
from pytamaro.localization import translate
from pytamaro.utils import export, is_notebook


def _warning_no_area(graphic: Graphic):
    """
    Emits a warning indicating that the graphic cannot be shown or saved
    because it has no area.

    :param graphic: graphic that cannot be shown or saved
    """
    size = graphic.bounds().round()
    # pylint: disable-next=line-too-long
    print(translate("EMPTY_AREA_OUTPUT", f"{size.width()}x{size.height()}"))


def _draw_to_canvas(canvas: Canvas, graphic: Graphic):
    """
    Draws a graphic to a canvas, correcting for the top-left position.

    :param canvas: canvas onto which to draw
    :param graphic: graphic to be drawn
    """
    bounds = graphic.bounds()
    canvas.translate(-bounds.left(), -bounds.top())
    graphic.draw(canvas)


# pylint: disable-next=invalid-name
def _save_as_SVG(filename: str, graphic: Graphic):
    """
    Save a graphic to an SVG file.

    :param filename: name of the file to be created, ending in ".svg"
    :param graphic: graphic to be saved
    """
    size = graphic.size()
    stream = FILEWStream(filename)
    canvas = SVGCanvas.Make(Rect.MakeSize(size), stream)
    _draw_to_canvas(canvas, graphic)
    del canvas
    stream.flush()
    # Manually add shape-rendering="crispEdges" to the SVG file.
    # We don't use the XML parser from the standard library because,
    # among other aspects, it does not properly maintain the doctype.
    with open(filename, "r", encoding="utf-8") as file:
        content = file.read()
    # `svg` tag may be self-closing
    new_content = re.sub("<svg(.*?)(/?)>", r'<svg\1 shape-rendering="crispEdges"\2>', content)
    with open(filename, "w", encoding="utf-8") as file:
        file.write(new_content)


def graphic_to_image(graphic: Graphic) -> Image:
    """
    Renders a graphic into a Skia image.

    :param graphic: graphic to be rendered
    :returns: rendered graphic as a Skia image
    """
    int_size = graphic.size().toCeil()
    surface = Surface(int_size.width(), int_size.height())
    _draw_to_canvas(surface.getCanvas(), graphic)
    return surface.makeImageSnapshot()


def graphic_to_pillow_image(graphic: Graphic) -> PILImage:
    """
    Renders a graphic and converts it into a Pillow image.

    :param graphic: graphic to be rendered and converted
    :returns: rendered graphic as a Pillow image
    """
    with io.BytesIO(graphic_to_image(graphic).encodeToData()) as stream:
        pil_image = PILImageMod.open(stream)
        pil_image.load()  # Ensure to make a copy of buffer
        return pil_image


# pylint: disable-next=invalid-name
def _save_as_PNG(filename: str, graphic: Graphic):
    """
    Save a graphic to a PNG file.

    :param filename: name of the file to be created, ending in ".png"
    :param graphic: graphic to be saved
    """
    graphic_to_image(graphic).save(filename, kPNG)


@export
def show_graphic(graphic: Graphic, debug: bool = False):
    """
    Show a graphic. Graphics with no area cannot be shown.

    When `debug` is `True`, adorns the visualization with useful information
    for debugging: a red border around the bounding box and a yellowish cross
    around the pinning position.

    :param graphic: graphic to be shown
    :param debug: can be optionally set to `True` to overlay debugging
           information
    """
    check_graphic(graphic)
    if graphic.empty_area():
        _warning_no_area(graphic)
    else:
        to_show = add_debug_info(graphic) if debug else graphic
        pil_image = graphic_to_pillow_image(to_show)
        if is_notebook():
            # pylint: disable-next=undefined-variable
            display(pil_image)  # type: ignore[name-defined]
        elif "PYTAMARO_OUTPUT_DATA_URI" in os.environ:
            buffer = io.BytesIO()
            pil_image.save(buffer, format="PNG")
            b64_str = base64.b64encode(buffer.getvalue()).decode("utf-8")
            print(f"data:image/png;base64,{b64_str}", end="")
        else:
            pil_image.show()


@export
def save_graphic(filename: str, graphic: Graphic, debug: bool = False):
    """
    Save a graphic to a file.
    Two file formats are supported: PNG (raster graphics) and SVG (vector graphics).
    The extension of the filename (either ".png" or ".svg") determines the format.

    Graphics with no area cannot be saved in the PNG format.

    When `debug` is `True`, adorns the visualization with useful information
    for debugging: a red border around the bounding box and a yellowish cross
    around the pinning position.

    :param filename: name of the file to create (with the extension)
    :param graphic: graphic to be saved
    :param debug: can be optionally set to `True` to overlay debugging
           information
    """
    check_type(filename, str, "filename")
    check_graphic(graphic)
    to_show = add_debug_info(graphic) if debug else graphic
    extension = Path(filename).suffix
    if extension == ".png":
        if graphic.empty_area():
            _warning_no_area(graphic)
        else:
            _save_as_PNG(filename, to_show)
    elif extension == ".svg":
        _save_as_SVG(filename, to_show)
    else:
        raise ValueError(translate("INVALID_FILENAME_EXTENSION"))


@export
def save_animation(filename: str, graphics: List[Graphic], duration: int = 40, loop: bool = True):
    """
    Save a sequence of graphics as an animation (GIF).

    Graphics are sequentially reproduced (normally at 25 frames per second) in
    a loop (unless specificied otherwise).

    :param filename: name of the file to create, including the extension '.gif'
    :param graphics: list of graphics to be saved as an animation
    :param duration: duration in milliseconds for each frame
           (defaults to 40 milliseconds, which leads to 25 frames per second)
    :param loop: whether the GIF should loop indefinitely (defaults to true)
    """
    check_type(filename, str, "filename")
    if Path(filename).suffix != ".gif":
        raise ValueError(translate("INVALID_FILENAME_GIF"))
    check_type(graphics, list, "graphics")
    if len(graphics) == 0:
        raise ValueError(translate("EMPTY_GRAPHICS_LIST"))
    pil_images = list(map(graphic_to_pillow_image, graphics))
    pil_images[0].save(
        filename,
        save_all=True,
        append_images=pil_images[1:],
        duration=duration,
        loop=0 if loop else 1,  # loop 0 means "indefinitely", 1 means "once"
    )


@export
def show_animation(graphics: List[Graphic], duration: int = 40, loop: bool = True):
    """
    Show a sequence of graphics as an animation (GIF).

    Graphics are sequentially reproduced (normally at 25 frames per second) in
    a loop (unless specificied otherwise).

    :param graphics: list of graphics to be shown as an animation
    :param duration: duration in milliseconds for each frame
           (defaults to 40 milliseconds, which leads to 25 frames per second)
    :param loop: whether the animation should loop indefinitely (defaults to true)
    """
    with NamedTemporaryFile(suffix=".gif", delete=False) as file:
        save_animation(file.name, graphics, duration, loop)
        if is_notebook():
            # pylint: disable-next=import-outside-toplevel, import-error
            from IPython.display import Image as IPythonImage  # type: ignore[import]
            with open(file.name, "rb") as stream:
                # pylint: disable-next=undefined-variable
                display(IPythonImage(stream.read()))  # type: ignore[name-defined]
        elif "PYTAMARO_OUTPUT_DATA_URI" in os.environ:
            with open(file.name, "rb") as stream:
                b64_str = base64.b64encode(stream.read()).decode("utf-8")
                print(f"data:image/gif;base64,{b64_str}", end="")
        elif sys.platform == "win32":
            os.startfile(file.name)
        elif sys.platform == "darwin":
            subprocess.call(["open", "-a", "Safari", file.name])
        else:
            subprocess.call(["xdg-open", file.name])
