"""Base classes to manage formatting of analysis results"""

# Standard library
from abc import ABC, abstractmethod

# Non-standard
import pandas as pd


class FormatterBase(ABC):
    """Base class for formatting of results spit out by analysis methods"""

    def __init__(self, analysis):
        """Link formatter to analysis object"""
        self.analysis = analysis

    def _to_results(self):
        self.analysis.results.data = self._to_results_data()
        self.analysis.results.metadata = self._to_results_metadata()

    def _to_results_metadata(self):
        transform_metadata = self._get_transform_metadata()
        other_metadata = self._to_metadata()
        return {**other_metadata, 'transforms': transform_metadata}

    def _get_transform_metadata(self):
        """Metadata about image transforms (rotation, crop etc.) """
        info = {}

        for name, correction in self.analysis.data_series.corrections.items():
            # Because correction data typically cannot be saved to JSON
            info[name] = bool(correction.data)

        for name, transform in self.analysis.data_series.transforms.items():
            info[name] = transform.data

        return info

    def _regenerate_data_from_results(self, num):
        """How to go back to raw data from data stored in results"""
        if self.analysis.results.data is None:  # analysis not run or reset
            analysis_data = {}
        else:
            analysis_data = self._regenerate_analysis_data(num=num)
        additional_data = self._regenerate_additional_data(num=num)
        return {'num': num, **analysis_data, **additional_data}

    # ============================= To subclass ==============================

    @abstractmethod
    def _prepare_data_storage(self):
        """Prepare structure(s) that will hold the analyzed data"""
        pass

    @abstractmethod
    def _store_data(self, data):
        """How to store data generated by analysis on a single image.

        Parameters
        ----------
        data : dict
            Dictionary of data, output of Analysis.analyze()
        """
        pass

    @abstractmethod
    def _to_results_data(self):
        """How to pass stored data into a Results class/subclass.

        (is executed at the end of analysis)

        Returns
        ------
        Any
            data in the format that will be stored in results.data
        """
        return None

    def _to_metadata(self):
        """What metadata to save in the Results class/subclass.

        [OPTIONAL]
        (is executed at the end of analysis)

        Returns
        ------
        dict
            metadata dictionary that will be stored in results.metadata
            (note: transform metadata added automatically)
        """
        return {}

    def _regenerate_analysis_data(self, num):
        """How to go back to raw data (as spit out by the analysis methods
        during analysis) from data saved in results or files.

        [OPTIONAL]

        Useful for plotting / animating results again after analysis, among
        other things.

        Parameters
        ----------
        num : int
            data identifier in the data series

        Returns
        -------
        dict
            data in the format generated by analysis.analyze()

        Notes
        -----
            'num' key is added automatically by _regenerate_data_from_results()
            in the output dict.
        """
        return {}

    def _regenerate_additional_data(self, num):
        """Any additional data to add to analysis data regeneration output by
        _regenerate_analysis_data() (e.g. images, specrea etc.)

        [OPTIONAL]

        Parameters
        ----------
        num : int
            data identifier in the data series

        Returns
        -------
        dict
        """
        return {}


class PandasFormatterBase(FormatterBase):
    """Base class for formatting results as a single pandas DataFrame

    In this formatter, both data and metadata as passed as dicts.
    """
    @property
    def columns(self):
        return list(self._column_names())

    @property
    def results_dataframe(self):
        """Indicate where pandas dataframs is in results.

        In many cases, it will be results.data directly (default)
        but in some applications it will be something else
        (e.g. multiformatters).

        Subclass if necessary
        """
        return self.analysis.results.data

    # ================= Subclassing of FormatterBase methods =================

    def _prepare_data_storage(self):
        self.data = pd.DataFrame(columns=self.columns)
        self.data.index.name = 'num'

    def _store_data(self, data):
        num = data['num']

        # Doing two times the same analysis (e.g. when using inspect(live=True)
        # Should return the same result, so this case is simply ignored)
        if num in self.data.index and self.analysis.independent_results:
            return

        # Avoid bugs if data_to_results_row deactivated and does not
        # process data
        if self._data_to_results_row(data) is None:
            return

        self.data.loc[num] = self._data_to_results_row(data)

    def _regenerate_analysis_data(self, num):
        """How to go back to raw data from data stored in results"""
        data = self.results_dataframe[self.columns]
        try:
            row = data.loc[num]
        except KeyError:    # Analysis not made for the specific num
            return {}
        else:
            return self._results_row_to_data(row)

    # ============= FormatterBase/Formatter methods to subclass ==============

    @abstractmethod
    def _to_results_data(self):
        """How to pass stored data into a Results class/subclass.

        (is executed at the end of analysis)

        Returns
        ------
        Any
            data in the format that will be stored in results.data
        """
        return None

    def _to_metadata(self):
        """What metadata to save in the Results class/subclass.

        [OPTIONAL]
        (is executed at the end of analysis)

        Returns
        ------
        dict
            metadata dictionary that will be stored in results.metadata
            (note: transform metadata added automatically)
        """
        return {}

    def _regenerate_additional_data(self, num):
        """Any additional data to add to analysis data regeneration output by
        _regenerate_analysis_data() (e.g. images, specrea etc.)

        [OPTIONAL]

        Parameters
        ----------
        num : int
            data identifier in the data series

        Returns
        -------
        dict
        """
        return {}

    # ================= New methods to define in subclasses ==================

    @abstractmethod
    def _column_names(self):
        """Columns of the analysis data (iterable)"""
        pass

    @abstractmethod
    def _data_to_results_row(self, data):
        """Generate iterable of data that fits in the defined columns.

        Input
        -----
        data is a dictionary, output of Analysis.analyze()

        Returns
        -------
        iterable
            must have a length equal to self._column_names()
        """
        pass

    def _results_row_to_data(self, row):
        """Go from row of data to raw data

        [Optional]
        For data regeneration from stored results

        Returns
        -------
        dict
        """
        return {}
