#!/usr/bin/env python3
#
# Copyright 2013 The Flutter 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 argparse
import csv
import json
import sys
import matplotlib.pyplot as plt  # pylint: disable=import-error
from matplotlib.backends.backend_pdf import PdfPages as pdfp  # pylint: disable=import-error


class BenchmarkResult:  # pylint: disable=too-many-instance-attributes

  def __init__(self, name, backend, time_unit, draw_call_count):
    self.name = name
    self.series = {}
    self.series_labels = {}
    self.backend = backend
    self.large_y_values = False
    self.y_limit = 200
    self.time_unit = time_unit
    self.draw_call_count = draw_call_count
    self.optional_values = {}

  def __repr__(self):
    return 'Name: % s\nBackend: % s\nSeries: % s\nSeriesLabels: % s\n' % (
        self.name, self.backend, self.series, self.series_labels
    )

  def add_data_point(self, family, xval, yval):
    if family not in self.series:
      self.series[family] = {'x': [], 'y': []}

    self.series[family]['x'].append(xval)
    self.series[family]['y'].append(yval)

    if yval > self.y_limit:
      self.large_y_values = True

  def add_optional_value(self, name, xval, yval):
    if name not in self.optional_values:
      self.optional_values[name] = {}

    self.optional_values[name][xval] = yval

  def set_family_label(self, family, label):
    # I'm not keying the main series dict off the family label
    # just in case we get data where the two aren't a 1:1 mapping
    if family in self.series_labels:
      assert self.series_labels[family] == label
      return

    self.series_labels[family] = label

  def plot(self):
    figures = []
    figures.append(plt.figure(dpi=1200, frameon=False, figsize=(11, 8.5)))

    for family in self.series:
      plt.plot(
          self.series[family]['x'],
          self.series[family]['y'],
          label=self.series_labels[family]
      )

    plt.xlabel('Benchmark Seed')
    plt.ylabel('Time (' + self.time_unit + ')')

    title = ''
    # Crop the Y axis so that we can see what's going on at the lower end
    if self.large_y_values:
      plt.ylim((0, self.y_limit))
      title = self.name + ' ' + self.backend + ' (Cropped)'
    else:
      title = self.name + ' ' + self.backend

    if self.draw_call_count != -1:
      title += '\nDraw Call Count: ' + str(int(self.draw_call_count))

    plt.title(title)

    plt.grid(which='both', axis='both')

    plt.legend(fontsize='xx-small')
    plt.plot()

    if self.large_y_values:
      # Plot again but with the full Y axis visible
      figures.append(plt.figure(dpi=1200, frameon=False, figsize=(11, 8.5)))
      for family in self.series:
        plt.plot(
            self.series[family]['x'],
            self.series[family]['y'],
            label=self.series_labels[family]
        )

      plt.xlabel('Benchmark Seed')
      plt.ylabel('Time (' + self.time_unit + ')')
      title = self.name + ' ' + self.backend + ' (Complete)'

      if self.draw_call_count != -1:
        title += '\nDraw Call Count: ' + str(int(self.draw_call_count))

      plt.title(title)

      plt.grid(which='both', axis='both')

      plt.legend(fontsize='xx-small')
      plt.plot()

    return figures

  def write_csv(self, writer):
    # For now assume that all our series have the same x values
    # this is true for now, but may differ in the future with benchmark changes
    x_values = []
    y_values = []
    for family in self.series:
      x_values = ['x'] + self.series[family]['x']
      y_values.append([self.series_labels[family]] + self.series[family]['y'])

    for name in self.optional_values:
      column = [name]
      for key in self.optional_values[name]:
        column.append(self.optional_values[name][key])
      y_values.append(column)

    writer.writerow([self.name, self.draw_call_count])
    for line, _ in enumerate(x_values):
      row = [x_values[line]]
      for series, _ in enumerate(y_values):
        row.append(y_values[series][line])
      writer.writerow(row)


