# SPDX-License-Identifier: GPL-3.0-or-later
"""
Custom figure implementation.

Acts as an interface between matplotlib and GObject.
"""
import contextlib
import logging

from gi.repository import GObject, Gio, Graphs, Gtk

from graphs import artist, misc

from matplotlib import RcParams, figure, pyplot


def _ellipsize(text: str, max_chars: int) -> str:
    """Truncate text with an ellipsis."""
    return (text[:max_chars] + "…") if len(text) > max_chars else text


class Figure(GObject.Object, figure.Figure):
    """Custom Figure."""

    __gtype_name__ = "GraphsFigure"

    def __init__(
        self,
        style_params: tuple[RcParams, dict],
        items: Gio.ListModel,
        parent=None,
        figure_settings: Graphs.FigureSettings = None,
    ):
        GObject.Object.__init__(self)
        self._style_params = style_params
        self._items = items
        self.parent = parent
        pyplot.rcParams.update(self._style_params[0])  # apply style_params
        figure.Figure.__init__(self, tight_layout=True)

        self.axis = self.add_subplot(111)
        self.top_left_axis = self.axis.twiny()
        self.right_axis = self.axis.twinx()
        self.top_right_axis = self.top_left_axis.twinx()
        self._legend_axis = self.axis
        self._artists = []

        if figure_settings is not None:
            bottom_scale = \
                Graphs.scale_to_string(figure_settings.get_bottom_scale())
            top_scale = \
                Graphs.scale_to_string(figure_settings.get_top_scale())
            left_scale = \
                Graphs.scale_to_string(figure_settings.get_left_scale())
            right_scale = \
                Graphs.scale_to_string(figure_settings.get_right_scale())
            self.axis.set_xscale(bottom_scale)
            self.axis.set_yscale(left_scale)
            self.top_left_axis.set_xscale(top_scale)
            self.top_left_axis.set_yscale(left_scale)
            self.right_axis.set_xscale(bottom_scale)
            self.right_axis.set_yscale(right_scale)
            self.top_right_axis.set_xscale(top_scale)
            self.top_right_axis.set_yscale(right_scale)

            limits = figure_settings.get_limits().values()
            self.axis.set_xlim(limits[0], limits[1])
            self.axis.set_ylim(limits[4], limits[5])
            self.top_left_axis.set_xlim(limits[2], limits[3])
            self.top_left_axis.set_ylim(limits[4], limits[5])
            self.right_axis.set_xlim(limits[0], limits[1])
            self.right_axis.set_ylim(limits[6], limits[7])
            self.top_right_axis.set_xlim(limits[2], limits[3])
            self.top_right_axis.set_ylim(limits[6], limits[7])

            title = figure_settings.get_title()
            self.axis.set_title(title, picker=True).id = "title"
            self.axis.set_xlabel(
                figure_settings.get_bottom_label(),
                picker=True,
            ).id = "bottom_label"
            self.top_left_axis.set_xlabel(
                figure_settings.get_top_label(),
                picker=True,
            ).id = "top_label"
            self.axis.set_ylabel(
                figure_settings.get_left_label(),
                picker=True,
            ).id = "left_label"
            self.right_axis.set_ylabel(
                figure_settings.get_right_label(),
                picker=True,
            ).id = "right_label"

            self._hide_unselected = figure_settings.get_hide_unselected()
            self._legend = figure_settings.get_legend()
            pos = figure_settings.get_legend_position()
            self._legend_position = Graphs.legend_position_to_string(pos)
        else:
            self._hide_unselected = False
            self._legend = True
            self._legend_position = "best"

        self._item_handlers = [items.connect("items-changed", self._redraw)]
        if isinstance(items, Gtk.SelectionModel):
            self._item_handlers.append(
                items.connect("selection-changed", self._redraw),
            )
        self._redraw()

    def _drop_artists(self) -> None:
        """Release the current artist wrappers and their subscriptions."""
        for handle in self._artists:
            with contextlib.suppress(AttributeError):
                handle.disconnect_item()
        self._artists = []

    def detach(self) -> None:
        """Stop tracking the data model before the canvas is replaced."""
        for handler in self._item_handlers:
            self._items.disconnect(handler)
        self._item_handlers = []
        self._drop_artists()

    def _redraw(self, *_args) -> None:
        logging.debug("redrawing figure")
        # bottom, top, left, right
        used_axes = [False, False, False, False]
        visible_axes = [False, False, False, False]
        drawable_items = [x for x in self._items if x.get_selected()] \
            if self._hide_unselected else list(self._items)
        for item in drawable_items:
            xposition = item.get_xposition()
            yposition = item.get_yposition()
            visible_axes[xposition] = True
            visible_axes[2 + yposition] = True
            used_axes[xposition + 2 * yposition] = True
        axes_directions = (
            ("bottom", "left"),  # axis
            ("top", "left"),  # top_left_axis
            ("bottom", "right"),  # right_axis
            ("top", "right"),  # top_right_axis
        )

        if not any(visible_axes):
            visible_axes = (True, False, True, False)  # Left and bottom
            used_axes = (True, False, False, False)  # self.axis visible
            self._legend_axis = self.axis

        params, graphs_params = self._style_params
        draw_frame = params["axes.spines.bottom"]
        ticks = "both" if params["xtick.minor.visible"] else "major"
        for directions, axis, used \
                in zip(axes_directions, self.axes, used_axes):
            axis.get_xaxis().set_visible(False)
            axis.get_yaxis().set_visible(False)
            # Set tick where requested, as long as that axis is not occupied
            # and visible
            if (
                params[f"xtick.{directions[0]}"]
                or params[f"ytick.{directions[1]}"]
            ):
                tick_params = {}
                for i, direction in enumerate(misc.DIRECTIONS):
                    tick_shown = (
                        (draw_frame and not visible_axes[i])
                        or direction in directions
                    ) and params[f"{'x' if i < 2 else 'y'}tick.{direction}"]

                    tick_params[direction] = tick_shown
                    if graphs_params["ticklabels"]:
                        tick_params[f"label{direction}"] = tick_shown

                axis.tick_params(which=ticks, **tick_params)

            for handle in [*axis.collections, *axis.lines, *axis.texts]:
                handle.remove()
            axis_legend = axis.get_legend()
            if axis_legend is not None:
                axis_legend.remove()
            for direction in misc.DIRECTIONS:
                axis.spines[direction].set_visible(
                    direction in directions and used or draw_frame,
                )
            if used:
                self._legend_axis = axis

        self.axis.get_xaxis().set_visible(visible_axes[0])
        self.top_left_axis.get_xaxis().set_visible(visible_axes[1])
        self.axis.get_yaxis().set_visible(visible_axes[2])
        self.right_axis.get_yaxis().set_visible(visible_axes[3])

        self._drop_artists()
        self._artists = [
            artist.new_for_item(self, item)
            for item in reversed(drawable_items)
        ]
        self.update_legend()

    def update_legend(self, *_args) -> None:
        """Update the legend or hide if not used."""
        if self._legend and self._artists:
            handles = [
                handle.get_artist() for handle in self._artists
                if handle.legend
            ]
            if handles:
                max_char = max(10, int(self.bbox.width / 15))
                labels = [_ellipsize(h.get_label(), max_char) for h in handles]
                self._legend_axis.legend(
                    handles=handles,
                    labels=labels,
                    loc=self._legend_position,
                    frameon=True,
                    reverse=True,
                )
                self.queue_draw()
                return
        legend = self._legend_axis.get_legend()
        if legend is not None:
            legend.remove()
        self.queue_draw()

    def set_properties(*args):
        """Pass method on to Figure."""
        figure.Figure.set_properties(*args)

    def set_property(*args):
        """Pass method on to Figure."""
        figure.Figure.set_property(*args)

    def queue_draw(self) -> None:
        """Queue a draw when in a canvas."""
        with contextlib.suppress(AttributeError):
            self.canvas.queue_draw()

    @GObject.Property(type=bool, default=False)
    def hide_unselected(self) -> bool:
        """Whether or not to hide unselected items."""
        return self._hide_unselected

    @hide_unselected.setter
    def hide_unselected(self, hide_unselected: bool) -> None:
        self._hide_unselected = hide_unselected
        self._redraw()

    @GObject.Property(type=bool, default=True)
    def legend(self) -> bool:
        """Whether or not, the legend is visible."""
        return self._legend

    @legend.setter
    def legend(self, legend: bool) -> None:
        self._legend = legend
        self.update_legend()

    @GObject.Property(
        type=Graphs.LegendPosition,
        default=Graphs.LegendPosition.BEST,
    )
    def legend_position(self) -> int:
        """Legend Position."""
        return Graphs.LegendPosition.from_string(self._legend_position)

    @legend_position.setter
    def legend_position(self, pos: Graphs.LegendPosition) -> None:
        self._legend_position = Graphs.legend_position_to_string(pos)
        self.update_legend()

    @GObject.Property(type=str)
    def title(self) -> str:
        """Figure title."""
        return self.axis.get_title()

    @title.setter
    def title(self, title: str) -> None:
        self.axis.set_title(title, picker=True).id = "title"
        self.queue_draw()

    @GObject.Property(type=str)
    def bottom_label(self) -> str:
        """Label of the bottom axis."""
        return self.axis.get_xlabel()

    @bottom_label.setter
    def bottom_label(self, label: str) -> None:
        self.axis.set_xlabel(label, picker=True).id = "bottom_label"
        self.queue_draw()

    @GObject.Property(type=str)
    def left_label(self) -> str:
        """Label of the left axis."""
        return self.axis.get_ylabel()

    @left_label.setter
    def left_label(self, label: str) -> None:
        self.axis.set_ylabel(label, picker=True).id = "left_label"
        self.queue_draw()

    @GObject.Property(type=str)
    def top_label(self) -> str:
        """Label of the top axis."""
        return self.top_left_axis.get_xlabel()

    @top_label.setter
    def top_label(self, label: str) -> None:
        self.top_left_axis.set_xlabel(label, picker=True).id = "top_label"
        self.queue_draw()

    @GObject.Property(type=str)
    def right_label(self) -> str:
        """Label of the right axis."""
        return self.right_axis.get_ylabel()

    @right_label.setter
    def right_label(self, label: str) -> None:
        self.right_axis.set_ylabel(label, picker=True).id = "right_label"
        self.queue_draw()

    @GObject.Property(type=int)
    def bottom_scale(self) -> int:
        """Scale of the bottom axis."""
        return Graphs.scale_from_string(self.axis.get_xscale())

    @bottom_scale.setter
    def bottom_scale(self, scale: int) -> None:
        scale = Graphs.scale_to_string(scale)
        for axis in (self.axis, self.right_axis):
            axis.set_xscale(scale)
            axis.set_xlim(None, None)
        self.queue_draw()

    @GObject.Property(type=int)
    def left_scale(self) -> int:
        """Scale of the left axis."""
        return Graphs.scale_from_string(self.axis.get_yscale())

    @left_scale.setter
    def left_scale(self, scale: int) -> None:
        scale = Graphs.scale_to_string(scale)
        for axis in (self.axis, self.top_left_axis):
            axis.set_yscale(scale)
            axis.set_ylim(None, None)
        self.queue_draw()

    @GObject.Property(type=int)
    def top_scale(self) -> int:
        """Scale of the top axis."""
        return Graphs.scale_from_string(self.top_left_axis.get_xscale())

    @top_scale.setter
    def top_scale(self, scale: int) -> None:
        scale = Graphs.scale_to_string(scale)
        for axis in (self.top_right_axis, self.top_left_axis):
            axis.set_xscale(scale)
            axis.set_xlim(None, None)
        self.queue_draw()

    @GObject.Property(type=int)
    def right_scale(self) -> int:
        """Scale of the right axis."""
        return Graphs.scale_from_string(self.right_axis.get_yscale())

    @right_scale.setter
    def right_scale(self, scale: int) -> None:
        scale = Graphs.scale_to_string(scale)
        for axis in (self.top_right_axis, self.right_axis):
            axis.set_yscale(scale)
            axis.set_ylim(None, None)
        self.queue_draw()

    @GObject.Property(type=float)
    def min_bottom(self) -> float:
        """Lower limit for the bottom axis."""
        return self.axis.get_xlim()[0]

    @min_bottom.setter
    def min_bottom(self, value: float) -> None:
        for axis in (self.axis, self.right_axis):
            axis.set_xlim(value, None)
        self.queue_draw()

    @GObject.Property(type=float)
    def max_bottom(self) -> float:
        """Upper limit for the bottom axis."""
        return self.axis.get_xlim()[1]

    @max_bottom.setter
    def max_bottom(self, value: float) -> None:
        for axis in (self.axis, self.right_axis):
            axis.set_xlim(None, value)
        self.queue_draw()

    @GObject.Property(type=float)
    def min_left(self) -> float:
        """Lower limit for the left axis."""
        return self.axis.get_ylim()[0]

    @min_left.setter
    def min_left(self, value: float) -> None:
        for axis in (self.axis, self.top_left_axis):
            axis.set_ylim(value, None)
        self.queue_draw()

    @GObject.Property(type=float)
    def max_left(self) -> float:
        """Upper limit for the left axis."""
        return self.axis.get_ylim()[1]

    @max_left.setter
    def max_left(self, value: float) -> None:
        for axis in (self.axis, self.top_left_axis):
            axis.set_ylim(None, value)
        self.queue_draw()

    @GObject.Property(type=float)
    def min_top(self) -> float:
        """Lower limit for the top axis."""
        return self.top_left_axis.get_xlim()[0]

    @min_top.setter
    def min_top(self, value: float) -> None:
        for axis in (self.top_left_axis, self.top_right_axis):
            axis.set_xlim(value, None)
        self.queue_draw()

    @GObject.Property(type=float)
    def max_top(self) -> float:
        """Upper limit for the top axis."""
        return self.top_left_axis.get_xlim()[1]

    @max_top.setter
    def max_top(self, value: float) -> None:
        for axis in (self.top_left_axis, self.top_right_axis):
            axis.set_xlim(None, value)
        self.queue_draw()

    @GObject.Property(type=float)
    def min_right(self) -> float:
        """Lower limit for the right axis."""
        return self.right_axis.get_ylim()[0]

    @min_right.setter
    def min_right(self, value: float) -> None:
        for axis in (self.right_axis, self.top_right_axis):
            axis.set_ylim(value, None)
        self.queue_draw()

    @GObject.Property(type=float)
    def max_right(self) -> float:
        """Upper limit for the right axis."""
        return self.right_axis.get_ylim()[1]

    @max_right.setter
    def max_right(self, value: float) -> None:
        for axis in (self.right_axis, self.top_right_axis):
            axis.set_ylim(None, value)
        self.queue_draw()
