# Copyright 2018 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================

"""Miscellaneous utility functions."""

import numpy as np
from sklearn.metrics import average_precision_score
import torch
import torch.nn.functional as F
import torch.utils.data


def to_numpy(array):
  if isinstance(array, np.ndarray):
    return array
  if isinstance(array, torch.autograd.Variable):
    array = array.data
  if array.is_cuda:
    array = array.cpu()

  return array.numpy()


def squeeze(array):
  if not isinstance(array, list) or len(array) > 1:
    return array
  else:  # len(array) == 1:
    return array[0]


def unsqueeze(array):
  if isinstance(array, list):
    return array
  else:
    return [array]


def is_due(*args):
  """Determines whether to perform an action or not, depending on the epoch.
     Used for logging, saving, learning rate decay, etc.

  Args:
    *args: epoch, due_at (due at epoch due_at) epoch, num_epochs,
          due_every (due every due_every epochs)
          step, due_every (due every due_every steps)
  Returns:
    due: boolean: perform action or not
  """
  if len(args) == 2 and isinstance(args[1], list):
    epoch, due_at = args
    due = epoch in due_at
  elif len(args) == 3:
    epoch, num_epochs, due_every = args
    due = (due_every >= 0) and (epoch % due_every == 0 or epoch == num_epochs)
  else:
    step, due_every = args
    due = (due_every > 0) and (step % due_every == 0)

  return due


def softmax(w, t=1.0, axis=None):
  w = np.array(w) / t
  e = np.exp(w - np.amax(w, axis=axis, keepdims=True))
  dist = e / np.sum(e, axis=axis, keepdims=True)
  return dist


def distance_metric(student, teacher, option, weights=None):
  """Distance metric to calculate the imitation loss.

  Args:
    student: batch_size x n_classes
    teacher: batch_size x n_classes
    option: one of [cosine, l2, l2, kl]
    weights: batch_size or float

  Returns:
    The computed distance metric.
  """
  if option == 'cosine':
    dists = 1 - F.cosine_similarity(student, teacher.detach(), dim=1)
  elif option == 'l2':
    dists = (student-teacher.detach()).pow(2).sum(1)
  elif option == 'l1':
    dists = torch.abs(student-teacher.detach()).sum(1)
  elif option == 'kl':
    assert weights is None
    T = 8
    # averaged for each minibatch
    dist = F.kl_div(
        F.log_softmax(student / T), F.softmax(teacher.detach() / T)) * (
            T * T)
    return dist
  else:
    raise NotImplementedError

  if weights is None:
    dist = dists.mean()
  else:
    dist = (dists * weights).mean()

  return dist


def get_segments(input, timestep):
  """Split entire input into segments of length timestep.

  Args:
    input: 1 x total_length x n_frames x ...
    timestep: the timestamp.

  Returns:
    input: concatenated video segments
    start_indices: indices of the segments
  """
  assert input.size(0) == 1, 'Test time, batch_size must be 1'

  input.squeeze_(dim=0)
  # Find overlapping segments
  length = input.size()[0]
  step = timestep // 2
  num_segments = (length - timestep) // step + 1
  start_indices = (np.arange(num_segments) * step).tolist()
  if length % step > 0:
    start_indices.append(length - timestep)

  # Get the segments
  segments = []
  for s in start_indices:
    segment = input[s: (s + timestep)].unsqueeze(0)
    segments.append(segment)
  input = torch.cat(segments, dim=0)
  return input, start_indices

def get_stats(logit, label):
  '''
  Calculate the accuracy.
  '''
  logit = to_numpy(logit)
  label = to_numpy(label)

  pred = np.argmax(logit, 1)
  acc = np.sum(pred == label)/label.shape[0]

  return acc, pred, label


def get_stats_detection(logit, label, n_classes=52):
  '''
  Calculate the accuracy and average precisions.
  '''
  logit = to_numpy(logit)
  label = to_numpy(label)
  scores = softmax(logit, axis=1)

  pred = np.argmax(logit, 1)
  length = label.shape[0]
  acc = np.sum(pred == label)/length

  keep_bg = label == 0
  acc_bg = np.sum(pred[keep_bg] == label[keep_bg])/label[keep_bg].shape[0]
  ratio_bg = np.sum(keep_bg)/length

  keep_action = label != 0
  acc_action = np.sum(
      pred[keep_action] == label[keep_action]) / label[keep_action].shape[0]

  # Average precision
  y_true = np.zeros((len(label), n_classes))
  y_true[np.arange(len(label)), label] = 1
  acc = np.sum(pred == label)/label.shape[0]
  aps = average_precision_score(y_true, scores, average=None)
  aps = list(filter(lambda x: not np.isnan(x), aps))
  ap = np.mean(aps)

  return ap, acc, acc_bg, acc_action, ratio_bg, pred, label


def info(text):
  print('\033[94m' + text + '\033[0m')


def warn(text):
  print('\033[93m' + text + '\033[0m')


def err(text):
  print('\033[91m' + text + '\033[0m')