def main():
  parser = argparse.ArgumentParser()

  parser.add_argument(
      'filename',
      action='store',
      help='Path to the JSON output from Google Benchmark'
  )
  parser.add_argument(
      '-o',
      '--output-pdf',
      dest='output_pdf',
      action='store',
      default='output.pdf',
      help='Filename to output the PDF of graphs to.'
  )
  parser.add_argument(
      '-c',
      '--output-csv',
      dest='output_csv',
      action='store',
      default='output.csv',
      help='Filename to output the CSV data to.'
  )

  args = parser.parse_args()
  json_data = parse_json(args.filename)
  return process_benchmark_data(json_data, args.output_pdf, args.output_csv)


def error(message):
  print(message)
  sys.exit(1)


def extrac_attributes_label(benchmark_result):
  # Possible attribute keys are:
  #  AntiAliasing
  #  HairlineStroke
  #  StrokedStyle
  #  FilledStyle
  attributes = ['AntiAliasing', 'HairlineStroke', 'StrokedStyle', 'FilledStyle']
  label = ''

  for attr in attributes:
    try:
      if benchmark_result[attr] != 0:
        label += attr + ', '
    except KeyError:
      pass

  return label[:-2]


def process_benchmark_data(benchmark_json, output_pdf, output_csv):
  benchmark_results_data = {}

  for benchmark_result in benchmark_json:
    # Skip aggregate results
    if 'aggregate_name' in benchmark_result:
      continue

    benchmark_variant = benchmark_result['name'].split('/')
    # The final split is always `real_time` and can be discarded
    benchmark_variant.remove('real_time')

    splits = len(benchmark_variant)
    # First split is always the benchmark function name
    benchmark_name = benchmark_variant[0]
    # The last split is always the seeded value into the benchmark
    benchmark_seeded_value = benchmark_variant[splits - 1]
    # The second last split is always the backend
    benchmark_backend = benchmark_variant[splits - 2]
    # Time taken (wall clock time) for benchmark to run
    benchmark_real_time = benchmark_result['real_time']
    benchmark_unit = benchmark_result['time_unit']

    benchmark_family_index = benchmark_result['family_index']

    benchmark_family_label = ''
    if splits > 3:
      for i in range(1, splits - 2):
        benchmark_family_label += benchmark_variant[i] + ', '

    benchmark_family_attributes = extrac_attributes_label(benchmark_result)

    if benchmark_family_attributes == '':
      benchmark_family_label = benchmark_family_label[:-2]
    else:
      benchmark_family_label = benchmark_family_label + benchmark_family_attributes

    if 'DrawCallCount' in benchmark_result:
      benchmark_draw_call_count = benchmark_result['DrawCallCount']
    else:
      benchmark_draw_call_count = -1

    optional_keys = [
        'DrawCallCount_Varies', 'VerbCount', 'PointCount', 'VertexCount',
        'GlyphCount'
    ]

    if benchmark_name not in benchmark_results_data:
      benchmark_results_data[benchmark_name] = BenchmarkResult(
          benchmark_name, benchmark_backend, benchmark_unit,
          benchmark_draw_call_count
      )

    for key in optional_keys:
      if key in benchmark_result:
        benchmark_results_data[benchmark_name].add_optional_value(
            key, benchmark_seeded_value, benchmark_result[key]
        )

    benchmark_results_data[benchmark_name].add_data_point(
        benchmark_family_index, benchmark_seeded_value, benchmark_real_time
    )
    benchmark_results_data[benchmark_name].set_family_label(
        benchmark_family_index, benchmark_family_label
    )

  pdf = pdfp(output_pdf)

  csv_file = open(output_csv, 'w')
  csv_writer = csv.writer(csv_file)

  for benchmark in benchmark_results_data:
    figures = benchmark_results_data[benchmark].plot()
    for fig in figures:
      pdf.savefig(fig)
    benchmark_results_data[benchmark].write_csv(csv_writer)
  pdf.close()


def parse_json(filename):
  try:
    json_file = open(filename, 'r')
  except:  # pylint: disable=bare-except
    error('Unable to load file.')

  try:
    json_data = json.load(json_file)
  except JSONDecodeError:  # pylint: disable=undefined-variable
    error('Invalid JSON. Unable to parse.')

  return json_data['benchmarks']


if __name__ == '__main__':
  sys.exit(main())
