# Copyright 2018 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

from __future__ import annotations

# pylint: disable=too-many-lines

import csv
import os
import pathlib
import io
import shlex
import urllib.parse


from typing import Callable, Final, Iterable, Optional, Union


from core import benchmark_finders
from core import benchmark_utils

from telemetry.story import story_filter


_SHARD_MAP_DIR = os.path.join(os.path.dirname(__file__), 'shard_maps')

_ALL_BENCHMARKS_BY_NAMES = dict(
  (b.Name(), b) for b in benchmark_finders.GetAllBenchmarks()
)
GTEST_STORY_NAME = '_gtest_'


def _IsPlatformSupported(benchmark, platform: str) -> bool:
  supported = benchmark.GetSupportedPlatformNames(benchmark.SUPPORTED_PLATFORMS)
  return 'all' in supported or platform in supported


class _PerfPlatform(object):
  def __init__(
    self,
    name: str,
    description: str,
    benchmark_configs: PerfSuite,
    num_shards: int,
    platform_os: str,
    is_fyi: bool = False,
    run_reference_build: bool = False,
    pinpoint_only: bool = False,
    executables: Optional[frozenset[ExecutableConfig]] = None,
    crossbench: Optional[frozenset[CrossbenchConfig]] = None,
  ):
    benchmark_config_set = benchmark_configs.Frozenset()
    self._name = name
    self._description = description
    self._platform_os = platform_os
    # For sorting ignore case and 'segments' in the bot name.
    self._sort_key = name.lower().replace('-', ' ')
    self._is_fyi = is_fyi
    self.run_reference_build = run_reference_build
    self.pinpoint_only = pinpoint_only
    self.executables = executables or frozenset()
    self.crossbench = crossbench or frozenset()
    assert num_shards
    self._num_shards = num_shards
    # pylint: disable=redefined-outer-name
    self._benchmark_configs: frozenset[TelemetryConfig] = frozenset(
      [
        b
        for b in benchmark_config_set
        if _IsPlatformSupported(b.benchmark, self._platform_os)
      ]
    )
    # pylint: enable=redefined-outer-name
    benchmark_names = [config.name for config in self._benchmark_configs]
    assert len(set(benchmark_names)) == len(benchmark_names), (
      'Make sure that a benchmark does not appear twice.'
    )

    base_file_name = name.replace(' ', '_').lower()
    self._timing_file_path = os.path.join(
      _SHARD_MAP_DIR, 'timing_data', base_file_name + '_timing.json'
    )
    self.shards_map_file_name = base_file_name + '_map.json'
    self._shards_map_file_path = os.path.join(
      _SHARD_MAP_DIR, self.shards_map_file_name
    )

  def __lt__(self, other):
    if not isinstance(other, type(self)):
      return NotImplemented
    # pylint: disable=protected-access
    return self._sort_key < other._sort_key

  @property
  def num_shards(self) -> int:
    return self._num_shards

  @property
  def shards_map_file_path(self) -> str:
    return self._shards_map_file_path

  @property
  def timing_file_path(self) -> str:
    return self._timing_file_path

  @property
  def name(self) -> str:
    return self._name

  @property
  def description(self) -> str:
    return self._description

  @property
  def platform(self) -> str:
    return self._platform_os

  @property
  def benchmarks_to_run(self) -> frozenset[TelemetryConfig]:
    # TODO(crbug.com/40628256): Deprecate this in favor of benchmark_configs
    # as part of change to make sharding scripts accommodate abridged
    # benchmarks.
    return frozenset({b.benchmark for b in self._benchmark_configs})

  @property
  def benchmark_configs(self) -> frozenset[TelemetryConfig]:
    return self._benchmark_configs

  @property
  def is_fyi(self) -> bool:
    return self._is_fyi

  @property
  def is_official(self) -> bool:
    return not self._is_fyi

  @property
  def builder_url(self) -> Optional[str]:
    if self.pinpoint_only:
      return None
    return (
      'https://ci.chromium.org/p/chrome/builders/ci/%s'
      % urllib.parse.quote(self._name)
    )


class BenchmarkConfig(object):
  def __init__(
    self, name: str, repeat: int = 1, flags: tuple[str, ...] = ()
  ) -> None:
    self.name: Final[str] = name
    self.repeat: Final[int] = repeat
    self.flags: Final[tuple[str, ...]] = flags


