# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
'''A container for timeline-based events and traces and can handle importing
raw event data from different sources. This model closely resembles that in the
trace_viewer project:
https://code.google.com/p/trace-viewer/
'''

from operator import attrgetter

from telemetry.timeline import async_slice as async_slice_module
from telemetry.timeline import bounds
from telemetry.timeline import event_container
from telemetry.timeline import inspector_importer
from telemetry.timeline import slice as slice_module
from telemetry.timeline import process as process_module
from telemetry.timeline import surface_flinger_importer
from telemetry.timeline import tab_id_importer
from telemetry.timeline import trace_data as trace_data_module
from telemetry.timeline import trace_event_importer

# Register importers for data

_IMPORTERS = [
    inspector_importer.InspectorTimelineImporter,
    tab_id_importer.TabIdImporter,
    trace_event_importer.TraceEventTimelineImporter,
    surface_flinger_importer.SurfaceFlingerTimelineImporter
]


class MarkerMismatchError(Exception):
  def __init__(self):
    super(MarkerMismatchError, self).__init__(
        'Number or order of timeline markers does not match provided labels')


class MarkerOverlapError(Exception):
  def __init__(self):
    super(MarkerOverlapError, self).__init__(
        'Overlapping timeline markers found')


def IsSliceOrAsyncSlice(t):
  if t == async_slice_module.AsyncSlice:
    return True
  return t == slice_module.Slice


