"""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())

    # ================= 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

        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.analysis.results.data[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 {}

    def _regenerate_additional_data(num):
        """Any additional data to include in data regeneration

        [Optional]
        For data regeneration from stored results

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