// Copyright (C) 2024 The Android Open Source Project
//
// 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 type {Trace} from '../../public/trace';
import type {PerfettoPlugin} from '../../public/plugin';
import {METRIC_HANDLERS} from './handlers/handlerRegistry';
import type {MetricData, MetricHandlerMatch} from './handlers/metricUtils';
import AndroidCujsPlugin from '../com.android.AndroidCujs';
import Wattson from '../org.kernel.Wattson';

const JANK_CUJ_QUERY_PRECONDITIONS = `
  SELECT RUN_METRIC('android/android_blocking_calls_cuj_metric.sql');
`;

function getMetricsFromHash(): string[] {
  // TODO(stevegolton): this uses `dev.perfetto.PinAndroidPerfMetrics` for
  // back-compat reasons only. Figure out a way to preserve backwards
  // compatibility of plugin arguments when plugins change id.
  const metricVal = location.hash;
  const regex = new RegExp(`dev.perfetto.PinAndroidPerfMetrics:metrics=(.*)`);
  const match = metricVal.match(regex);
  if (match === null) {
    return [];
  }
  const capturedString = match[1];
  let metricList: string[] = [];
  if (capturedString.includes('--')) {
    metricList = capturedString.split('--');
  } else {
    metricList = [capturedString];
  }
  return metricList.map((metric) => decodeURIComponent(metric));
}

let metrics: string[];

/**
 * Plugin that adds and pins the debug track for the metric passed
 * For more context -
 * This plugin reads the names of regressed metrics from the url upon loading
 * It then checks the metric names against some handlers and if they
 * match it accordingly adds the debug tracks for them
 * This way when comparing two different perfetto traces before and after
 * the regression, the user will not have to manually search for the
 * slices related to the regressed metric
 */
export default class implements PerfettoPlugin {
  static readonly id = 'com.android.PinAndroidPerfMetrics';
  static readonly dependencies = [AndroidCujsPlugin, Wattson];

  static onActivate(): void {
    metrics = getMetricsFromHash();
    Wattson.updateWindowsOfInterest(metrics);
  }

  async onTraceLoad(ctx: Trace) {
    ctx.commands.registerCommand({
      id: 'com.android.PinAndroidPerfMetrics',
      name: 'Add and Pin: Jank Metric Slice',
      callback: async () => {
        const metric = await ctx.omnibox.prompt(
          'Metrics names (separated by comma)',
        );
        if (metric === undefined) return;
        const metricList = metric.split(',');
        this.callHandlers(metricList, ctx);
      },
    });
    if (metrics.length !== 0) {
      const plugin = ctx.plugins.getPlugin(AndroidCujsPlugin);
      await plugin.pinJankCujs(ctx);
      await plugin.pinLatencyCujs(ctx);
      this.callHandlers(metrics, ctx);
    }
  }

  private async callHandlers(metricsList: string[], ctx: Trace) {
    // List of metrics that actually match some handler
    const metricsToShow: MetricHandlerMatch[] =
      this.getMetricsToShow(metricsList);

    if (metricsToShow.length === 0) {
      return;
    }

    await ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS);
    for (const {metricData, metricHandler} of metricsToShow) {
      metricHandler.addMetricTrack(metricData, ctx);
    }
  }

  private getMetricsToShow(metricList: string[]): MetricHandlerMatch[] {
    const sortedMetricList = [...metricList].sort();
    const validMetrics: MetricHandlerMatch[] = [];
    const alreadyMatchedMetricData: Set<string> = new Set();
    for (const metric of sortedMetricList) {
      for (const metricHandler of METRIC_HANDLERS) {
        const metricData = metricHandler.match(metric);
        if (!metricData) continue;
        const jsonMetricData = this.metricDataToJson(metricData);
        if (!alreadyMatchedMetricData.has(jsonMetricData)) {
          alreadyMatchedMetricData.add(jsonMetricData);
          validMetrics.push({
            metricData: metricData,
            metricHandler: metricHandler,
          });
        }
      }
    }
    return validMetrics;
  }

  private metricDataToJson(metricData: MetricData): string {
    // Used to have a deterministic keys order.
    return JSON.stringify(metricData, Object.keys(metricData).sort());
  }
}
