import collections.abc
from pathlib import Path
import matplotlib

from ..models.plot_specs import FigureSpec, FigureSpecList
from .._matplotlib_axes import MatplotlibAxes
from ..utils.dict_view import DictView


class HeadlessFigures:
    """
    A headless "view" for a FigureSpecList model.

    It does not produce a graphical user interface. Instead, it provides
    methods for exporting figures as images.

    Examples
    --------

    Export all the figures to a directory. They will be named by their title.
    If there are duplciate titles, a counting number will appended like
    x-1.png, x-2.png.

    >>> headless = HeadlessFigures(model)
    >>> headless.export_all("path/to/directory/")

    Control the format.

    >>> headless.export_all("path/to/directory/", format="png")
    >>> headless.export_all("path/to/directory/", format="jpg")
    """

    def __init__(self, model: FigureSpecList):

        self.model = model
        # Map Figure UUID to widget with HeadlessFigure
        self._figures = {}

        for figure_spec in model:
            self._add_figure(figure_spec)
        model.events.added.connect(self._on_figure_added)
        model.events.removed.connect(self._on_figure_removed)

    @property
    def figures(self):
        "Read-only access to the mapping FigureSpec UUID -> HeadlessFigure"
        return DictView(self._figures)

    def _on_figure_added(self, event):
        figure_spec = event.item
        self._add_figure(figure_spec)

    def _add_figure(self, figure_spec):
        "Create a new matplotlib Figure."
        figure = HeadlessFigure(figure_spec)
        self._figures[figure_spec.uuid] = figure

    def _on_figure_removed(self, event):
        "Remove the associated tab and close its canvas."
        figure_spec = event.item
        figure = self._figures[figure_spec.uuid]
        figure.close_figure()
        del self._figures[figure_spec.uuid]

    def close_figures(self):
        for figure in self._figures.values():
            figure.close_figure()

    close = close_figures

    def export_all(self, directory, format="png", **kwargs):
        """
        Export all figures.

        Parameters
        ----------
        directory : str | Path
        format : str, optional
            Default is "png".
        **kwargs :
            Passed through to matplotlib.figure.Figure.savefig

        Returns
        -------
        filenames : List[String]
        """
        # Avoid name collisions in the case of duplicate titles by appending
        # "-1", "-2", "-3", ... to duplicates.
        titles_tallied = {}
        filenames = []
        for figure_spec in self.model:
            title = figure_spec.title
            if title in titles_tallied:
                filename = f"{title}-{titles_tallied[title]}"
                titles_tallied[title] += 1
            else:
                filename = title
                titles_tallied[title] = 1
            filename = str(Path(directory, f"{filename}.{format}"))
            figure = self._figures[figure_spec.uuid]
            figure.export(filename, format=format, **kwargs)
            filenames.append(filename)
        return filenames


class HeadlessFigure:
    """
    A Headless "view" for a FigureSpec model. This always contains one Figure.

    Examples
    --------

    Export the figure.

    >>> headless = HeadlessFigure(model)
    >>> headless.export("my-figure.png")
    """

    def __init__(self, model: FigureSpec):
        self.model = model
        self.figure, self.axes_list = _make_figure(model)
        self.figure.suptitle(model.title)
        self._axes = {}
        for axes_spec, axes in zip(model.axes, self.axes_list):
            self._axes[axes_spec.uuid] = MatplotlibAxes(model=axes_spec, axes=axes)

        model.events.title.connect(self._on_title_changed)
        # The FigureSpec model does not currently allow axes to be added or
        # removed, so we do not need to handle changes in model.axes.

    @property
    def axes(self):
        "Read-only access to the mapping AxesSpec UUID -> MatplotlibAxes"
        return DictView(self._axes)

    def _on_title_changed(self, event):
        self.figure.suptitle(event.value)

    def close_figure(self):
        _close_figure(self.figure)

    close = close_figure

    def export(self, filename, format="png", **kwargs):
        """
        Export figure.

        Parameters
        ----------
        filename : str | Path
        format : str, optional
            Default is "png".
        **kwargs :
            Passed through to matplotlib.figure.Figure.savefig
        """
        self.figure.savefig(str(filename), format=format, **kwargs)


def _make_figure(figure_spec):
    "Create a Figure and Axes."
    matplotlib.use("Agg")  # must set before importing matplotlib.pyplot
    import matplotlib.pyplot as plt  # noqa

    # TODO Let FigureSpec give different options to subplots here,
    # but verify that number of axes created matches the number of axes
    # specified.
    fig, axes = plt.subplots(len(figure_spec.axes))
    # Handl return type instability in plt.subplots.
    if not isinstance(axes, collections.abc.Iterable):
        axes = [axes]
    return fig, axes


def _close_figure(figure):
    """
    Workaround for matplotlib regression relating to closing figures in Agg

    See https://github.com/matplotlib/matplotlib/pull/18184/
    """
    # TODO It would be better to switch the approach based on matplotlib
    # versions known to have this problem, rather than blindly trying. Update
    # this once a fixed has been released and we know the earliest version of
    # matplotlib that does not have this bug.
    try:
        figure.canvas.close()
    except AttributeError:
        from matplotlib._pylab_helpers import Gcf

        num = next(
            (
                manager.num
                for manager in Gcf.figs.values()
                if manager.canvas.figure == figure
            ),
            None,
        )
        if num is not None:
            Gcf.destroy(num)
