import numpy
import math
import os

from .._library import BoundingBox


available_sources = {
  'direct'        : ('topleft', 'bottomright'),
  'eyes'          : ('leye', 'reye'),
  'left-profile'  : ('eye', 'mouth'),
  'right-profile' : ('eye', 'mouth'),
  'ellipse'       : ('center', 'angle', 'axis_radius')
}

# This struct specifies, which paddings should be applied to which source.
# All values are relative to the inter-node distance
default_paddings = {
  'direct'        : None,
  'eyes'          : {'left' : -1.0, 'right' : +1.0, 'top': -0.7, 'bottom' : 1.7}, # These parameters are used to match Cosmin's implementation (which was buggy...)
  'left-profile'  : {'left' : -0.2, 'right' : +0.8, 'top': -1.0, 'bottom' : 1.0},
  'right-profile' : {'left' : -0.8, 'right' : +0.2, 'top': -1.0, 'bottom' : 1.0},
  'ellipse'       : None
}


def bounding_box_from_annotation(source=None, padding=None, **kwargs):
  """Creates a bounding box from the given parameters.
  If 'sources' are specified, the according keywords (see available_sources) must be given as well.
  Otherwise, the source is estimated from the given keyword parameters if possible.

  If 'topleft' and 'bottomright' are given (i.e., the 'direct' source), they are taken as is.
  Note that the 'bottomright' is NOT included in the bounding box.

  For source 'ellipse', the bounding box is computed to capture the whole ellipse, even if it is rotated.

  For other sources (i.e., 'eyes'), the center of the two given positions is computed, and the 'padding' is applied.
  If 'padding ' is None (the default) then the default_paddings of this source are used instead.
  """

  if source is None:
    # try to estimate the source
    for s,k in available_sources.items():
      # check if the according keyword arguments are given
      if k[0] in kwargs and k[1] in kwargs:
        # check if we already assigned a source before
        if source is not None:
          raise ValueError("The given list of keywords (%s) is ambiguous. Please specify a source" % kwargs)
        # assign source
        source = s

    # check if a source could be estimated from the keywords
    if source is None:
      raise ValueError("The given list of keywords (%s) could not be interpreted" % kwargs)

  assert source in available_sources

  # use default padding if not specified
  if padding is None:
    padding = default_paddings[source]

  keys = available_sources[source]
  if source == 'ellipse':
    # compute the tight bounding box for the ellipse
    angle = kwargs['angle']
    axis = kwargs['axis_radius']
    center = kwargs['center']
    dx = abs(math.cos(angle) * axis[0]) + abs(math.sin(angle) * axis[1])
    dy = abs(math.sin(angle) * axis[0]) + abs(math.cos(angle) * axis[1])
    top = center[0] - dy
    bottom = center[0] + dy
    left = center[1] - dx
    right = center[1] + dx
  elif padding is None:
    # There is no padding to be applied -> take nodes as they are
    top    = kwargs[keys[0]][0]
    bottom = kwargs[keys[1]][0]
    left   = kwargs[keys[0]][1]
    right  = kwargs[keys[1]][1]
  else:
    # apply padding
    pos_0 = kwargs[keys[0]]
    pos_1 = kwargs[keys[1]]
    tb_center = float(pos_0[0] + pos_1[0]) / 2.
    lr_center = float(pos_0[1] + pos_1[1]) / 2.
    distance = math.sqrt((pos_0[0] - pos_1[0])**2 + (pos_0[1] - pos_1[1])**2)

    top    = tb_center + padding['top'] * distance
    bottom = tb_center + padding['bottom'] * distance
    left   = lr_center + padding['left'] * distance
    right  = lr_center + padding['right'] * distance

  return BoundingBox((top, left), (bottom - top, right - left))


def expected_eye_positions(bounding_box):
  """Computes the expected eye positions based on the relative coordinates of the bounding box."""
  top, left, right = default_paddings['eyes']['top'], default_paddings['eyes']['left'], default_paddings['eyes']['right']
  inter_eye_distance = (bounding_box.size[1]) / (right - left)
  return {
    'reye':(bounding_box.top_f - top*inter_eye_distance, bounding_box.left_f - left/2.*inter_eye_distance),
    'leye':(bounding_box.top_f - top*inter_eye_distance, bounding_box.right_f - right/2.*inter_eye_distance)
  }



def parallel_part(data, parallel):
  """Splits off samples from the the given data list and the given number of parallel jobs based on the SGE_TASK_ID."""
  if parallel is None or "SGE_TASK_ID" not in os.environ:
    return data

  data_per_job = int(math.ceil(float(len(data)) / float(parallel)))
  task_id = int(os.environ['SGE_TASK_ID'])
  first = (task_id-1) * data_per_job
  last = min(len(data), task_id * data_per_job)
  return data[first:last]



def quasi_random_indices(number_of_total_items, number_of_desired_items = None):
  """Returns a quasi-random list of indices that will contain exactly the number of desired indices (or the number of total items in the list, if this is smaller)."""
  # check if we need to compute a sublist at all
  if number_of_desired_items is None or number_of_desired_items >= number_of_total_items or number_of_desired_items < 0:
    return range(number_of_total_items)

  increase = float(number_of_total_items)/float(number_of_desired_items)
  # generate a regular quasi-random index list
  return [int((i +.5)*increase) for i in range(number_of_desired_items)]
