# Copyright 2018 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.

import contextlib
import json
import os
import tempfile

from tracing.value import histogram_set
from tracing.value import merge_histograms
from tracing.value.diagnostics import generic_set
from tracing.value.diagnostics import reserved_infos


ALL_NAMES = list(reserved_infos.AllNames())


def _LoadHistogramSet(dicts):
  hs = histogram_set.HistogramSet()
  hs.ImportDicts(dicts)
  return hs


@contextlib.contextmanager
def TempFile():
  try:
    temp = tempfile.NamedTemporaryFile(delete=False)
    yield temp
  finally:
    os.unlink(temp.name)


def GetGroupingLabelFromHistogram(hist):
  tags = hist.diagnostics.get(reserved_infos.STORY_TAGS.name) or []
  tags_to_use = [t.split(':') for t in tags if ':' in t]
  return '_'.join(v for _, v in sorted(tags_to_use))


def ComputeTestPath(hist):
  path = hist.name

  # If a Histogram represents a summary across multiple stories, then its
  # 'stories' diagnostic will contain the names of all of the stories.
  # If a Histogram is not a summary, then its 'stories' diagnostic will contain
  # the singular name of its story.
  is_summary = list(
      hist.diagnostics.get(reserved_infos.SUMMARY_KEYS.name, []))

  grouping_label = GetGroupingLabelFromHistogram(hist)
  if grouping_label and (
      not is_summary or reserved_infos.STORY_TAGS.name in is_summary):
    path += '/' + grouping_label

  is_ref = hist.diagnostics.get(reserved_infos.IS_REFERENCE_BUILD.name)
  if is_ref and len(is_ref) == 1:
    is_ref = is_ref.GetOnlyElement()

  story_name = hist.diagnostics.get(reserved_infos.STORIES.name)
  if story_name and len(story_name) == 1 and not is_summary:
    escaped_story_name = story_name.GetOnlyElement()
    path += '/' + escaped_story_name
    if is_ref:
      path += '_ref'
  elif is_ref:
    path += '/ref'

  return path


def Dumps(obj):
  return json.dumps(obj, separators=(',', ':')).encode('utf-8')


def _MergeHistogramSetByPath(hs):
  with TempFile() as temp:
    temp.write(Dumps(hs.AsDicts()))
    temp.close()

    return merge_histograms.MergeHistograms(temp.name, (
        reserved_infos.TEST_PATH.name,))


def _GetAndDeleteHadFailures(hs):
  had_failures = False
  for h in hs:
    had_failures_diag = h.diagnostics.get(reserved_infos.HAD_FAILURES.name)
    if had_failures_diag:
      del h.diagnostics[reserved_infos.HAD_FAILURES.name]
      had_failures = True
  return had_failures


def _MergeAndReplaceSharedDiagnostics(diagnostic_name, hs):
  merged = None
  for h in hs:
    d = h.diagnostics.get(diagnostic_name)
    if not d:
      continue

    if not merged:
      merged = d
    else:
      merged.AddDiagnostic(d)
      h.diagnostics[diagnostic_name] = merged


def Batch(histograms, max_bytes, strict=False):
  all_json = Dumps(histograms.AsDicts())
  if max_bytes == 0:
    return [all_json]

  avg_hist_size = len(all_json) / len(histograms)
  del all_json
  avg_batch_size = max(1, int(round(max_bytes / avg_hist_size)))

  # Return an array of HistogramSet json strings, each not larger than
  # max_bytes. Take |avg_batch_size| histograms, then add or remove one at a
  # time until the batch_json size is just under max_bytes.

  histograms = list(histograms)

  def DumpsFirst(n):
    hs = histogram_set.HistogramSet(histograms[:n])
    if n > 1:
      hs.DeduplicateDiagnostics()
    return Dumps(hs.AsDicts())

  batches = []
  while histograms:
    batch_size = avg_batch_size
    batch_json = DumpsFirst(batch_size)
    while len(batch_json) < max_bytes and batch_size < len(histograms):
      batch_size += 1
      batch_json = DumpsFirst(batch_size)
    while len(batch_json) > max_bytes and batch_size > 1:
      batch_size -= 1
      batch_json = DumpsFirst(batch_size)
    if strict and len(batch_json) > max_bytes and batch_size == 1:
      raise ValueError('Found a single Histogram larger than max_bytes')
    histograms = histograms[batch_size:]
    batches.append(batch_json)

  return batches


