#
# OtterTune - analysis/gpr_models.py
#
# Copyright (c) 2017-18, Carnegie Mellon University Database Group
#
# Author: Dana Van Aken

import copy
import json
import os

import gpflow
import numpy as np
import tensorflow as tf

from .gprc import GPRC


class BaseModel(object):

    # Min/max bounds for the kernel lengthscales
    _LENGTHSCALE_BOUNDS = (0.1, 10.)

    # Keys for each kernel's hyperparameters
    _KERNEL_HP_KEYS = []

    # The key for the likelihood parameter
    _LIKELIHOOD_HP_KEY = 'GPRC/likelihood/variance'

    def __init__(self, X, y, hyperparameters=None, optimize_hyperparameters=False,
                 learning_rate=0.001, maxiter=5000, **kwargs):
        # Store model kwargs
        self._model_kwargs = {
            'hyperparameters': hyperparameters,
            'optimize_hyperparameters': optimize_hyperparameters,
            'learning_rate': learning_rate,
            'maxiter': maxiter,
        }

        # Store kernel kwargs
        kernel_kwargs = self._get_kernel_kwargs(X_dim=X.shape[1], **kwargs)
        if hyperparameters is not None:
            self._assign_kernel_hyperparams(hyperparameters, kernel_kwargs)
        self._kernel_kwargs = copy.deepcopy(kernel_kwargs)

        # Build the kernels and the model
        with gpflow.defer_build():
            k = self._build_kernel(kernel_kwargs, optimize_hyperparameters=optimize_hyperparameters, **kwargs)
            m = GPRC(X, y, kern=k)
            if hyperparameters is not None and self._LIKELIHOOD_HP_KEY in hyperparameters:
                m.likelihood.variance = hyperparameters[self._LIKELIHOOD_HP_KEY]
        m.compile()

        # If enabled, optimize the hyperparameters
        if optimize_hyperparameters:
            opt = gpflow.train.AdamOptimizer(learning_rate)
            opt.minimize(m, maxiter=maxiter)
        self.model = m

    def _get_kernel_kwargs(self, **kwargs):
        return []

    def _build_kernel(self, kernel_kwargs, **kwargs):
        return None

    def get_hyperparameters(self):
        return {k: float(v) if v.ndim == 0 else v.tolist()
                for k, v in self.model.read_values().items()}

    def get_model_parameters(self):
        return {
            'model_params': copy.deepcopy(self._model_kwargs),
            'kernel_params': copy.deepcopy(self._kernel_kwargs)
        }

    def _assign_kernel_hyperparams(self, hyperparams, kernel_kwargs):
        for i, kernel_keys in enumerate(self._KERNEL_HP_KEYS):
            for key in kernel_keys:
                if key in hyperparams:
                    argname = key.rsplit('/', 1)[-1]
                    kernel_kwargs[i][argname] = hyperparams[key]

    @staticmethod
    def load_hyperparameters(path, hp_idx=0):
        with open(path, 'r') as f:
            hyperparams = json.load(f)['hyperparameters']
        if isinstance(hyperparams, list):
            assert hp_idx >= 0, 'hp_idx: {} (expected >= 0)'.format(hp_idx)
            if hp_idx >= len(hyperparams):
                hp_idx = -1
            hyperparams = hyperparams[hp_idx]
        return hyperparams


class BasicGP(BaseModel):

    _KERNEL_HP_KEYS = [
        [
            'GPRC/kern/kernels/0/variance',
            'GPRC/kern/kernels/0/lengthscales',
        ]
    ]

    def _get_kernel_kwargs(self, **kwargs):
        X_dim = kwargs.pop('X_dim')
        return [
            {
                'input_dim': X_dim,
                'ARD': False
            }
        ]

    def _build_kernel(self, kernel_kwargs, **kwargs):
        ls = 2
        var = 1
        if kwargs.get('lengthscales') is not None:
            ls = kwargs['lengthscales']
        if kwargs.get('variance') is not None:
            var = kwargs['variance']
        k = gpflow.kernels.Matern12(variance=var, lengthscales=ls, **kernel_kwargs[0])
        if kwargs.pop('optimize_hyperparameters'):
            k.lengthscales.transform = gpflow.transforms.Logistic(
                *self._LENGTHSCALE_BOUNDS)
        return k


class ExpWhiteGP(BaseModel):

    _KERNEL_HP_KEYS = [
        [
            'GPRC/kern/kernels/0/variance',
            'GPRC/kern/kernels/0/lengthscales',
        ],
        [
            'GPRC/kern/kernels/1/variance',
        ],
    ]

    def _get_kernel_kwargs(self, **kwargs):
        X_dim = kwargs.pop('X_dim')
        return [
            {
                'input_dim': X_dim,
                'ARD': False
            },
            {
                'input_dim': X_dim,
            },
        ]

    def _build_kernel(self, kernel_kwargs, **kwargs):
        k0 = gpflow.kernels.Exponential(**kernel_kwargs[0])
        k1 = gpflow.kernels.White(**kernel_kwargs[1])
        if kwargs.pop('optimize_hyperparameters'):
            k0.lengthscales.transform = gpflow.transforms.Logistic(
                *self._LENGTHSCALE_BOUNDS)
        k = k0 + k1
        return k


_MODEL_MAP = {
    'BasicGP': BasicGP,
    'ExpWhiteGP': ExpWhiteGP,
}


def create_model(model_name, **kwargs):
    # Update tensorflow session settings to enable GPU sharing
    gpflow.settings.session.update(gpu_options=tf.GPUOptions(allow_growth=True))
    check_valid(model_name)
    return _MODEL_MAP[model_name](**kwargs)


def check_valid(model_name):
    if model_name not in _MODEL_MAP:
        raise ValueError('Invalid GPR model name: {}'.format(model_name))