class TelemetryConfig(BenchmarkConfig):
  def __init__(
    self, benchmark, abridged: bool = False, repeat: Optional[int] = None
  ):
    """A configuration for a benchmark that helps decide how to shard it.

    Args:
      benchmark: the benchmark.Benchmark object.
      abridged: True if the benchmark should be abridged so fewer stories
        are run, and False if the whole benchmark should be run.
      repeat: number of times to repeat the entire story set.
        can be None, which defaults to the benchmark default pageset_repeat.
    """
    self.benchmark = benchmark
    super().__init__(benchmark.Name(), self.get_repeat(repeat))
    self.abridged: Final[bool] = abridged
    self._stories: Optional[tuple[str, ...]] = None
    self._exhaustive_stories: Optional[tuple[str, ...]] = None

  def get_repeat(self, repeat: Optional[int]) -> int:
    if repeat is not None:
      return repeat
    return self.benchmark.options.get('pageset_repeat', 1)

  @property
  def stories(self) -> tuple[str, ...]:
    if self._stories is not None:
      return self._stories
    story_set = benchmark_utils.GetBenchmarkStorySet(self.benchmark())
    abridged_story_set_tag = (
      story_set.GetAbridgedStorySetTagFilter() if self.abridged else None
    )
    story_filter_obj = story_filter.StoryFilter(
      abridged_story_set_tag=abridged_story_set_tag
    )
    stories = story_filter_obj.FilterStories(story_set)
    self._stories = tuple(story.name for story in stories)
    return self._stories

  @property
  def exhaustive_stories(self) -> tuple[str, ...]:
    if self._exhaustive_stories is not None:
      return self._exhaustive_stories
    story_set = benchmark_utils.GetBenchmarkStorySet(
      self.benchmark(), exhaustive=True
    )
    abridged_story_set_tag = (
      story_set.GetAbridgedStorySetTagFilter() if self.abridged else None
    )
    story_filter_obj = story_filter.StoryFilter(
      abridged_story_set_tag=abridged_story_set_tag
    )
    stories = story_filter_obj.FilterStories(story_set)
    self._exhaustive_stories = tuple(story.name for story in stories)
    return self._exhaustive_stories


class ExecutableConfig(BenchmarkConfig):
  def __init__(
    self,
    name: str,
    path: Optional[str] = None,
    flags: tuple[str, ...] = (),
    estimated_runtime: int = 60,
    repeat: int = 1,
  ):
    super().__init__(name, repeat, flags)
    self.path: Final[str] = path or name
    self.estimated_runtime: Final[int] = estimated_runtime
    self.abridged: Final[bool] = False
    self.stories: Final[tuple[str, ...]] = (GTEST_STORY_NAME,)


class CrossbenchConfig(BenchmarkConfig):
  def __init__(
    self,
    name: str,
    crossbench_name: str,
    estimated_runtime: int = 60,
    stories: Optional[tuple[str]] = None,
    flags: tuple[str, ...] = (),
    repeat: int = 1,
    auto_enable_field_trials: bool = True,
  ):
    flags = self._process_flags(flags, auto_enable_field_trials)
    super().__init__(name, repeat, flags)
    self.crossbench_name: Final[str] = crossbench_name
    self.estimated_runtime: Final[int] = estimated_runtime
    self.stories: Final[tuple[str, ...]] = stories or ('default',)

  def _process_flags(
    self, flags: tuple[str, ...], auto_enable_field_trials: bool
  ) -> tuple[str, ...]:
    if auto_enable_field_trials:
      # Somewhat hacky solution if we want to run without field trials.
      if "--disable-field-trials" not in flags and (
        "--disable-field-trial-config" not in flags
      ):
        flags += ("--enable-field-trials=benchmarking",)
    assert len(flags) == len(set(flags)), (
      f"Found duplicate arguments in {flags}"
    )
    return flags


class PerfSuite(object):
  def __init__(self, configs):
    self._configs: dict[str, TelemetryConfig] = dict()
    self.Add(configs)

  def Frozenset(self) -> frozenset[TelemetryConfig]:
    return frozenset(self._configs.values())

  def Add(
    self, configs: Union[Iterable[Union[str, TelemetryConfig]], PerfSuite]
  ) -> PerfSuite:
    if isinstance(configs, PerfSuite):
      configs = configs.Frozenset()
    for config in configs:
      if isinstance(config, str):
        config = _TelemetryConfig(config)
      if config.name in self._configs:
        raise ValueError('Cannot have duplicate benchmarks/executables.')
      self._configs[config.name] = config
    return self

  def Remove(self, config_names: Iterable[str]) -> PerfSuite:
    for name in config_names:
      del self._configs[name]
    return self

  def Abridge(self, config_names: Iterable[str]) -> PerfSuite:
    for name in config_names:
      del self._configs[name]
      self._configs[name] = _TelemetryConfig(name, abridged=True)
    return self

  def Repeat(
    self, config_names: Iterable[str], pageset_repeat: int
  ) -> PerfSuite:
    for name in config_names:
      self._configs[name] = _TelemetryConfig(
        name,
        abridged=self._configs[name].abridged,
        pageset_repeat=pageset_repeat,
      )
    return self


