# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see plotpy/LICENSE for details)

# pylint: disable=C0103

"""
Label Item builder
------------------

This module provides a set of factory functions to simplify the creation
of label items.
"""

# Note: when adding method to builder classes, please do not forget to update the
# documentation (see builder.rst file). Because of class inheritance, the methods
# are not automatically documented (otherwise, they would be sorted alphabetically,
# due to a limitation of sphinx auto-doc).

from __future__ import annotations

from collections.abc import Callable

from plotpy.config import CONF, _, make_title
from plotpy.items import (
    CurveItem,
    DataInfoLabel,
    ImageItem,
    LabelItem,
    LegendBoxItem,
    RangeComputation2d,
    RangeInfo,
    RectangleShape,
    SelectedLegendBoxItem,
    XRangeComputation,
    XRangeSelection,
    YRangeComputation,
    YRangeSelection,
)
from plotpy.styles import LabelParam, LabelParamWithContents, LegendParam

# default offset positions for anchors
ANCHOR_OFFSETS = {
    "TL": (5, 5),
    "TR": (-5, 5),
    "BL": (5, -5),
    "BR": (-5, -5),
    "L": (5, 0),
    "R": (-5, 0),
    "T": (0, 5),
    "B": (0, -5),
}

LABEL_COUNT = 0


