"""Viewers to inspect data stored in file series"""

from abc import ABC, abstractmethod

# Nonstandard
from matplotlib.animation import FuncAnimation
from matplotlib.widgets import Slider


class KeyPressSlider(Slider):
    """Slider to inspect images, with keypress response"""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._connect_events()
        if self.valstep is None:
            self.keystep = (self.valmax - self.valmin) * 0.01
        else:
            self.keystep = self.valstep

    def _increase_val(self, nstep=1):
        """Increase value by a nstep steps; got to beginning if > valmax"""
        new_val = self.val + nstep * self.keystep
        if new_val > self.valmax:
            new_val = self.valmin
        self.set_val(new_val)

    def _decrease_val(self, nstep=1):
        """Increase value by a nstep steps; got to beginning if > valmax"""
        new_val = self.val - nstep * self.keystep
        if new_val < self.valmin:
            new_val = self.valmax
        self.set_val(new_val)

    def _connect_events(self):
        self.cid_keypressk = self.ax.figure.canvas.mpl_connect(
            'key_press_event',
            self._on_key_press,
        )

    def _on_key_press(self, event):
        if event.key == 'right':
            self._increase_val()
        if event.key == 'left':
            self._decrease_val()
        if event.key == 'up':
            self._increase_val(nstep=10)
        if event.key == 'down':
            self._decrease_val(nstep=10)


class DataViewerBase(ABC):
    """Base class for data inspection from file series with matplotlib"""

    def __init__(self):
        """Init Image Viewer"""
        self.plot_init_done = False

        # The parameters below are changed if necessary when methods are called
        self.kwargs = {}
        self.transform = True

    # ========================= Methods to subclass ==========================

    @abstractmethod
    def _create_figure(self):
        """Create figure and axes

        Has to define at least self.fig., and self.axs if self.axs is not
        defined in self._first_plot()
        """
        pass

    def _initialize(self):
        """Anything else to do before first plots.

        [Optional]
        """
        pass

    @abstractmethod
    def _get_data(self, num):
        """How to get image / analysis and other data to _plot for each frame.

        Parameters
        ----------
        num : int
            Image identifier

        Returns
        -------
        Any
            Output: data (arbitrary data format usable by _first_plot() and
            _update_plot())
        """
        pass

    @abstractmethod
    def _first_plot(self, data):
        """What to do the first time data arrives on the _plot.

        self.updated_artists must be defined here.
        self.axs must be defined here, except if done in self._create_figure()

        Parameters
        ----------
        data : Any
            Data generated by the analysis

        Returns
        -------
        None
        """
        pass

    @abstractmethod
    def _update_plot(self, data):
        """What to do upon iterations of the _plot after the first time.

        Parameters
        ----------
        data : Any
            Data generated by the analysis

        Returns
        -------
        None
        """
        pass

    # ========================= Other useful methods =========================

    def _plot(self, num):
        """How to plot data"""
        data = self._get_data(num)

        if not self.plot_init_done:
            self._first_plot(data)
            self.plot_init_done = True
        else:
            self._update_plot(data)

        return self.updated_artists

    # ========================= Main public methods ==========================

    def show(self, num=0):
        """Show a single, non-animated image (num: image number)."""
        self._create_figure()
        self._initialize()
        self.plot_init_done = False
        self._plot(num=num)
        return self.axs

    def animate(self, nums, blit=False):
        """Animate an image _plot with a FuncAnimation

        Parameters
        ----------
        nums : iterable of ints
            frames to consider for the animation

        blit : bool
            if True, use blitting for fast rendering
        """
        self._create_figure()
        self._initialize()
        self.plot_init_done = False

        animation = FuncAnimation(
            fig=self.fig,
            func=self._plot,
            frames=nums,
            cache_frame_data=False,
            repeat=False,
            blit=blit,
            init_func=lambda: None,  # prevents calling twice the first num
        )

        return animation

    def inspect(self, nums):
        """Inspect image series with a slider.

        Parameters
        ----------
        nums : iterable of ints
            frames to consider for the animation
        """
        num_min = min(nums)
        num_max = max(nums)

        if num_max > num_min:  # avoids division by 0 error when just 1 image
            num_step = (num_max - num_min) // (len(nums) - 1)
        else:
            num_step = 1

        self._create_figure()
        self._initialize()
        self.plot_init_done = False

        self._plot(num=num_min)

        self.fig.subplots_adjust(bottom=0.1)
        ax_slider = self.fig.add_axes([0.1, 0.01, 0.8, 0.03])

        slider = KeyPressSlider(
            ax=ax_slider,
            label='#',
            valmin=num_min,
            valmax=num_max,
            valinit=num_min,
            valstep=num_step,
            color='steelblue',
            alpha=0.5,
        )

        slider.on_changed(self._plot)

        return slider


class AnalysisViewerBase(DataViewerBase):
    """Matplotlib viewer to display analysis results alongside data"""

    def __init__(self, analysis):
        """Init analysis viewer

        Parameters
        ----------
        analysis : Analysis object
        """
        self.analysis = analysis

        # live = True when viewing actual analysis being made
        # live = False when viewing from results (analysis already performed)
        self.live = False

        # save = True means that if there is live analysis, it will
        # overwrite existing results
        # save = False means that live analysis does not impact analysis.results
        self.save = False

        super().__init__()

    # =========== Definition of methods required by DataViewerBase ===========

    def _initialize(self):
        """What to do before first plot"""
        if self.live:
            self.cid_close = self.fig.canvas.mpl_connect(
                'close_event',
                self._on_fig_close,
            )
            self.analysis._initialize()

    def _get_data(self, num):
        """Analyses classes should define adequate methods if needed"""
        # Actual anlysis being made, with a live view
        if self.live:
            data = self.analysis.analyze(num, details=True)
            self.analysis._store_data(data)
            return data
        # Post-analysis inspection of results
        else:
            return self._regenerate_data_from_results(num)

    # ============================ Other methods =============================

    def _on_fig_close(self, event):
        """This is because we want the analysis (i.e. animation) to finish
        before saving the data in live mode."""
        # if live, it's the _on_fig_close() method of the viewer which takes
        # care of saving the data, because if not, save_results() is called
        # at the beginning of the FuncAnimation (i.e., analysis in this case,
        # and no data is saved)
        if self.live:
            self.analysis._finalize()
            if self.save:
                self.analysis._save()
                self.save = False
            self.live = False

    # ========================= Methods to subclass ==========================

    # -------------------- Inherited from DataViewerBase ---------------------

    @abstractmethod
    def _create_figure(self):
        """Create figure and axes

        Has to define at least self.fig., and self.axs if self.axs is not
        defined in self._first_plot()
        """
        pass

    @abstractmethod
    def _first_plot(self, data):
        """What to do the first time data arrives on the _plot.

        self.updated_artists must be defined here.
        self.axs must be defined here, except if done in self._create_figure()

        Parameters
        ----------
        data : Any
            Data generated by the analysis

        Returns
        -------
        None
        """
        pass

    @abstractmethod
    def _update_plot(self, data):
        """What to do upon iterations of the _plot after the first time.

        Parameters
        ----------
        data : Any
            Data generated by the analysis

        Returns
        -------
        None
        """
        pass

    # ----------------------- New methods to subclass ------------------------

    def _regenerate_data_from_results(self, num):
        """To subclass (optional)"""
        pass


class FormattedAnalysisViewerBase(AnalysisViewerBase):
    """AnalysisViewer for FormattedAnalysis."""

    def _regenerate_data_from_results(self, num):
        return self.analysis.formatter._regenerate_data_from_results(num)