def _TelemetryConfig(
  benchmark_name: str, abridged: bool = False, pageset_repeat: int | None = None
):
  benchmark = _ALL_BENCHMARKS_BY_NAMES[benchmark_name]
  return TelemetryConfig(benchmark, abridged, pageset_repeat)


BenchmarkConfigFactory = Callable[..., BenchmarkConfig]
_BENCHMARKS_CONFIG_FACTORIES: dict[str, BenchmarkConfigFactory] = {}
for b in _ALL_BENCHMARKS_BY_NAMES:
  _BENCHMARKS_CONFIG_FACTORIES[b] = _TelemetryConfig


def _register(name):

  def _decorator(func):
    if name in _BENCHMARKS_CONFIG_FACTORIES:
      raise ValueError('Duplicate benchmark config: %s' % name)
    _BENCHMARKS_CONFIG_FACTORIES[name] = func
    return func

  return _decorator


@_register('sync_performance_tests')
def _sync_performance_tests(
  estimated_runtime: int = 110, path=None, flags: tuple[str, ...] = ()
):
  return ExecutableConfig(
    'sync_performance_tests',
    path=path,
    flags=('--test-launcher-jobs=1', '--test-launcher-retry-limit=0', *flags),
    estimated_runtime=estimated_runtime,
  )


@_register('base_perftests')
def _base_perftests(
  estimated_runtime: int = 270, path=None, flags: tuple[str, ...] = ()
):
  return ExecutableConfig(
    'base_perftests',
    path=path,
    flags=('--test-launcher-jobs=1', '--test-launcher-retry-limit=0', *flags),
    estimated_runtime=estimated_runtime,
  )


@_register('components_perftests')
def _components_perftests(
  estimated_runtime: int = 110, flags: tuple[str, ...] = ()
):
  return ExecutableConfig(
    'components_perftests',
    flags=('--xvfb', *flags),
    estimated_runtime=estimated_runtime,
  )


@_register('dawn_perf_tests')
def _dawn_perf_tests(estimated_runtime: int = 270, flags: tuple[str, ...] = ()):
  return ExecutableConfig(
    'dawn_perf_tests',
    flags=('--test-launcher-jobs=1', '--test-launcher-retry-limit=0', *flags),
    estimated_runtime=estimated_runtime,
  )


@_register('tint_benchmark')
def _tint_benchmark(estimated_runtime: int = 180, flags: tuple[str, ...] = ()):
  return ExecutableConfig(
    'tint_benchmark',
    flags=('--use-chrome-perf-format', *flags),
    estimated_runtime=estimated_runtime,
  )


@_register('load_library_perf_tests')
def _load_library_perf_tests(
  estimated_runtime: int = 3, flags: tuple[str, ...] = ()
):
  return ExecutableConfig(
    'load_library_perf_tests', flags=flags, estimated_runtime=estimated_runtime
  )


@_register('performance_browser_tests')
def _performance_browser_tests(
  estimated_runtime: int = 67, flags: tuple[str, ...] = ()
):
  return ExecutableConfig(
    'performance_browser_tests',
    path='browser_tests',
    flags=(
      '--full-performance-run',
      '--test-launcher-jobs=1',
      '--test-launcher-retry-limit=0',
      # Allow the full performance runs to take up to 60 seconds (rather
      # than the default of 30 for normal CQ browser test runs).
      '--ui-test-action-timeout=60000',
      '--ui-test-action-max-timeout=60000',
      '--test-launcher-timeout=60000',
      '--gtest_filter=*/TabCapturePerformanceTest.*:*/CastV2PerformanceTest.*',
      *flags,
    ),
    estimated_runtime=estimated_runtime,
  )


@_register('tracing_perftests')
def _tracing_perftests(estimated_runtime: int = 5, flags: tuple[str, ...] = ()):
  return ExecutableConfig(
    'tracing_perftests', flags=flags, estimated_runtime=estimated_runtime
  )


@_register('views_perftests')
def _views_perftests(estimated_runtime: int = 7, flags: tuple[str, ...] = ()):
  return ExecutableConfig(
    'views_perftests',
    flags=('--xvfb', *flags),
    estimated_runtime=estimated_runtime,
  )