def AddReservedDiagnostics(histogram_dicts, names_to_values, max_bytes=0):
  # We need to generate summary statistics for anything that had a story, so
  # filter out every histogram with no stories, then merge. If you keep the
  # histograms with no story, you end up with duplicates.
  hs_with_stories = _LoadHistogramSet(histogram_dicts)
  if len(hs_with_stories) == 0:
    return []

  hs_with_stories.FilterHistograms(
      lambda h: not h.diagnostics.get(reserved_infos.STORIES.name, []))

  hs_with_no_stories = _LoadHistogramSet(histogram_dicts)
  hs_with_no_stories.FilterHistograms(
      lambda h: h.diagnostics.get(reserved_infos.STORIES.name, []))

  # TODO(#3987): Refactor recipes to call merge_histograms separately.
  # This call combines all repetitions of a metric for a given story into a
  # single histogram.
  hs = histogram_set.HistogramSet()
  hs.ImportDicts(hs_with_stories.AsDicts())

  for h in hs:
    h.diagnostics[reserved_infos.TEST_PATH.name] = (
        generic_set.GenericSet([ComputeTestPath(h)]))

  _GetAndDeleteHadFailures(hs)
  dicts_across_repeats = _MergeHistogramSetByPath(hs)

  had_failures = _GetAndDeleteHadFailures(hs_with_stories)

  if not had_failures:
    # This call creates summary metrics across each tag set of stories.
    hs = histogram_set.HistogramSet()
    hs.ImportDicts(hs_with_stories.AsDicts())
    hs.FilterHistograms(lambda h: not GetGroupingLabelFromHistogram(h))

    for h in hs:
      h.diagnostics[reserved_infos.SUMMARY_KEYS.name] = (
          generic_set.GenericSet(['name', 'storyTags']))
      h.diagnostics[reserved_infos.TEST_PATH.name] = (
          generic_set.GenericSet([ComputeTestPath(h)]))

    dicts_across_stories = _MergeHistogramSetByPath(hs)

    # This call creates summary metrics across the entire story set.
    hs = histogram_set.HistogramSet()
    hs.ImportDicts(hs_with_stories.AsDicts())

    for h in hs:
      h.diagnostics[reserved_infos.SUMMARY_KEYS.name] = (
          generic_set.GenericSet(['name']))
      h.diagnostics[reserved_infos.TEST_PATH.name] = (
          generic_set.GenericSet([ComputeTestPath(h)]))

    dicts_across_names = _MergeHistogramSetByPath(hs)
  else:
    dicts_across_stories = []
    dicts_across_names = []

  # Now load everything into one histogram set. First we load the summary
  # histograms, since we need to mark them with SUMMARY_KEYS.
  # After that we load the rest, and then apply all the diagnostics specified
  # on the command line. Finally, since we end up with a lot of diagnostics
  # that no histograms refer to, we make sure to prune those.
  histograms = histogram_set.HistogramSet()
  histograms.ImportDicts(dicts_across_names)
  histograms.ImportDicts(dicts_across_stories)
  histograms.ImportDicts(dicts_across_repeats)
  histograms.ImportDicts(hs_with_no_stories.AsDicts())

  histograms.DeduplicateDiagnostics()
  for name, value in names_to_values.items():
    assert name in ALL_NAMES
    histograms.AddSharedDiagnosticToAllHistograms(
        name, generic_set.GenericSet([value]))
  histograms.RemoveOrphanedDiagnostics()

  if len(histograms) == 0:
    return []

  return Batch(histograms, max_bytes)
