# Base code is from https://github.com/cs230-stanford/cs230-code-examples
import json
import logging
import os
import shutil

import torch
import matplotlib.pyplot as plt


class Params():
    """Class that loads hyperparameters from a json file.
    Example:
    ```
    params = Params(json_path)
    print(params.learning_rate)
    params.learning_rate = 0.5  # change the value of learning_rate in params
    ```
    """

    def __init__(self, json_path):
        with open(json_path) as f:
            params = json.load(f)
            self.__dict__.update(params)

    def save(self, json_path):
        with open(json_path, 'w') as f:
            json.dump(self.__dict__, f, indent=4)

    def update(self, json_path):
        """Loads parameters from json file"""
        with open(json_path) as f:
            params = json.load(f)
            self.__dict__.update(params)

    @property
    def dict(self):
        """Gives dict-like access to Params instance by `params.dict['learning_rate']"""
        return self.__dict__


class RunningAverage():
    """A simple class that maintains the running average of a quantity
    
    Example:
    ```
    loss_avg = RunningAverage()
    loss_avg.update(2)
    loss_avg.update(4)
    loss_avg() = 3
    ```
    """

    def __init__(self):
        self.steps = 0
        self.total = 0

    def update(self, val):
        self.total += val
        self.steps += 1

    def __call__(self):
        return self.total / float(self.steps)


def set_logger(log_path):
    """Set the logger to log info in terminal and file `log_path`.
    In general, it is useful to have a logger so that every output to the terminal is saved
    in a permanent file. Here we save it to `model_dir/train.log`.
    Example:
    ```
    logging.info("Starting training...")
    ```
    Args:
        log_path: (string) where to log
    """
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)

    if not logger.handlers:
        # Logging to a file
        file_handler = logging.FileHandler(log_path)
        file_handler.setFormatter(
            logging.Formatter('%(asctime)s:%(levelname)s: %(message)s'))
        logger.addHandler(file_handler)

        # Logging to console NOTE
        # stream_handler = logging.StreamHandler()
        # stream_handler.setFormatter(logging.Formatter('%(message)s'))
        # logger.addHandler(stream_handler)


def save_dict_to_json(d, json_path):
    """Saves dict of floats in json file
    Args:
        d: (dict) of float-castable values (np.float, int, float, etc.)
        json_path: (string) path to json file
    """
    with open(json_path, 'w') as f:
        # We need to convert the values to float for json (it doesn't accept np.array, np.float, )
        d = {k: float(v) for k, v in d.items()}
        json.dump(d, f, indent=4)


def save_checkpoint(state, is_best, checkpoint):
    """Saves model and training parameters at checkpoint + 'last.pth.tar'. If is_best==True, also saves
    checkpoint + 'best.pth.tar'
    Args:
        state: (dict) contains model's state_dict, may contain other keys such as epoch, optimizer state_dict
        is_best: (bool) True if it is the best model seen till now
        checkpoint: (string) folder where parameters are to be saved
    """
    filepath = os.path.join(checkpoint, 'last.pth.tar')
    if not os.path.exists(checkpoint):
        print("Checkpoint Directory does not exist! Making directory {}".
              format(checkpoint))
        os.mkdir(checkpoint)
    else:
        # print("Checkpoint Directory exists! ")
        pass
    torch.save(state, filepath)
    if is_best:
        shutil.copyfile(filepath, os.path.join(checkpoint, 'best.pth.tar'))


def load_checkpoint(checkpoint, model, optimizer=None):
    """Loads model parameters (state_dict) from file_path. If optimizer is provided, loads state_dict of
    optimizer assuming it is present in checkpoint.
    Args:
        checkpoint: (string) filename which needs to be loaded
        model: (torch.nn.Module) model for which the parameters are loaded
        optimizer: (torch.optim) optional: resume optimizer from checkpoint
    """
    if not os.path.exists(checkpoint):
        raise ("File doesn't exist {}".format(checkpoint))
    checkpoint = torch.load(checkpoint)
    model.load_state_dict(checkpoint['state_dict'])
    model.task_lr = checkpoint['task_lr_dict']

    if optimizer:
        optimizer.load_state_dict(checkpoint['optim_dict'])

    return checkpoint


def plot_training_results(model_dir, plot_history):
    """
    Plot training results (procedure) during training.

    Args:
        plot_history: (dict) a dictionary containing historical values of what 
                      we want to plot
    """
    tr_losses = plot_history['train_loss']
    val_losses = plot_history['val_loss']
    te_losses = plot_history['test_loss']
    tr_accs = plot_history['train_acc']
    val_accs = plot_history['val_acc']
    te_accs = plot_history['test_acc']

    plt.figure(0)
    plt.plot(list(range(len(tr_losses))), tr_losses, label='train_loss')
    plt.plot(list(range(len(val_losses))), val_losses, label='val_loss')
    plt.plot(list(range(len(te_losses))), te_losses, label='test_loss')
    plt.title('Loss trend')
    plt.xlabel('episode')
    plt.ylabel('ce loss')
    plt.legend()
    plt.savefig(os.path.join(model_dir, 'loss_trend'), dpi=200)
    plt.clf()

    plt.figure(1)
    plt.plot(list(range(len(tr_accs))), tr_accs, label='train_acc')
    plt.plot(list(range(len(val_accs))), val_accs, label='val_acc')
    plt.plot(list(range(len(te_accs))), te_accs, label='test_acc')
    plt.title('Accuracy trend')
    plt.xlabel('episode')
    plt.ylabel('accuracy')
    plt.legend()
    plt.savefig(os.path.join(model_dir, 'accuracy_trend'), dpi=200)
    plt.clf()
