# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)

from typing import Literal

import scipp as sc

from ...core.utils import maybe_variable_to_number
from ...graphics.bbox import BoundingBox
from ...utils import parse_mutually_exclusive


class Canvas:
    """
    Plotly-based canvas used to render 2D graphics.
    It provides a figure and some axes, as well as functions for controlling the zoom,
    panning, and the scale of the axes.

    Parameters
    ----------
    figsize:
        The width and height of the figure, in inches.
    title:
        The title to be placed above the figure.
    user_vmin:
        The minimum value for the vertical axis. If a number (without a unit) is
        supplied, it is assumed that the unit is the same as the current vertical axis
        unit.
    user_vmax:
        The maximum value for the vertical axis. If a number (without a unit) is
        supplied, it is assumed that the unit is the same as the current vertical axis
        unit.
    """

    def __init__(
        self,
        figsize: tuple[float, float] | None = None,
        title: str | None = None,
        user_vmin: sc.Variable | float | None = None,
        user_vmax: sc.Variable | float | None = None,
        xmin: sc.Variable | float | None = None,
        xmax: sc.Variable | float | None = None,
        ymin: sc.Variable | float | None = None,
        ymax: sc.Variable | float | None = None,
        logx: bool | None = None,
        logy: bool | None = None,
        xlabel: str | None = None,
        ylabel: str | None = None,
        norm: Literal['linear', 'log', None] = None,
        **ignored,
    ):
        # Note on the `**ignored`` keyword arguments: the figure which owns the canvas
        # creates both the canvas and an artist object (Line or Image). The figure
        # accepts keyword arguments, and has to somehow forward them to the canvas and
        # the artist. Since the figure has no detailed knowledge of the underlying
        # backend that implements the canvas, it cannot have specific arguments (such
        # as `layout` for specifying a Plotly layout).
        # Instead, we forward all the kwargs from the figure to both the canvas and the
        # artist, and filter out the artist kwargs with `**ignored`.

        ymin = parse_mutually_exclusive(vmin=user_vmin, ymin=ymin)
        ymax = parse_mutually_exclusive(vmax=user_vmax, ymax=ymax)
        logy = parse_mutually_exclusive(norm=norm, logy=logy)

        import plotly.graph_objects as go

        self.fig = go.FigureWidget(
            layout={
                'modebar_remove': [
                    'zoom',
                    'pan',
                    'select',
                    'toImage',
                    'zoomIn',
                    'zoomOut',
                    'autoScale',
                    'resetScale',
                    'lasso2d',
                ],
                'margin': {'l': 0, 'r': 0, 't': 0 if title is None else 40, 'b': 0},
                'dragmode': False,
                'width': 600 if figsize is None else figsize[0],
                'height': 400 if figsize is None else figsize[1],
            }
        )
        self.figsize = figsize
        self._xmin = xmin
        self._xmax = xmax
        self._ymin = ymin
        self._ymax = ymax
        self._xlabel = xlabel
        self._ylabel = ylabel
        self.units = {}
        self.dims = {}
        self._own_axes = False
        if title:
            self.title = title
        self.bbox = BoundingBox()

        logx = False if logx is None else logx
        logy = False if logy is None else logy
        if logx:
            self.xscale = 'log'
        if logy:
            self.yscale = 'log'
        if xlabel is not None:
            self.xlabel = xlabel
        if ylabel is not None:
            self.ylabel = ylabel

    def to_widget(self):
        return self.fig

    def save(self, filename: str):
        """
        Save the figure to file.
        The default directory for writing the file is the same as the
        directory where the script or notebook is running.

        Parameters
        ----------
        filename:
            Name of the output file. Possible file extensions are ``.jpg``, ``.png``,
            ``.svg``, ``.pdf``, and ``.html`.
        """
        ext = filename.split('.')[-1]
        if ext == 'html':
            self.fig.write_html(filename)
        else:
            self.fig.write_image(filename)

    def set_axes(self, dims, units, dtypes):
        """
        Set the axes dimensions and units.

        Parameters
        ----------
        dims:
            The dimensions of the data.
        units:
            The units of the data.
        dtypes:
            The data types of the data.
        """
        self.dims = dims
        self.units = units
        self.dtypes = dtypes
        key = 'y' if 'y' in self.units else 'data'
        self.bbox = BoundingBox(
            xmin=maybe_variable_to_number(self._xmin, unit=self.units['x']),
            xmax=maybe_variable_to_number(self._xmax, unit=self.units['x']),
            ymin=maybe_variable_to_number(self._ymin, unit=self.units[key]),
            ymax=maybe_variable_to_number(self._ymax, unit=self.units[key]),
        )

    @property
    def empty(self) -> bool:
        """
        Check if the canvas is empty.
        """
        return not self.dims

    @property
    def title(self) -> str:
        """
        Get or set the title of the plot.
        """
        return self.fig.layout.title.text

    @title.setter
    def title(self, text: str):
        layout = self.fig.layout
        if not text:
            layout.margin.t = 0
        elif layout.margin.t == 0:
            layout.margin.t = 40
        layout.title = text

    @property
    def xlabel(self) -> str:
        """
        Get or set the label of the x-axis.
        """
        return self.fig.layout.xaxis.title.text

    @xlabel.setter
    def xlabel(self, lab: str):
        self.fig.layout.xaxis.title = lab

    @property
    def ylabel(self) -> str:
        """
        Get or set the label of the y-axis.
        """
        return self.fig.layout.yaxis.title.text

    @ylabel.setter
    def ylabel(self, lab: str):
        self.fig.layout.yaxis.title = lab

    @property
    def xscale(self) -> Literal['linear', 'log']:
        """
        Get or set the scale of the x-axis ('linear' or 'log').
        """
        return self.fig.layout.xaxis.type or 'linear'

    @xscale.setter
    def xscale(self, scale: Literal['linear', 'log']):
        self.fig.update_xaxes(type=scale)

    @property
    def yscale(self) -> Literal['linear', 'log']:
        """
        Get or set the scale of the y-axis ('linear' or 'log').
        """
        return self.fig.layout.yaxis.type or 'linear'

    @yscale.setter
    def yscale(self, scale: Literal['linear', 'log']):
        self.fig.update_yaxes(type=scale)

    @property
    def xmin(self) -> float:
        """
        Get or set the lower (left) bound of the x-axis.
        """
        return self.fig.layout.xaxis.range[0]

    @xmin.setter
    def xmin(self, value: float):
        self.fig.layout.xaxis.range = [value, self.xmax]

    @property
    def xmax(self) -> float:
        """
        Get or set the upper (right) bound of the x-axis.
        """
        return self.fig.layout.xaxis.range[1]

    @xmax.setter
    def xmax(self, value: float):
        self.fig.layout.xaxis.range = [self.xmin, value]

    @property
    def xrange(self) -> tuple[float, float]:
        """
        Get or set the range/limits of the x-axis.
        """
        return self.fig.layout.xaxis.range

    @xrange.setter
    def xrange(self, value: tuple[float, float]):
        self.fig.layout.xaxis.range = value

    @property
    def ymin(self) -> float:
        """
        Get or set the lower (bottom) bound of the y-axis.
        """
        return self.fig.layout.yaxis.range[0]

    @ymin.setter
    def ymin(self, value: float):
        self.fig.layout.yaxis.range = [value, self.ymax]

    @property
    def ymax(self) -> float:
        """
        Get or set the upper (top) bound of the y-axis.
        """
        return self.fig.layout.yaxis.range[1]

    @ymax.setter
    def ymax(self, value: float):
        self.fig.layout.yaxis.range = [self.ymin, value]

    @property
    def yrange(self) -> tuple[float, float]:
        """
        Get or set the range/limits of the y-axis.
        """
        return self.fig.layout.yaxis.range

    @yrange.setter
    def yrange(self, value: tuple[float, float]):
        self.fig.layout.yaxis.range = value

    @property
    def logx(self) -> bool:
        """
        Get or set whether the x-axis is in logarithmic scale.
        """
        return self.xscale == 'log'

    @logx.setter
    def logx(self, value: bool):
        self.xscale = 'log' if value else 'linear'

    @property
    def logy(self) -> bool:
        """
        Get or set whether the y-axis is in logarithmic scale.
        """
        return self.yscale == 'log'

    @logy.setter
    def logy(self, value: bool):
        self.yscale = 'log' if value else 'linear'

    def reset_mode(self):
        """
        Reset the modebar mode to nothing, to disable all zoom/pan tools.
        """
        self.fig.update_layout(dragmode=False)

    def zoom(self):
        """
        Activate the underlying Plotly zoom tool.
        """
        self.fig.update_layout(dragmode='zoom')

    def pan(self):
        """
        Activate the underlying Plotly pan tool.
        """
        self.fig.update_layout(dragmode='pan')

    def panzoom(self, value: Literal['pan', 'zoom', None]):
        """
        Activate or deactivate the pan or zoom tool, depending on the input value.
        """
        if value == 'zoom':
            self.zoom()
        elif value == 'pan':
            self.pan()
        elif value is None:
            self.reset_mode()

    def download_figure(self):
        """
        Save the figure to a PNG file via a pop-up dialog.
        """
        self.fig.write_image('figure.png')

    def toggle_logx(self):
        """
        Toggle the scale between ``linear`` and ``log`` along the horizontal axis.
        """
        self.xscale = 'log' if self.xscale in ('linear', None) else 'linear'

    def toggle_logy(self):
        """
        Toggle the scale between ``linear`` and ``log`` along the vertical axis.
        """
        self.yscale = 'log' if self.yscale in ('linear', None) else 'linear'

    def draw(self):
        pass

    def update_legend(self):
        pass

    def has_user_xlabel(self) -> bool:
        """
        Return ``True`` if the user has set an x-axis label.
        """
        return self._xlabel is not None

    def has_user_ylabel(self) -> bool:
        """
        Return ``True`` if the user has set a y-axis label.
        """
        return self._ylabel is not None