class LabelBuilder:
    """Class regrouping a set of factory functions to simplify the creation
    of label items."""

    def label(
        self,
        text: str,
        g: tuple[float, float] | str,
        c: tuple[int, int],
        anchor: str,
        title: str = "",
    ) -> LabelItem:
        """Make a label `plot item`

        Args:
            text: label text
            g: position in plot coordinates or relative position (string)
            c: position in canvas coordinates
            anchor: anchor position in relative position (string)
            title: label title. Default is ''

        Returns:
            :py:class:`.LabelItem` object

        Examples::
            make.label("Relative position", (x[0], y[0]), (10, 10), "BR")
            make.label("Absolute position", "R", (0,0), "R")
        """
        basename = _("Label")
        param = LabelParamWithContents(basename, icon="label.png")
        param.read_config(CONF, "plot", "label")
        if title:
            param.label = title
        else:
            global LABEL_COUNT
            LABEL_COUNT += 1
            param.label = make_title(basename, LABEL_COUNT)
        if isinstance(g, tuple):
            param.abspos = False
            param.xg, param.yg = g
        else:
            param.abspos = True
            param.absg = g
        if c is None:
            c = ANCHOR_OFFSETS[anchor]
        param.xc, param.yc = c
        param.anchor = anchor
        return LabelItem(text, param)

    def legend(
        self,
        anchor: str = "TR",
        c: tuple[int, int] | None = None,
        restrict_items: list | None = None,
    ) -> LegendBoxItem | SelectedLegendBoxItem:
        """Make a legend `plot item`

        Args:
            anchor: legend position in relative position (string)
            c: position in canvas coordinates
            restrict_items: list of items to be shown in legend box.
             Default is None. If None, all items are shown in legend box.
             If [], no item is shown in legend box. If [item1, item2],
             item1, item2 are shown in legend box.

        Returns:
            :py:class:`.LegendBoxItem` or
            :py:class:`.SelectedLegendBoxItem` object
        """
        param = LegendParam(_("Legend"), icon="legend.png")
        param.read_config(CONF, "plot", "legend")
        param.abspos = True
        param.absg = anchor
        param.anchor = anchor
        if c is None:
            c = ANCHOR_OFFSETS[anchor]
        param.xc, param.yc = c
        if restrict_items is None:
            return LegendBoxItem(param)
        else:
            return SelectedLegendBoxItem(param, restrict_items)

    def info_label(
        self,
        anchor: str,
        comps: list[XRangeSelection] | list[YRangeSelection],
        title: str | None = None,
    ) -> DataInfoLabel:
        """Make an info label `plot item`

        Args:
            anchor: anchor position. See :py:class:`.LabelParam` for details
            comps: list of :py:class:`.label.XRangeComputation` or
             :py:class:`.label.YRangeComputation` objects
            title: label name. Default is None

        Returns:
            :py:class:`.DataInfoLabel` object
        """
        basename = _("Computation")
        param = LabelParam(basename, icon="label.png")
        param.read_config(CONF, "plot", "info_label")
        if title is not None:
            param.label = title
        else:
            global LABEL_COUNT
            LABEL_COUNT += 1
            param.label = make_title(basename, LABEL_COUNT)
        param.abspos = True
        param.absg = anchor
        param.anchor = anchor
        c = ANCHOR_OFFSETS[anchor]
        param.xc, param.yc = c
        return DataInfoLabel(param, comps)

    def range_info_label(
        self,
        range: XRangeSelection | YRangeSelection,
        anchor: str,
        label: str,
        function: Callable = None,
        title: str | None = None,
    ) -> DataInfoLabel:
        """Make an info label `plot item` showing an `XRangeSelection` or
        `YRangeSelection` object information.

        Args:
            range: range selection object
            anchor: anchor position. See :py:class:`.LabelParam` for details
            label: label name. See :py:class:`.DataInfoLabel` for details
            function: function to apply to the range selection object
             Default is None (default function is `lambda x, dx: (x, dx)`)
            title: label name. Default is None

        Returns:
            :py:class:`.DataInfoLabel` object

        Example::

            x = linspace(-10, 10, 10)
            y = sin(sin(sin(x)))
            range = make.xrange(-2, 2)
            disp = make.range_info_label(range, 'BL', "x = %.1f ± %.1f cm",
                                         lambda x, dx: (x, dx))
        """
        info = RangeInfo(label, range, function)
        return self.info_label(anchor, info, title=title)

    def computation(
        self,
        range: XRangeSelection | YRangeSelection,
        anchor: str,
        label: str,
        curve: CurveItem | None = None,
        function: Callable | None = None,
        title: str | None = None,
    ) -> DataInfoLabel:
        """Make a computation label `plot item`

        Args:
            range: range selection object
            anchor: anchor position. See :py:class:`.LabelParam` for details
            label: label name. See :py:class:`.DataInfoLabel` for details
            curve: curve item (not used for `YRangeSelection`)
            function: function to apply to the range selection object
             Default is None (default function is `lambda x, dx: (x, dx)`)

        Returns:
            :py:class:`.DataInfoLabel` object
        """
        if title is None:
            title = curve.param.label
        if isinstance(range, XRangeSelection):
            specs = [(curve, label, function)]
        elif isinstance(range, YRangeSelection):
            specs = [(label, function)]
        else:
            raise TypeError(
                "Invalid range selection type: "
                f"{type(range).__name__}. "
                "Expected XRangeSelection or YRangeSelection."
            )
        return self.computations(range, anchor, specs, title=title)

    def computations(
        self,
        rangeselection: XRangeSelection | YRangeSelection,
        anchor: str,
        specs: list[tuple[CurveItem, str, Callable]] | list[tuple[str, Callable]],
        title: str | None = None,
    ) -> DataInfoLabel:
        """Make computation labels  `plot item`

        Args:
            rangeselection: range selection object
            anchor: anchor position. See :py:class:`.LabelParam` for details
            specs: list of (curve, label, function) tuples for `XRangeComputation`,
             list of (label, function) tuples for `YRangeComputation`
            title: label name. Default is None

        Returns:
            Data info label object
        """
        comps = []
        if isinstance(rangeselection, XRangeSelection):
            same_curve = True
            curve0 = None
            for curve, label, function in specs:
                comp = XRangeComputation(label, curve, rangeselection, function)
                comps.append(comp)
                if curve0 is None:
                    curve0 = curve
                same_curve = same_curve and curve is curve0
            if title is None and same_curve:
                title = curve.param.label
        elif isinstance(rangeselection, YRangeSelection):
            for label, function in specs:
                comp = YRangeComputation(label, rangeselection, function)
                comps.append(comp)
        else:
            raise TypeError(
                "Invalid range selection type: "
                f"{type(rangeselection).__name__}. "
                "Expected XRangeSelection or YRangeSelection."
            )
        return self.info_label(anchor, comps, title=title)

    def computation2d(
        self,
        rect: RectangleShape,
        anchor: str,
        label: str,
        image: ImageItem,
        function: Callable,
        title: str | None = None,
    ) -> DataInfoLabel:
        """Make a 2D computation label `plot item`

        Args:
            rect: rectangle selection object
            anchor: anchor position. See :py:class:`.LabelParam` for details
            label: label name. See :py:class:`.DataInfoLabel` for details
            image: image item
            function: function to apply to the rectangle selection object
             Default is None (default function is `lambda x, dx: (x, dx)`)
            title: label name. Default is None

        Returns:
            Data info label object
        """
        return self.computations2d(
            rect, anchor, [(image, label, function)], title=title
        )

    def computations2d(
        self, rect: RectangleShape, anchor: str, specs: list, title: str | None = None
    ) -> DataInfoLabel:
        """Make 2D computation labels `plot item`

        Args:
            rect: rectangle selection object
            anchor: anchor position. See :py:class:`.LabelParam` for details
            specs: list of (image, label, function) tuples
            title: label name. Default is None

        Returns:
            Data info label object
        """
        comps = []
        same_image = True
        image0 = None
        for image, label, function in specs:
            comp = RangeComputation2d(label, image, rect, function)
            comps.append(comp)
            if image0 is None:
                image0 = image
            same_image = same_image and image is image0
        if title is None and same_image:
            title = image.param.label
        return self.info_label(anchor, comps, title=title)
