"""Data generation.

  channel_set descr = {
      name: str,
      start: InRange or Number,
      duration: InRange or Number,
      step: InRange or Number,
      count: int,
      fields: [Field, ]}

  InRange is {
      start: Number,
      stop: Number,
      opt step: Number,
      opt random: Bool}

  Field is {
      name: str,
      fun: callable or Value,
      opt init: Value}

"""

import random
import Queue

from attel.util import utils_base


_CurrentTimeMs = 0


def GetCurrentTimeMs(prev=None):
  return _CurrentTimeMs


def SetCurrentTimeMs(time_ms):
  global _CurrentTimeMs
  _CurrentTimeMs = time_ms


def _NextInRange(inrangeOrNum, prev):
  if not isinstance(inrangeOrNum, dict):
    return inrangeOrNum
  inrange = inrangeOrNum
  if inrange.get('random'):
    return GenerateRandomInRange(inrange)
  return GenerateNextInRange(inrange, prev)


def GetMaxInRange(inrangeOrNum):
  if not isinstance(inrangeOrNum, dict):
    return inrangeOrNum
  return inrangeOrNum.get('stop')


def CreateChannels(chIdBase, descr):
  """Creates data generation channels.

  Args:
    chIdBase: int, A channel id base value.
    descr: dict, A channel_set.
  Yields:
    list<dict>, channels.
  """
  for field_descr in descr['fields']:
    if 'init' not in field_descr and 'fun' not in field_descr:
      raise AttributeError('no init and fun in %s', descr['name'])

  start, duration = None, None
  for channel_index in xrange(descr.get('count', 1)):
    name = '%s_%i' % (descr['name'], channel_index)
    start = _NextInRange(descr['start'], start)
    duration = _NextInRange(descr['duration'], duration)
    channel = {
        'name': name,
        'chId': chIdBase + channel_index,
        'start': start,
        'stop': start + duration,
        'step': _NextInRange(descr.get('step', 1), None),
        'fields': descr['fields'],
    }
    yield channel


def GetNextTimeMs(channel):
  if 'prevPoint' not in channel:
    return channel['start']
  time_ms = channel['prevPoint']['time_ms'] + channel['step']
  return time_ms if time_ms < channel['stop'] else None


class ChannelRunner(object):
  __metaclass__ = utils_base.Singleton

  def __init__(self, sender=None):
    self.channelQueue = Queue.PriorityQueue()
    self.sender = sender or id
    self.chIdBase = 0

  def AddChannel(self, genDescr, callback=None):
    for channel in CreateChannels(self.chIdBase, genDescr):
      channel['callback'] = callback or id
      self.chIdBase += 1
      next_ms = GetNextTimeMs(channel)
      if next_ms is not None:
        self.channelQueue.put((next_ms, channel))

  def IsEmpty(self):
    return self.channelQueue.empty()

  def RunTill(self, limit_ms):
    try:
      while True:
        time_ms, channel = self.channelQueue.get_nowait()
        if time_ms > limit_ms:
          self.channelQueue.put((time_ms, channel))
          return
        point = GenerateChannelData(channel)
        channel['prevPoint'] = point
        self.sender(point)
        channel['callback'](point)
        next_ms = GetNextTimeMs(channel)
        if next_ms is not None:
          self.channelQueue.put((next_ms, channel))
    except Queue.Empty:
      pass


def GenerateChannelData(channel):
  point = GenerateDataPoint(channel, channel.get('prevPoint'))
  point['time_ms'] = GetCurrentTimeMs()
  point['chId'] = channel['chId']
  return point


def GenerateDataPoint(channel, prev=None):
  """Generates a data point given a metadata descriptor and a previous value.

  Args:
    channel: dict, A channel.
    prev: dict, A previous value.
  """
  result = {}
  for field_descr in channel['fields']:
    name = field_descr['name']
    if not prev and 'init' in field_descr:
      result[name] = field_descr['init']
      continue
    prev_value = prev.get(name) if prev else None
    result[name] = field_descr.get('fun', lambda prev: prev)(prev=prev_value)
  return result


def GenerateRandomInRange(range, prev=None):
  """Generates random value in range.

  Args:
    range: dict, A range descriptor:
      {start: Value, stop: Value, opt step: Value}
    prev: unused.
  Returns:
    int or float, Random value.
  """
  start, stop = range['start'], range['stop']
  if type(start) == float or type(stop) == float:
    return random.uniform(start, stop)
  if 'step' in range:
    return random.randrange(start, stop, range['step'])
  return random.randint(start, stop)


def GenerateNextInRange(range, prev=None):
  """Generates next value in range.

  Args:
    range: dict, A range descriptor:
      {start: Value, stop: Value, opt step: Value}
    prev: int or float or None, A previous value or None.
  Returns:
    int or float, Random value.
  """
  start = range['start']
  if prev is None:
    return start
  stop = range.get('stop')
  step = range.get('step', 1)
  if stop is None:
    return prev + step
  if type(start) == float or type(stop) == float:
    return start + (float(prev) - start + step) % (stop - start)
  return start + (prev - start + step) % (stop - start)


def GenerateRandomChoice(choices, prev=None):
  """Generates a random choice from a sequence.

  Args:
    choices: sequence.
  Returns:
    A value.
  """
  return random.choice(choices)


def GenerateNextChoice(choices, prev=None):
  """Generates the next choice from a sequence (round robin).

  Args:
    choices: sequence.
  Returns:
    A value.
  """
  if prev is None:
    return GenerateRandomChoice(choices, prev)
  return choices[(choices.index(prev) + 1) % len(choices)]
