import abc
import logging
from pathlib import Path

from immuneML.reports.ReportOutput import ReportOutput
from immuneML.reports.ReportResult import ReportResult
from immuneML.util.Logger import print_log


class Report(metaclass=abc.ABCMeta):
    """
    This class defines what report classes should look like: they all have to inherit this class and implement the abstract methods: build_object()
    from parameters and generate() the report once all properties are set (in immuneML this will be taken care of by the instructions). If there are
    any prerequisites needed to run the report (e.g., check if all parameter values are properly set), the check_prerequisites function should be
    overwritten to reflect that and determine if everything is set before generate() is run. See specific functions for more details.

    """

    def __init__(self, name: str = None, result_path: Path = None, number_of_processes: int = 1):
        self.name = name
        self.result_path = result_path
        self.number_of_processes = number_of_processes

    @classmethod
    @abc.abstractmethod
    def build_object(cls, **kwargs):
        """
        Creates the object of the subclass of the Report class from the parameters so that it can be used in the analysis. Depending on the type of
        the report, the parameters provided here will be provided in parsing time, while the other necessary parameters (e.g., subset of the data from
        which the report should be created) will be provided at runtime. For more details, see specific direct subclasses of this class, describing
        different types of reports.

        Args:

            **kwargs: keyword arguments that will be provided by users in the specification (if immuneML is used as a command line tool) or in the
             dictionary when calling the method from the code, and which should be used to create the report object

        Returns:

            the object of the appropriate report class

        """
        pass

    @abc.abstractmethod
    def _generate(self) -> ReportResult:
        """
        The function that needs to be implemented by the Report subclasses which actually creates the report (figures, tables, text files), depending
        on the specific aim of the report. After checking all prerequisites (e.g., if all parameters were set properly), generate_report() will call
        this function and return its result.

        Returns:

            ReportResult object which encapsulates all outputs (figure, table, and text files) so that they can be conveniently linked to in the
            final output of instructions

        """
        pass

    def check_prerequisites(self) -> bool:
        """
        Checks prerequisites for the generation of the report of specific class (e.g., if the class of the MLMethod instance is the one required by
        the report, if the data has been encoded to make a report of encoded dataset). In the instructions in immuneML, this function is used to
        determine whether to call generate_report() in the specific situation. Each report subclass has its own set of prerequisites. If the report
        cannot be run, the information on this will be logged and the report skipped in the specific situation. No error will be raised. See
        subclasses of the class :py:obj:`~immuneML.workflows.instructions.Instruction.Instruction` for more information on how the reports are executed.

        Returns:
             boolean value True if the prerequisites are o.k., and False otherwise.
        """
        return True

    def set_context(self, context: dict):
        """
        Context is a dictionary with information that is accessible from the level of instruction and can be used to precompute certain values that
        can be later reused to speed up the generation of the subsequent reports of the same time. For instance, if one should compute the distance
        between all repertoires based on the sequence content, it is possible to store the full dataset in the context, compute the distances on the
        full dataset and then only extract the distances need for the current dataset in the later calls (e.g., when training dataset is passed as
        input). Only some reports will need this functionality.

        Warning: It is very important to be careful when using the context to avoid leaking the information between training and test datasets.

        Args:

            context (dict): a dictionary where the values are variables that are typically only available on the top-level of the instruction, and
                which are used to precompute results in order to speed up subsequent generation of the same report on subsets of those values.

        Returns:

            self - so that it can be chained with the other function calls

        """
        return self

    def generate_report(self) -> ReportResult:
        """
        Generates a report of the given class if the prerequisites are satisfied. It handles all exceptions so that if there is an error while
        generating a report, the execution of the rest of the code (e.g., more time-expensive parts, like instructions) is not influenced.

        Returns:

            ReportResult object which encapsulates all outputs (figure, table, and text files) so that they can be conveniently linked to in the
            final output of instructions

        # """
        try:
            if self.check_prerequisites():
                return self._generate()
            else:
                print_log(
                    f"Report {self.name} failed while checking the prerequisites and was not generated, see the "
                    f"log file for more information.",
                    include_datetime=True)
                return ReportResult(name=f"{self.name} (failed)",
                                    info="This report failed while checking the prerequisites. This usually means "
                                         "the wrong dataset type was used, that wrong input parameters were specified "
                                         "or that the specific report is not applicable in this case. "
                                         "See the log file for more information.")
        except Exception as e:
            logging.exception(f"An exception occurred while generating report {self.name}. See the details below:")
            print_log(
                f"Report {self.name} encountered an error and could not be generated (error: {e}). "
                f"See the log file for more info.",
                include_datetime=True)
            return ReportResult(name=f"{self.name} (failed)",
                                info="This report failed during execution, see the log file for more information.")

    def _safe_plot(self, output_written=True, plot_callable="_plot", **kwargs):
        """
        A wrapper around the function _plot() which catches any error that may be thrown by this function (e.g. errors in R),
        and shows an informative warning message instead. This is to prevent immuneML from crashing when the analysis has been
        completed but only a figure could not be plotted.

        Args:

            output_written: indicates whether the output was written to a file, this changes the error message.

            kwargs: passed to _plot()

        Returns:

            the results of _plot(), typically a ReportOutput object

        """
        warning_mssg = f"{self.__class__.__name__}: an error occurred when attempting to plot the data. \n"
        if output_written:
            warning_mssg += "\nThe data has been written to a file, but no plot has been created."
        else:
            warning_mssg += "\nNo plot has been created."
        try:
            plot = getattr(self, plot_callable, None)
            if callable(plot):
                return plot(**kwargs)
        except Exception as e:
            logging.exception(
                f"An exception occurred while plotting the data in report {self.name}. See the details below:")
            print_log(warning_mssg, include_datetime=True)

    def _write_output_table(self, table, file_path, name=None):
        sep = "," if file_path.suffix == ".csv" else "\t"
        table.to_csv(file_path, index=False, sep=sep)

        return ReportOutput(path=file_path, name=name)