class TimelineModel(event_container.TimelineEventContainer):
  def __init__(self, trace_data=None, shift_world_to_zero=True):
    """ Initializes a TimelineModel.

    Args:
        trace_data: trace_data.TraceData containing events to import
        shift_world_to_zero: If true, the events will be shifted such that the
            first event starts at time 0.
    """
    super(TimelineModel, self).__init__(name='TimelineModel', parent=None)
    self._bounds = bounds.Bounds()
    self._thread_time_bounds = {}
    self._processes = {}
    self._browser_process = None
    self._gpu_process = None
    self._surface_flinger_process = None
    self._frozen = False
    self._tab_ids_to_renderer_threads_map = {}
    self.import_errors = []
    self.metadata = []
    self.flow_events = []
    if trace_data is not None:
      self.ImportTraces(trace_data, shift_world_to_zero=shift_world_to_zero)

  def IterChildContainers(self):
    for process in self._processes.itervalues():
      yield process

  def GetAllProcesses(self):
    return self._processes.values()

  def GetAllThreads(self):
    threads = []
    for process in self._processes.values():
      threads.extend(process.threads.values())
    return threads

  @property
  def bounds(self):
    return self._bounds

  @property
  def processes(self):
    return self._processes

  @property
  #pylint: disable=E0202
  def browser_process(self):
    return self._browser_process

  @browser_process.setter
  #pylint: disable=E0202
  def browser_process(self, browser_process):
    self._browser_process = browser_process

  @property
  #pylint: disable=E0202
  def gpu_process(self):
    return self._gpu_process

  @gpu_process.setter
  #pylint: disable=E0202
  def gpu_process(self, gpu_process):
    self._gpu_process = gpu_process

  @property
  #pylint: disable=E0202
  def surface_flinger_process(self):
    return self._surface_flinger_process

  @surface_flinger_process.setter
  #pylint: disable=E0202
  def surface_flinger_process(self, surface_flinger_process):
    self._surface_flinger_process = surface_flinger_process

  def AddMappingFromTabIdToRendererThread(self, tab_id, renderer_thread):
    if self._frozen:
      raise Exception('Cannot add mapping from tab id to renderer thread once '
                      'trace is imported')
    self._tab_ids_to_renderer_threads_map[tab_id] = renderer_thread

  def ImportTraces(self, trace_data, shift_world_to_zero=True):
    """Populates the model with the provided trace data.

    trace_data must be an instance of TraceData.

    Passing shift_world_to_zero=True causes the events to be shifted such that
    the first event starts at time 0.
    """
    if self._frozen:
      raise Exception("Cannot add events once trace is imported")
    assert isinstance(trace_data, trace_data_module.TraceData)

    importers = self._CreateImporters(trace_data)

    for importer in importers:
      # TODO: catch exceptions here and add it to error list
      importer.ImportEvents()
    for record in trace_data.metadata_records:
      self.metadata.append(record)
    self.FinalizeImport(shift_world_to_zero, importers)

  def FinalizeImport(self, shift_world_to_zero=False, importers=None):
    if importers == None:
      importers = []
    self.UpdateBounds()
    if not self.bounds.is_empty:
      for process in self._processes.itervalues():
        process.AutoCloseOpenSlices(self.bounds.max,
                                    self._thread_time_bounds)

    for importer in importers:
      importer.FinalizeImport()

    for process in self.processes.itervalues():
      process.FinalizeImport()

    if shift_world_to_zero:
      self.ShiftWorldToZero()
    self.UpdateBounds()

    # Because of FinalizeImport, it would probably be a good idea
    # to prevent the timeline from from being modified.
    self._frozen = True

  def ShiftWorldToZero(self):
    self.UpdateBounds()
    if self._bounds.is_empty:
      return
    shift_amount = self._bounds.min
    for event in self.IterAllEvents():
      event.start -= shift_amount

  def UpdateBounds(self):
    self._bounds.Reset()
    for event in self.IterAllEvents():
      self._bounds.AddValue(event.start)
      self._bounds.AddValue(event.end)

    self._thread_time_bounds = {}
    for thread in self.GetAllThreads():
      self._thread_time_bounds[thread] = bounds.Bounds()
      for event in thread.IterEventsInThisContainer(
          event_type_predicate=lambda t: True,
          event_predicate=lambda e: True):
        if event.thread_start != None:
          self._thread_time_bounds[thread].AddValue(event.thread_start)
        if event.thread_end != None:
          self._thread_time_bounds[thread].AddValue(event.thread_end)

  def GetOrCreateProcess(self, pid):
    if pid not in self._processes:
      assert not self._frozen
      self._processes[pid] = process_module.Process(self, pid)
    return self._processes[pid]

  def FindTimelineMarkers(self, timeline_marker_names):
    """Find the timeline events with the given names.

    If the number and order of events found does not match the names,
    raise an error.
    """
    # Make sure names are in a list and remove all None names
    if not isinstance(timeline_marker_names, list):
      timeline_marker_names = [timeline_marker_names]
    names = [x for x in timeline_marker_names if x is not None]

    # Gather all events that match the names and sort them.
    events = []
    name_set = set()
    for name in names:
      name_set.add(name)

    def IsEventNeeded(event):
      if event.parent_slice != None:
        return
      return event.name in name_set

    events = list(self.IterAllEvents(
      recursive=True,
      event_type_predicate=IsSliceOrAsyncSlice,
      event_predicate=IsEventNeeded))
    events.sort(key=attrgetter('start'))

    # Check if the number and order of events matches the provided names,
    # and that the events don't overlap.
    if len(events) != len(names):
      raise MarkerMismatchError()
    for (i, event) in enumerate(events):
      if event.name != names[i]:
        raise MarkerMismatchError()
    for i in xrange(0, len(events)):
      for j in xrange(i+1, len(events)):
        if events[j].start < events[i].start + events[i].duration:
          raise MarkerOverlapError()

    return events

  def GetRendererProcessFromTabId(self, tab_id):
    renderer_thread = self.GetRendererThreadFromTabId(tab_id)
    if renderer_thread:
      return renderer_thread.parent
    return None

  def GetRendererThreadFromTabId(self, tab_id):
    return self._tab_ids_to_renderer_threads_map.get(tab_id, None)

  def _CreateImporters(self, trace_data):
    def FindImporterClassForPart(part):
      for importer_class in _IMPORTERS:
        if importer_class.GetSupportedPart() == part:
          return importer_class

    importers = []
    for part in trace_data.active_parts:
      importer_class = FindImporterClassForPart(part)
      if not importer_class:
        raise Exception('No importer found for %s' % repr(part))
      importers.append(importer_class(self, trace_data))

      importers.sort(key=lambda k: k.import_order)

    return importers