#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import gzip
import os
import sys
import re
import json
from StringIO import StringIO
from datetime import datetime
from datetime import timedelta
from urlparse import urlparse
from dateutil.parser import parse as dateparse

from google.appengine.ext import ndb

from telemetry.timeline import model as model_module
from telemetry.timeline import trace_data as trace_data_module
from telemetry.timeline import event as trace_event

from models import Project
from models import Action
from models import ActionDetail
from models import ActionDetailExtended
from models import Log

class TraceProcessor():

  __temp_actions = {}
  __js_blame = {}

  def log (self, project, trace_info, extended_info,
          status, records_imported=0):

    if (project == None):
      return

    log = Log(
      parent=project.key,
      filename=trace_info.filename,
      date=datetime.today(),
      status=status,
      records_imported=records_imported
    )
    log.put()

  def process (self, project, trace_string, trace_info, extended_info):

    try:
      if re.search('json$', trace_info.filename):

        # Re-encode to ISO-8859-1
        trace_string = trace_string.decode('UTF-8', 'ignore')
        trace_string = trace_string.encode('ISO-8859-1', 'ignore')

        trace_json = json.loads(trace_string)
      elif re.search('json.gz$', trace_info.filename):
        gzip_trace_string = gzip.GzipFile(
          fileobj=StringIO(trace_string)
        ).read()
        trace_json = json.loads(gzip_trace_string)
      else:
        self.log(project, trace_info, extended_info,
          'Error reading file: neither .json nor .json.gz')
        return
    except Exception, e:
      self.log(project, trace_info, extended_info,
        'JSON parse error')
      return

    try:
      parsed_data = trace_data_module.TraceData(trace_json)
      model = model_module.TimelineModel(parsed_data)
      processes = model.GetAllProcesses()
    except Exception, e:
      self.log(project, trace_info, extended_info,
        'Error processing the file.')
      return

    summarizable = []
    trace_json = None

    # If there is a process to filter by, use that. Otherwise
    # find all non-empty and non-tracing processes and append
    # them to the list.
    if (trace_info.process != None):
      summarizable = [
        x for x in processes
        if x.labels == trace_info.process]
    else:
      for p in processes:
        if (p.labels != None and
            p.labels != 'chrome://tracing' and
            p.labels != 'BackgroundPage'):
          summarizable.append(p)

    # There should only be one process to process...
    if len(summarizable) == 0:
      self.log(project, trace_info, extended_info, 'No process found')
    elif len(summarizable) > 1:
      self.log(project, trace_info, extended_info,
          'Multiple tabs (trace process) found.')
    else:
      return self.analyze_trace_and_append_actions(
        project,
        trace_info,
        summarizable.pop(),
        model.bounds,
        extended_info)

  def analyze_trace_and_append_actions (self, project, trace_info, process,
      bounds, extended_info):

    threads = self.get_threads(process)
    renderer_thread = self.get_thread_by_name(process, 'CrRendererMain')
    time_ranges = self.get_time_ranges(renderer_thread)
    labels = extended_info['labels']
    secret = extended_info['secret']
    status = ''
    records_imported = []

    if project == None:
      status = "No project found with secret %s" % secret
      return None

    # If a single label is provided...
    if (self.is_single_label(labels)):

      labels = self.wrap_labels_in_list_if_needed(labels)

      #...and the Action of that label is for a Load Action, the trace is
      # considered to match entirely to the Action. If the Action is not a
      # Load Action, then only the ranges will be matched.
      if (self.label_is_for_load_action(project, labels)):

        status = 'Single label (%s), label is for load action' % labels[0]

        # Ignore time ranges and reset to the whole window
        time_ranges = [trace_event.TimelineEvent(category='Load',
          name=labels[0],
          start=bounds.min,
          duration=(bounds.max - bounds.min))]

        records_imported = self.create_action_details_from_trace(project,
            labels, time_ranges, threads, trace_info, extended_info)

      # If the Action of that label is not a Load Action, then look for
      # time ranges of that label.
      else:

        status = 'Single label (%s), label is not for a Load Action' % labels[0]
        records_imported = self.create_action_details_from_trace(project,
            labels, time_ranges, threads, trace_info, extended_info)

    # If multiple labels are provided and the trace contains ranges,
    # those ranges will be mapped to existing Actions in the Project
    # with those labels.
    elif (self.is_multi_label(labels) and len(time_ranges) > 0):

      status = 'Multiple labels, trace contains ranges'
      records_imported = self.create_action_details_from_trace(project,
          labels, time_ranges, threads, trace_info, extended_info)

    # If multiple labels are provided and the trace does not contain ranges,
    # no Actions will be findable, so the import will be a no-op.
    elif (self.is_multi_label(labels) and len(time_ranges) == 0):

      # If, however, only one of the labels provided is for a Load Action,
      # then the trace will be assumed to match to that Action.
      action = self.get_single_load_action_from_multi_label(project, labels)

      # Only go ahead if we found a single Load Action
      if (action != None):

        status = ('Multiple labels, trace contains no ranges. '
                  'Single Load Action label found.')

        # Ignore time ranges and reset to the whole window
        time_ranges = [trace_event.TimelineEvent(category=action.name,
          name=action.label,
          start=bounds.min,
          duration=(bounds.max - bounds.min))]

        records_imported = self.create_action_details_from_trace(project,
            [action.name], time_ranges, threads, trace_info, extended_info)

    # If no labels are provided..
    elif (len(labels) == 0):

      # ...and no ranges exist, then a match will be assumed if and
      # only if there is one Load Action in the Project.
      if (len(time_ranges) == 0):

        action = self.get_single_load_action_from_project(project)

        if (action != None):

          status = ('No labels, trace contains no ranges. '