# Package: poetic (poetic-py)
# Author: Kevin Wang
#
# The MIT License (MIT)
#
# Copyright 2020 Kevin Wang
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
""" Module for processing prediction results.

The results module processes the outputs of prediction results
from the Predictor of the predictor module. The functionalities
provided include statistical summaries, io, and diagnostic
reports.

Examples:

    The results module's Diagnostics module can be used as part of
    the prediction workflow. To use its methods with the Predictor
    class: 
    
    .. code-block:: python
    
        import poetic
    
        pred = poetic.Predictor()
        result = pred.predict("This is an example.")
        result.run_diagnostics() # One-stop function
        result.to_file("<PATH>")
        
    To use without the Predictor class:
    
    .. code-block:: python
    
        import poetic
    
        pred = [0.1, 0.2, 0.3]
        result = Diagnostics(predictions=pred)
        five_number_summary = result.five_number()
        
    All public methods can be used without the run_diagnostics() method, but
    they depend on the diagnostic attribute, which the run_diagnostic()
    method generates. 


"""


import numpy as np
import csv
from poetic.util import Info

from typing import Optional, List, Sequence, Union, Dict


class Diagnostics():
    """ Class for storing and processing prediction results.
    
    ``Diagnostics`` is the default base class of ``Predictions``, which is 
    generated by the ``Predictor`` class. It can also be used as a standalone 
    class for processing any numeric results and strings stored in lists.
    
    Args:
        predictions (list): Predictions of poetic scores.
        sentences (list, optional): Sentences associated with the predictions.

    Attributes:
        predictions (list): Predictions of poetic scores.
        sentences (list): Sentences associated with the predictions.
        diagnostics(dict): A dictionary of diagnostics statistics,
            including sentence count, five number summary, and the predictions themselves. 
    """

    def __init__(self, predictions: List[float], sentences: Optional[List[str]]=None) -> None:

        self.predictions = predictions
        self.sentences = sentences
        self.diagnostics = None


    def __str__(self) -> str:
        """ String representation for ``str()``. 
        
        This string representation returns a summary of of the object. It will truncate
        results if there are more than 15 prediction entries. To get a string representation
        of all the results in a dictionary cast into string, use repr() method.
        
        Returns:
            str: String representation of the object.    
        """
        
        general_message = "Diagnostics object for the following predictions: "
        predictions = str(self.predictions)
        if len(predictions) > 15:
            predictions = predictions[0:14] + "..."
        return general_message + predictions


    def __repr__(self) -> str:
        """ String representation for ``repr()``.
        
        This string representation returns a dictionary cast into a string with three
        attributes of this class: predictions, sentences, and diagnostics. 
        
        Returns:
            str: String representation of the object.
        """
        
        repr = {"Predictions": self.predictions, 
                "Sentences": self.sentences,
                "Diagnostics": self.diagnostics}
        return str(repr)


    def __len__(self) -> int:
        """ Method for ``len()``.

        Returns: 
            int: The length of the predictions attribute
        """
        return len(self.predictions)


    @classmethod
    def five_number(cls, input: Union["numpy.ndarray", List[float]]) -> Dict[str, float]:
        """Five number summary.

        This methods generates five number summary of a given input.
        The five number summary includes minimum, mean, median,
        standard deviation, and maximum. This is a class method.

        Parameters:
            input (numpy.ndarrau, list): An array like object.

        Returns:
            dict(str, float): A dictionary of five number results.
        """

        summary = {}
        summary["Min"] = np.min(input)
        summary["Mean"] = np.mean(input)
        summary["Median"] = np.median(input)
        summary["Stdev"] = np.std(input)
        summary["Max"] = np.max(input)

        return(summary)


    def run_diagnostics(self) -> None:
        """Run the diagnostics of the predictions.

        This methods generate diagnostics of the predictions,
        which include sentence count, five number summary, and
        the sentences themselves.

        """

        self.diagnostics = {}
        self.diagnostics["Sentence_count"] = len(self.predictions)
        self.diagnostics["Five_num"] = self.five_number(self.predictions)
        self.diagnostics["Predictions"] = self.predictions


    def to_file(self, path: str) -> None:
        """Saves diagnostics and predictions to a file.

        This methods saves the results to a csv or generates a
        diagnostics report along with the predictions. The supplied
        file path's file extension is used to determine which file
        to save. If a csv is explicitly desired, ``to_csv()`` method can
        be used. For all file extensions other than csv, a plain text
        report will be generated.

        Parameters:
            path (str): An string representing the file path.

        """


        # Check for csv
        path_len = len(path)
        if (path[(path_len-4):path_len]==".csv"):
            self.to_csv(path)
        else:
            # Plain Text report
            contents = self.generate_report()

            try:
                f = open(path, "w", encoding='utf-8')
                f.write(contents)
                f.close() # Close File

            except Exception as e:
                print(contents)
                print("Warning: Unable to open file at designated path.\n\n")
                raise e


    def to_csv(self, path: str) -> None:
        """Saves predictions and sentences to a csv file.

        This methods saves the results to a csv file. For a
        plain text diagnostics, please use the ``to_file()``
        method.

        Parameters:
            path (str): An string representing the file path.

        """
        try:
            with open(path, "w", encoding='utf-8', newline="") as file:
                writer = csv.writer(file)
                writer.writerow(["Sentence_num","Sentence", "Score"])
                # Loop through each prediction
                for i in range(0, len(self.predictions)):
                    if self.sentences is not None:
                        row = [i+1, self.sentences[i], self.predictions[i]]
                    else:
                        row = [i+1, "NA", self.predictions[i]]
                    # Write results
                    writer.writerow(row)

        except Exception as e:
            print("Warning: Unable to open file at designated path.\n\n")
            raise e


    def generate_report(self) -> str:
        """Generates the diagnostics report in string.

        This methods generates a diagnostics report as a string,
        with Poetic package information, five number summary, and
        all sentences and their poetic sores.

        Returns:
            str: A string with diagnostic report.
        """

        version = Info.version()

        # Program Information
        r = "Poetic\n"
        r += f"Version: {version}\n"
        r += 'For latest updates: www.github.com/kevin931/Poetic\n\n'
        # General Information
        r += "Diagnostics Report\n\n"
        r += "Model: Lexical Model\n"
        r += f"Number of Sentences: {self.diagnostics['Sentence_count']}\n\n"
        # Five Number Summary
        r += "~~~Five Number Summary~~~\n"
        r += f"Minimum: {self.diagnostics['Five_num']['Min']}\n"
        r += f"Mean: {self.diagnostics['Five_num']['Mean']}\n"
        r += f"Median: {self.diagnostics['Five_num']['Median']}\n"
        r += f"Maximum: {self.diagnostics['Five_num']['Max']}\n"
        r += f"Standard Deviation: {self.diagnostics['Five_num']['Stdev']}\n\n"
        # Score of each sentence
        r = r + "~~~All Scores~~~\n"
        for i in range(0, self.diagnostics["Sentence_count"]):
            r = r + f"Sentence #{i+1}: {self.predictions[i]}\n"

        return r