import sys


from attr_utils import _memoize_attr


from action import Action


class HasActions:
  def add_actions(self, actions):
    if not hasattr(self, 'actions'):
      self.actions = []
    self.actions.extend(actions)
    self.actions = sorted(self.actions, key=lambda action: action.timestamp)

  def actions_in_continuous_rank_order(self):
    # FIXME: multiplying query order nr by 1M is just a silly hack
    return _memoize_attr(self, '_actions_in_continuous_rank_order',
                         lambda: sorted(self.actions,
                                        key=lambda action:
                                        action.query.order_number() * 1000000 + int(getattr(
                                            action, 'rank', sys.maxsize))))

  def actions_by_type(self, action_type):
    return self.actions_by_filter(lambda a: a.action_type == action_type)

  def actions_by_type_until(self, action_type, seconds):
    return _memoize_attr(
        self,
        '_actions_by_' + str(action_type) + '_type_until_' + str(seconds),
        lambda: self._calculate_actions_by_type_until(action_type, seconds)
    )

  def actions_by_type_before_rank(self, action_type, rank):
    return _memoize_attr(
        self,
        '_actions_by_' + str(action_type) + '_type_before_rank_' + str(rank),
        lambda: self._calculate_actions_by_type_before_rank(action_type, rank)
    )

  def actions_by_filter(self, filter_func=lambda a: False, plain_actions=False):
    return [action if plain_actions else (idx, action) for idx, action in enumerate(self.actions) if filter_func(action)]

  def _calculate_actions_by_type_until(self, action_type, seconds):
    return self._calculate_actions_by_filter_until(lambda a: a.action_type == action_type, seconds)

  def _calculate_actions_by_type_before_rank(self, action_type, rank):
    return self._calculate_actions_by_filter_before_rank(lambda a: a.action_type == action_type, rank)

  def _calculate_actions_by_filter_until(self, filter_func=lambda a: False, seconds=0):
    actions = []
    for idx, action in enumerate(self.actions):
      if self.seconds_elapsed_at(action.timestamp) > seconds:
        break
      if filter_func(action):
        actions.append((idx, action))
    return actions

  def _calculate_actions_by_filter_before_rank(self, filter_func=lambda a: False, rank=0):
    actions = []
    for idx, action in enumerate(self.actions_in_continuous_rank_order()):
      # NOTE: not all actions have a rank, so we need to skip actions with no rank
      if hasattr(action, 'rank') and action.query.continuous_rank_at(int(action.rank)) > rank:
        break
      if filter_func(action):
        actions.append((idx, action))
    return actions

  def actions_by_filter_before_action(self, filter_func, target_action):
    actions = []
    for idx, action in enumerate(self.actions):
      if action == target_action:
        break
      if filter_func(action):
        actions.append((idx, action))
    return actions

  def action_duration_in_seconds_for(self, idx, action, end_action_type=None):
    next_timestamp = None
    if end_action_type is None:
      try:
        next_timestamp = self.actions[idx + 1].timestamp
      except IndexError:
        # Can not calculate duration if the action is dead last
        return None
    else:
      for ii in range(idx + 1, len(self.actions) - 1, 1):
        next_action = self.actions[ii]
        if next_action.action_type == end_action_type:
          next_timestamp = next_action.timestamp
          break
    # Duration can not be calculated if a next action was not found
    if next_timestamp is None:
      return None
    current_timestamp = action.timestamp
    delta = next_timestamp - current_timestamp
    return delta.total_seconds()

  def duration_in_seconds(self):
    # No actions? No duration.
    if len(self.actions) == 0:
      return None
    first_timestamp = self.get_start_timestamp()
    last_timestamp = self.get_end_timestamp()
    delta = last_timestamp - first_timestamp
    return delta.total_seconds()

  def get_start_timestamp(self):
    # No actions?
    if len(self.actions) == 0:
      return None
    return _memoize_attr(
        self,
        '_start_timestamp',
        lambda: self.actions[0].timestamp
    )

  def get_end_timestamp(self):
    # No actions?
    if len(self.actions) == 0:
      return None
    return self.actions[-1].timestamp

  def serp_switch_actions(self):
    return self.actions_by_type(Action.SERP_SWITCH_ACTION_NAME)

  def document_read_actions(self):
    return self.actions_by_type(Action.READ_ACTION_NAME)

  def document_read_actions_until(self, seconds):
    return self.actions_by_type_until(Action.READ_ACTION_NAME, seconds)

  def document_read_actions_before_rank(self, rank):
    return self.actions_by_type_before_rank(Action.READ_ACTION_NAME, rank)

  def document_marked_relevant_actions(self):
    return self.actions_by_type(Action.MARK_ACTION_NAME)

  def document_marked_relevant_actions_until(self, seconds):
    return self.actions_by_type_until(Action.MARK_ACTION_NAME, seconds)

  def document_marked_relevant_actions_before_rank(self, rank):
    return self.actions_by_type_before_rank(Action.MARK_ACTION_NAME, rank)

  def document_read_times(self):
    read_times = {}
    for read_event in self.document_read_events():
      document_id = read_event['document'].record_id
      duration = read_event['read_duration']
      if document_id in read_times:
        read_times[document_id] += duration
      else:
        read_times[document_id] = duration
    return read_times

  def document_read_events(self):
    read_events = []
    for idx, action in self.document_read_actions():
      action_duration = self.action_duration_in_seconds_for(idx, action)
      if action_duration is None:
        # It's possible that the duration can not be calculated (for session time slices)
        continue
      document = action.document
      rank = action.query.rank_of(document)
      result = action.query.result_at(rank) if rank is not None else None
      read_events.append({
          'document': document,
          'result': result,
          'read_duration': action_duration,
          'read_start_at_seconds': self.seconds_elapsed_at(action.timestamp),
          'rank': rank,
          'query': action.query,
          'continuous_rank': action.query.continuous_rank_at(rank) if rank is not None else None,
          'query_order_number': action.query.order_number(),
          'document_incidence': action.incidence_of_document(),
          'read_incidence': action.read_incidence_of_document(),
          'read_incidence_unique': action.unique_read_incidence_of_document()
      })
    return read_events

  def document_marked_relevant_events(self):
    mark_events = []
    for idx, action in self.document_marked_relevant_actions():
      action_duration = self.action_duration_in_seconds_for(idx, action)
      if action_duration is None:
        # It's possible that the duration can not be calculated (for session time slices)
        continue
      document = action.document
      rank = action.query.rank_of(document)
      mark_events.append({
          'document': document,
          'mark_duration': action_duration,
          'mark_start_at_seconds': self.seconds_elapsed_at(action.timestamp),
          'rank': rank,
          'continuous_rank': action.query.continuous_rank_at(rank) if rank is not None else None,
          'query_order_number': action.query.order_number(),
          'document_incidence': action.incidence_of_document(),
          'read_incidence': action.read_incidence_of_document(),
          'mark_incidence': action.mark_incidence_of_document(),
          'read_incidence_unique': action.unique_read_incidence_of_document(),
          'mark_incidence_unique': action.unique_mark_incidence_of_document()
      })
    return mark_events

  def snippet_scan_events(self):
    scan_events = []
    for idx, action in self.snippet_scan_actions():
      action_duration = self.action_duration_in_seconds_for(idx, action)
      if action_duration is None:
        # It's possible that the duration can not be calculated (for session time slices)
        continue
      document = action.document
      read_events.append({
          'document': document,
          'read_duration': action_duration,
          'read_start_at_seconds': self.seconds_elapsed_at(action.timestamp)
      })
    return read_events

  def seconds_elapsed_at(self, timestamp):
    return _memoize_attr(
        self,
        '_seconds_elapsed_at_' + str(timestamp),
        lambda: (timestamp - self.get_start_timestamp()).total_seconds()
    )

  def average_snippet_scanning_time_in_seconds(self):
    if len(self.seen_documents) == 0:
      return None
    return self.total_snippet_scanning_time_in_seconds() / len(self.seen_documents)

  def document_read_start_at_seconds(self, document):
    for idx, action in self.document_read_actions():
      if hasattr(action, 'document_id') and action.document_id == document.record_id:
        return self.seconds_elapsed_at(action.timestamp)
    return None

  def document_mark_start_at_seconds(self, document):
    for idx, action in self.document_marked_relevant_actions():
      if hasattr(action, 'document_id') and action.document_id == document.record_id:
        return self.seconds_elapsed_at(action.timestamp)
    return None