@_register('web_tests_cuj')
def _web_tests_cuj(estimated_runtime: int = 10, flags: tuple[str, ...] = ()):
  return CrossbenchConfig(
    'web_tests_cuj',
    'speedometer_3.1',
    estimated_runtime=estimated_runtime,
    flags=('--web-tests-cuj', '--debug', *flags),
  )


# Speedometer:
@_register('speedometer2.0.crossbench')
def _speedometer2_0_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'speedometer2.0.crossbench',
    'speedometer_2.0',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('speedometer2.1.crossbench')
def _speedometer2_1_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'speedometer2.1.crossbench',
    'speedometer_2.1',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('speedometer2.crossbench')
def _speedometer2_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  """Alias for the latest Speedometer 2.X version."""
  return CrossbenchConfig(
    'speedometer2.crossbench',
    'speedometer_2',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('speedometer3.0.crossbench')
def _speedometer3_0_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'speedometer3.0.crossbench',
    'speedometer_3.0',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('speedometer3.1.crossbench')
def _speedometer3_1_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'speedometer3.1.crossbench',
    'speedometer_3.1',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('speedometer3.crossbench')
def _speedometer3_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  """Alias for the latest Speedometer 3.X version."""
  return CrossbenchConfig(
    'speedometer3.crossbench',
    'speedometer_3',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register("speedometer3-turbolev.crossbench")
def _speedometer3_turbolev_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  flags += ("--js-flags=--turbolev",)
  return CrossbenchConfig(
    "speedometer3-turbolev.crossbench",
    "speedometer_3",
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register("speedometer3-turbolev_future.crossbench")
def _speedometer3_turbolev_future_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  flags += ("--js-flags=--turbolev-future",)
  return CrossbenchConfig(
    "speedometer3-turbolev_future.crossbench",
    "speedometer_3",
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('speedometer_main.crossbench')
def _speedometer_main_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  """The latest WIP speedometer version running all stories."""
  return CrossbenchConfig(
    'speedometer_main.crossbench',
    'speedometer_main',
    estimated_runtime=estimated_runtime,
    flags=('--detailed-metrics', *flags),
  )


@_register('speedometer_main.all.crossbench')
def _speedometer_main_all_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  """The latest WIP speedometer version running all stories,
  including experimental"""
  return CrossbenchConfig(
    'speedometer_main.all.crossbench',
    'speedometer_main',
    estimated_runtime=estimated_runtime,
    flags=('--stories=all', '--detailed-metrics', *flags),
  )


@_register('browser_startup.crossbench')
def _browser_startup_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  """Browser startup benchmark for InitialWebUI vs Baseline."""
  # We cannot use --browser-config here because it conflicts with the
  # --browser flag automatically added by the Chromium test runner.

  # NOTE: Keep this list in sync with:
  # third_party/crossbench/config/benchmark/browser_startup/browser.config.hjson
  INITIAL_WEBUI_FEATURES = (
    "InitialWebUI:high_stream_priority/true,"
    "WebUIReloadButton:WebUIReloadButtonDeferBrowserViewShow/true/"
    "WebUIReloadButtonKeepVisibleUntilPaint/true/"
    "WebUIReloadButtonPrewarmWebUI/true,"
    "SkipIPCChannelPausingForNonGuests,WebUIInProcessResourceLoadingV2,"
    "InitialWebUISyncNavStartToCommit,InitialWebUIWithoutExtensions,"
    "WebUIBundledCodeCache,SendGPUChannelEarly"
  )
  return CrossbenchConfig(
    'browser_startup.crossbench',
    'browser-startup',
    estimated_runtime=estimated_runtime,
    flags=(f'--enable-features={INITIAL_WEBUI_FEATURES}', *flags),
    repeat=10,
  )


@_register('speedometer3.a11y.crossbench')
def _speedometer3_a11y_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  """Latest Speedometer 3 with accessibility flag enabled."""
  return CrossbenchConfig(
    'speedometer3.a11y.crossbench',
    'speedometer_3',
    estimated_runtime=estimated_runtime,
    flags=('--extra-browser-args=--force-renderer-accessibility', *flags),
  )


# MotionMark:
@_register('motionmark1.2.crossbench')
def _motionmark1_2_crossbench(
  estimated_runtime: int = 360, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'motionmark1.2.crossbench',
    'motionmark_1.2',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('motionmark1.3.0.crossbench')
def _motionmark1_3_0_crossbench(
  estimated_runtime: int = 360, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'motionmark1.3.0.crossbench',
    'motionmark_1.3.0',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('motionmark1.3.1.crossbench')
def _motionmark1_3_1_crossbench(
  estimated_runtime: int = 360, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'motionmark1.3.1.crossbench',
    'motionmark_1.3.1',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('motionmark1.3.crossbench')
def _motionmark1_3_crossbench(
  estimated_runtime: int = 360, flags: tuple[str, ...] = ()
):
  """Alias for the latest MotionMark 1.3.X version."""
  return CrossbenchConfig(
    'motionmark1.3.crossbench',
    'motionmark_1.3',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('motionmark1.3-turbolev.crossbench')
def _motionmark1_3_turbolev_crossbench(
  estimated_runtime: int = 360, flags: tuple[str, ...] = ()
):
  flags += ('--js-flags=--turbolev',)
  return CrossbenchConfig(
    'motionmark1.3-turbolev.crossbench',
    'motionmark_1.3',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('motionmark_main.crossbench')
def _motionmark_main_crossbench(
  estimated_runtime: int = 360, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'motionmark_main.crossbench',
    'motionmark_main',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


# JetStream:
@_register('jetstream2.0.crossbench')
def _jetstream2_0_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'jetstream2.0.crossbench',
    'jetstream_2.0',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('jetstream2.1.crossbench')
def _jetstream2_1_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'jetstream2.1.crossbench',
    'jetstream_2.1',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('jetstream2.2.crossbench')
def _jetstream2_2_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'jetstream2.2.crossbench',
    'jetstream_2.2',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('jetstream2.crossbench')
def _jetstream2_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  """Alias of the latest JetStream 2.X version."""
  return CrossbenchConfig(
    'jetstream2.crossbench',
    'jetstream_2',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('jetstream3.0.crossbench')
def _jetstream3_0_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'jetstream3_0.crossbench',
    'jetstream_3.0',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('jetstream3.crossbench')
def _jetstream3_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'jetstream3.crossbench',
    'jetstream_3',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('jetstream_main.crossbench')
def _jetstream_main_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'jetstream_main.crossbench',
    'jetstream_main',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('jetstream3-turbolev.crossbench')
def _jetstream3_turbolev_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  flags += ('--js-flags=--turbolev',)
  return CrossbenchConfig(
    'jetstream3-turbolev.crossbench',
    'jetstream_3',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('jetstream3-turbolev_future.crossbench')
def _jetstream3_turbolev_future_crossbench(
  estimated_runtime: int = 180, flags: tuple[str, ...] = ()
):
  flags += ('--js-flags=--turbolev-future',)
  return CrossbenchConfig(
    'jetstream3-turbolev_future.crossbench',
    'jetstream_3',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


# LoadLine:
@_register('loadline_phone.crossbench')
def _loadline_phone_crossbench(
  estimated_runtime: int = 7000, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'loadline_phone.crossbench',
    'loadline-phone-fast',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('loadline_tablet.crossbench')
def _loadline_tablet_crossbench(
  estimated_runtime: int = 3600, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'loadline_tablet.crossbench',
    'loadline-tablet-fast',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('loadline2_phone.crossbench')
def _loadline2_phone_crossbench(
  estimated_runtime: int = 1000, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'loadline2_phone.crossbench',
    'loadline2-phone',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


# Webview:
@_register('loading.crossbench')
def _crossbench_loading(
  estimated_runtime: int = 750, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'loading.crossbench',
    'loading',
    estimated_runtime=estimated_runtime,
    flags=flags,
    auto_enable_field_trials=False,
  )


@_register('embedder.crossbench')
def _crossbench_embedder(
  estimated_runtime: int = 900, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'embedder.crossbench',
    'embedder',
    estimated_runtime=estimated_runtime,
    flags=flags,
    auto_enable_field_trials=False,
  )


@_register('gma.embedder.crossbench')
def _crossbench_gma_embedder(
  estimated_runtime: int = 900, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'gma.embedder.crossbench',
    'embedder',
    estimated_runtime=estimated_runtime,
    flags=flags,
    auto_enable_field_trials=False,
  )


@_register('shell.embedder.crossbench')
def _crossbench_shell_embedder(
  estimated_runtime: int = 900, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'shell.embedder.crossbench',
    'embedder',
    estimated_runtime=estimated_runtime,
    flags=flags,
    auto_enable_field_trials=False,
  )


@_register('devtools_frontend.crossbench')
def _devtools_frontend_crossbench(
  estimated_runtime: int = 60, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'devtools_frontend.crossbench',
    'devtools_frontend',
    estimated_runtime=estimated_runtime,
    flags=flags,
  )


@_register('blink-ai.crossbench')
def _crossbench_blink_ai(
  estimated_runtime: int = 300, flags: tuple[str, ...] = ()
):
  return CrossbenchConfig(
    'blink-ai.crossbench',
    'blink-ai',
    estimated_runtime=estimated_runtime,
    stories=('language_model',),
    flags=flags,
    auto_enable_field_trials=False,
  )


PLATFORM_INFO = {
  'linux-perf': {
    'description': (
      'Ubuntu-22.04, Precision 3930 Rack, NVIDIA GeForce GTX 1660'
    ),
    'num_shards': 4,
    'platform_os': 'linux',
    'is_fyi': False,
  },
  'linux-perf-pgo': {
    'description': 'Ubuntu-18.04, 8 core, NVIDIA Quadro P400',
    'num_shards': 26,
    'platform_os': 'linux',
    'is_fyi': False,
    'pinpoint_only': True,
  },
  'linux-perf-rel': {
    'description': 'Ubuntu-18.04, 8 core, NVIDIA Quadro P400',
    'num_shards': 2,
    'platform_os': 'linux',
    'is_fyi': False,
  },
  'linux-r350-perf': {
    'description': 'Ubuntu-22.04, 16 core',
    'num_shards': 30,
    'platform_os': 'linux',
    'is_fyi': False,
  },
  'linux-falcon-rak-5070-perf': {
    'description': 'Linux Falcon RAK 5070',
    'num_shards': 1,
    'platform_os': 'linux',
    'is_fyi': False,
  },
  'linux-nvidia-dgx-spark-arm-perf': {
    'description': 'Ubuntu-24.04, Nvidia DGX Spark',
    'num_shards': 1,
    'platform_os': 'linux',
    'is_fyi': False
  },
  'mac-intel-perf': {
    'description': 'Mac Mini 8,1, Core i7 3.2 GHz',
    'num_shards': 24,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m1_mini_2020-perf': {
    'description': 'Mac M1 Mini 2020',
    'num_shards': 28,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m1_mini_2020-perf-pgo': {
    'description': 'Mac M1 Mini 2020',
    'num_shards': 7,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m1_mini_2020-no-brp-perf': {
    'description': 'Mac M1 Mini 2020 with BRP disabled',
    'num_shards': 20,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m1-pro-perf': {
    'description': 'Mac M1 PRO 2020',
    'num_shards': 4,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m2-pro-perf': {
    'description': 'Mac M2 PRO Baremetal ARM',
    'num_shards': 20,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m3-pro-perf': {
    'description': 'Mac M3 PRO ARM',
    'num_shards': 4,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m4-mini-perf': {
    'description': 'Mac M4 mini ARM',
    'num_shards': 25,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m4-pro-perf': {
    'description': 'MacBook Pro M4 ARM',
    'num_shards': 15,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'mac-m5-pro-perf': {
    'description': 'Mac M5 PRO ARM',
    'num_shards': 2,
    'platform_os': 'mac',
    'is_fyi': False,
  },
  'win-10_laptop_low_end-perf': {
    'description': (
      'Low end windows 10 HP laptops. HD Graphics 5500, '
      'x86-64-i3-5005U, SSD, 4GB RAM.'
    ),
    'num_shards': 15,
    'platform_os': 'win',
    'is_fyi': False,
  },
  'win-10_laptop_low_end-perf-pgo': {
    'description': (
      'Low end windows 10 HP laptops. HD Graphics 5500, '
      'x86-64-i3-5005U, SSD, 4GB RAM.'
    ),
    'num_shards': 40,
    'platform_os': 'win',
    'is_fyi': False,
    'pinpoint_only': True,
  },
  'win-10-perf': {
    'description': (
      'Windows Intel HD 630 towers, Core i7-7700 3.6 GHz, '
      '16GB RAM, Intel Kaby Lake HD Graphics 630'
    ),
    'num_shards': 18,
    'platform_os': 'win',
    'is_fyi': False,
  },
  'win-10-perf-pgo': {
    'description': (
      'Windows Intel HD 630 towers, Core i7-7700 3.6 GHz, '
      '16GB RAM, Intel Kaby Lake HD Graphics 630'
    ),
    'num_shards': 18,
    'platform_os': 'win',
    'is_fyi': False,
    'pinpoint_only': True,
  },
  'win-10_amd_laptop-perf': {
    'description': 'Windows 10 Laptop with AMD chipset.',
    'num_shards': 3,
    'platform_os': 'win',
    'is_fyi': False,
  },
  'win-10_amd_laptop-perf-pgo': {
    'description': 'Windows 10 Laptop with AMD chipset.',
    'num_shards': 3,
    'platform_os': 'win',
    'is_fyi': False,
    'pinpoint_only': True,
  },
  'win-11-perf': {
    'description': 'Windows Dell PowerEdge R350',
    'num_shards': 20,
    'platform_os': 'win',
    'is_fyi': False,
  },
  'win-11-perf-pgo': {
    'description': 'Windows Dell PowerEdge R350',
    'num_shards': 26,
    'platform_os': 'win',
    'is_fyi': False,
    'pinpoint_only': True,
  },
  'win-falcon-rak-5070-perf': {
    'description': 'Windows Falcon RAK 5070',
    'num_shards': 1,
    'platform_os': 'win',
    'is_fyi': False,
  },
  'win-arm64-snapdragon-elite-perf': {
    'description': 'Windows Dell Snapdragon Elite',
    # TODO(crbug.com/541980022): reset to 28 when missing bots are found.
    'num_shards': 18,
    'platform_os': 'win',
    'is_fyi': False,
  },
  # TODO(crbug.com/525430279): Use all 8 bots to test the Canary image.
  'android-brya-kano-i5-8gb-perf': {
    'description': 'Brya SKU kano_12th_Gen_IntelR_CoreTM_i5_1235U_8GB',
    'num_shards': 8,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-corsola-steelix-8gb-perf': {
    'description': 'Corsola SKU steelix_MT8186_8GB',
    'num_shards': 7,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-nissa-uldren-8gb-perf': {
    'description': 'Nissa SKU uldren_99C4LZ/Q1XT/6W_8GB',
    'num_shards': 7,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel4-perf': {
    'description': 'Android R',
    'num_shards': 38,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel4-perf-pgo': {
    'description': 'Android R',
    'num_shards': 28,
    'platform_os': 'android',
    'is_fyi': False,
    'pinpoint_only': True,
  },
  'android-pixel4_webview-perf': {
    'description': 'Android R',
    'num_shards': 19,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel4_webview-perf-pgo': {
    'description': 'Android R',
    'num_shards': 12,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel6-perf-pgo': {
    'description': 'Android U',
    'num_shards': 8,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel6-pro-perf-pgo': {
    'description': 'Android T',
    'num_shards': 16,
    'platform_os': 'android',
    'is_fyi': False,
    'pinpoint_only': True,
  },
  'android-pixel-fold-perf': {
    'description': 'Android U',
    'num_shards': 8,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel-tangor-perf': {
    'description': 'Android U',
    'num_shards': 8,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-go-wembley-perf': {
    'description': 'Android U',
    'num_shards': 5,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-go-wembley_webview-perf': {
    'description': 'Android U',
    'num_shards': 5,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel9-perf': {
    'description': 'Android B',
    'num_shards': 4,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel9-pro-perf': {
    'description': 'Android B',
    'num_shards': 4,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel9-pro-xl-perf': {
    'description': 'Android B',
    'num_shards': 4,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel10-perf': {
    'description': 'Android B',
    'num_shards': 25,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel10_webview-perf': {
    'description': 'Android B',
    'num_shards': 23,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel10_webview-perf-pgo': {
    'description': 'Android B',
    'num_shards': 23,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'android-pixel10_webview-perf-pgo-heapdump': {
    'description': 'Android B',
    'num_shards': 1,
    'platform_os': 'android',
    'is_fyi': False,
  },
  'fuchsia-perf-nsn': {
    'description': '',
    'num_shards': 1,
    'platform_os': 'fuchsia',
    'is_fyi': True,
  },
  'fuchsia-perf-shk': {
    'description': '',
    'num_shards': 1,
    'platform_os': 'fuchsia',
    'is_fyi': True,
  },
  'win-10_laptop_low_end-perf_HP-Candidate': {
    'description': 'HP 15-BS121NR Laptop Candidate',
    'num_shards': 1,
    'platform_os': 'win',
    'is_fyi': True,
  },
  'chromeos-kevin-perf-fyi': {
    'description': '',
    'num_shards': 4,
    'platform_os': 'chromeos',
    'is_fyi': True,
  },
  'linux-perf-fyi': {
    'description': '',
    'num_shards': 1,
    'platform_os': 'linux',
    'is_fyi': True,
  },
}

# TODO: add more details.
BENCHMARK_INFO = {k: {} for k, v in _BENCHMARKS_CONFIG_FACTORIES.items()}


def LoadAllScheduleFiles() -> set[_PerfPlatform]:
  schedule_dir = pathlib.Path(__file__).resolve().parent / 'schedule'
  assert schedule_dir.is_dir(), f'Missing schedule directory {schedule_dir}'
  bot_to_csv_configs: dict[str, list[BenchmarkConfig]] = {}
  for file_path in schedule_dir.glob('*.csv'):
    LoadScheduleFile(file_path, bot_to_csv_configs)
  assert bot_to_csv_configs, 'No benchmark schedule configs generated'

  new_platforms = set()
  for name, platform_info in PLATFORM_INFO.items():
    csv_configs = bot_to_csv_configs.get(name, [])

    benchmark_configs = [
      c for c in csv_configs if isinstance(c, TelemetryConfig)
    ]
    executable_configs = frozenset(
      c for c in csv_configs if isinstance(c, ExecutableConfig)
    )
    crossbench_configs = frozenset(
      c for c in csv_configs if isinstance(c, CrossbenchConfig)
    )

    new_platform = _PerfPlatform(
      name=name,
      description=platform_info['description'],
      benchmark_configs=PerfSuite(benchmark_configs),
      num_shards=platform_info['num_shards'],
      platform_os=platform_info['platform_os'],
      is_fyi=platform_info.get('is_fyi', False),
      run_reference_build=platform_info.get('run_reference_build', False),
      pinpoint_only=platform_info.get('pinpoint_only', False),
      executables=executable_configs,
      crossbench=crossbench_configs,
    )
    new_platforms.add(new_platform)
  return new_platforms


def LoadScheduleFile(
  file_path: pathlib.Path, configs: dict[str, list[BenchmarkConfig]]
):
  name = file_path.stem
  factory = _BENCHMARKS_CONFIG_FACTORIES[name]
  # pylint: disable=comparison-with-callable)
  is_telemetry = factory == _TelemetryConfig
  # pylint: enable=comparison-with-callable)
  reader = ReadCSV(file_path)
  fieldnames = reader.fieldnames
  assert fieldnames, 'Missing field names'
  has_flags = 'flags' in fieldnames
  seen_bots: set[str] = set()
  for row in reader:
    if flags := ParseFlags(file_path, row, has_flags):
      row['flags'] = flags
    bot = row['bot']
    assert bot not in seen_bots, (
      f'Duplicate bot {bot!r} in schedule file {file_path}'
    )
    seen_bots.add(bot)
    config = _ParseScheduleConfigRow(row, name, factory, is_telemetry)
    configs.setdefault(bot, []).append(config)


def ReadCSV(file_path: pathlib.Path):
  contents = file_path.read_text(encoding='utf-8')
  reader = csv.DictReader(
    _StripComments(io.StringIO(contents)), restkey='*flag'
  )
  return reader


def _StripComments(iterator):
  for line in iterator:
    if line and line[0] != '#':
      yield line


def ParseFlags(file_path: pathlib.Path, row, has_flags: bool) -> str | None:
  if extraFlags := row.pop('*flag', None):
    assert has_flags, (
      f'Unexpected extra columns in {file_path}. Extra columns are only '
      'supported if a "flags" column exists.'
    )
    # Merge trailing extra flag values with the explicit flags.
    return ','.join((row['flags'], *extraFlags))
  return None


def _ParseScheduleConfigRow(
  row, name: str, factory: BenchmarkConfigFactory, is_telemetry: bool
) -> BenchmarkConfig:
  repeat = int(row.get('repeat', 1))
  kwargs = {}
  for k, v in row.items():
    if k in ('bot', 'repeat', 'shard'):
      continue
    kwargs[k] = _ParseScheduleConfigValue(k, v)

  if is_telemetry:
    return factory(name, pageset_repeat=repeat, **kwargs)

  config = factory(**kwargs)
  if hasattr(config, 'repeat'):
    config.repeat = repeat
  else:
    assert repeat == 1, f'Cannot use repeat > 1 yet on {name}'

  return config


def _ParseScheduleConfigValue(k, v):
  if k == 'flags':
    v = str(v)
    if v and v[0] == "'":
      raise ValueError(f'Unsupported single quote for flag escaping: {v!r}')
    return tuple(shlex.split(v))
  if v.lower() == 'true':
    return True
  if v.lower() == 'false':
    return False
  return int(v)


ALL_PLATFORMS: set[_PerfPlatform] = set()
ALL_PLATFORMS = LoadAllScheduleFiles()

assert ALL_PLATFORMS, 'No PerfPlatform found'
PLATFORMS_BY_NAME = {p.name: p for p in ALL_PLATFORMS}
FYI_PLATFORMS = {p for p in ALL_PLATFORMS if p.is_fyi}
OFFICIAL_PLATFORMS = {p for p in ALL_PLATFORMS if p.is_official}
ALL_PLATFORM_NAMES = {p.name for p in ALL_PLATFORMS}
OFFICIAL_PLATFORM_NAMES = {p.name for p in OFFICIAL_PLATFORMS}


def find_bot_platform(builder_name):
  for bot_platform in ALL_PLATFORMS:
    if bot_platform.name == builder_name:
      return bot_platform
  return None
