import math


def _circle_loss(labels,
                 scores,
                 rank_discount_form=None,
                 gamma=64.,
                 margin=0.25):
  """Returns the circle loss given the loss form.

  Args:
    labels: A list of graded relevance.
    scores: A list of item ranking scores.
    rank_discount_form: A string representing the form of rank discount.
    gamma: A float parameter used in circle loss.
    margin: A float parameter defining the margin in circle loss.

  Returns:
    A tuple of (sum of loss, sum of lambda weights, count of nonzero weights).
  """
  scores, labels = zip(*sorted(zip(scores, labels), reverse=True))

  def _rank_discount(rank_discount_form, rank):
    discount = {
        'LINEAR': 1. / rank,
        'LOG': 1. / math.log(1. + rank),
    }
    return discount[rank_discount_form]

  def _lambda_weight(label_diff, i, j):
    delta = math.fabs(label_diff)
    if rank_discount_form is not None:
      delta *= math.fabs(
          _rank_discount(rank_discount_form, i + 1) -
          _rank_discount(rank_discount_form, j + 1))
    else:
      delta = 1. if delta > 0 else 0
    return delta

  def _loss(si, sj, label_diff, delta):
    if label_diff <= 0:
      return 0.
    return math.exp(gamma * max(0., (1 + margin) - si) *
                    ((1 - margin) - si) + gamma * max(0., sj + margin) *
                    (sj - margin)) * delta

  loss = 0.
  weight = 0.
  count = 0.
  for i in range(len(labels)):
    for j in range(len(labels)):
      if labels[i] > labels[j]:
        delta = _lambda_weight(labels[i] - labels[j], i, j)
        part_loss = _loss(scores[i], scores[j], labels[i] - labels[j], delta)
        loss += part_loss
        weight += delta
        if weight > 0:
          count += 1.
  return loss, weight, count