// 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 './styles.scss';
import {addWattsonThreadTrack} from './wattson_thread_utils';
import type {App} from '../../public/app';
import {createAggregationTab} from '../../components/aggregation_adapter';
import {CounterTrack} from '../../components/tracks/counter_track';
import {SliceTrack} from '../../components/tracks/slice_track';
import type {PerfettoPlugin} from '../../public/plugin';
import type {Trace} from '../../public/trace';
import {SLICE_TRACK_KIND} from '../../public/track_kinds';
import {TrackNode} from '../../public/workspace';
import type {Engine} from '../../trace_processor/engine';
import {SourceDataset} from '../../trace_processor/dataset';
import {LONG, LONG_NULL, NUM, STR} from '../../trace_processor/query_result';
import type {RouteArgs} from '../../public/route_schema';
import {WattsonEstimateSelectionAggregator} from './estimate_aggregator';
import {
  WattsonCpuPackageSelectionAggregator,
  WattsonGpuPackageSelectionAggregator,
} from './package_aggregator';
import {WattsonProcessSelectionAggregator} from './process_aggregator';
import {WattsonThreadSelectionAggregator} from './thread_aggregator';
import {
  CPUSS_ESTIMATE_TRACK_KIND,
  GPUSS_ESTIMATE_TRACK_KIND,
  TPUSS_ESTIMATE_TRACK_KIND,
} from './track_kinds';
import {createCpuWarnings, missingWattsonCpuConfigs} from './warning';

const WINDOW_MAP: Record<string, string> = {
  perfetto_wattson_markers: 'markers',
  perfetto_wattson_trace: 'trace',
  perfetto_wattson_apps: 'atrace_apps',
  perfetto_wattson_app_startup: 'app_startup',
};

export default class Wattson implements PerfettoPlugin {
  static readonly id = `org.kernel.Wattson`;
  static readonly dependencies = [];
  public static windowsOfInterest = new Set<string>();

  static onActivate(_app: App, args: RouteArgs): void {
    const metrics: string[] = [];
    if (typeof args.metrics === 'string') {
      metrics.push(...args.metrics.split('--'));
    }
    Wattson.updateWindowsOfInterest(metrics);
  }

  async onTraceLoad(ctx: Trace): Promise<void> {
    const [
      markersSupported,
      cpuSupported,
      gpuSupported,
      tpuSupported,
      realCpuIdleCounters,
    ] = await Promise.all([
      hasWattsonMarkersSupport(ctx.engine),
      hasWattsonCpuSupport(ctx.engine),
      hasWattsonGpuSupport(ctx.engine),
      hasWattsonTpuSupport(ctx.engine),
      hasCpuIdleCounters(ctx.engine),
    ]);

    const missingEvents = markersSupported
      ? await missingWattsonCpuConfigs(ctx.engine)
      : [];

    // Short circuit if Wattson is not supported for this Perfetto trace
    if (!(markersSupported || cpuSupported || gpuSupported || tpuSupported)) {
      return;
    }

    // Register selection aggregators that are common to all subsystems.
    ctx.selection.registerAreaSelectionTab(
      createAggregationTab(ctx, new WattsonEstimateSelectionAggregator()),
    );

    const group = new TrackNode({name: 'Wattson', isSummary: true});
    ctx.defaultWorkspace.addChildInOrder(group);

    if (markersSupported) {
      await addWattsonMarkersElements(ctx, group);
    }
    if (cpuSupported || markersSupported) {
      await addWattsonCpuElements(
        ctx,
        group,
        missingEvents,
        realCpuIdleCounters,
      );
    }
    if (gpuSupported) {
      await addWattsonGpuElements(ctx, group);
    }
    if (tpuSupported) {
      await addWattsonTpuElements(ctx, group);
    }

    if (Wattson.windowsOfInterest.size > 0) {
      await this.pinThreadPowerTracks(ctx);
      Wattson.windowsOfInterest.clear();
    }
  }

  async pinThreadPowerTracks(ctx: Trace) {
    if (Wattson.windowsOfInterest.size === 0) return;

    // Gather all utids from all windows
    const windowQueries = Array.from(Wattson.windowsOfInterest)
      .map((window) => {
        const suffix = WINDOW_MAP[window];
        if (suffix === undefined) return undefined;
        return `
          INCLUDE PERFETTO MODULE wattson.aggregation;
          INCLUDE PERFETTO MODULE wattson.windows;

          SELECT utid
          FROM wattson_threads_aggregation!(wattson_window_${suffix})
          GROUP BY utid
          ORDER BY SUM(total_mws) DESC
          LIMIT 3
        `;
      })
      .filter((q): q is string => q !== undefined);

    const queryResults = await Promise.all(
      windowQueries.map((q) => ctx.engine.query(q)),
    );

    const utidsToPin = new Set<number>();
    for (const result of queryResults) {
      const it = result.iter({utid: NUM});
      for (; it.valid(); it.next()) {
        utidsToPin.add(it.utid);
      }
    }

    // Only add tracks of unique utids
    for (const utid of utidsToPin) {
      await addWattsonThreadTrack(ctx, utid, {pin: true, scrollTo: false});
    }
  }

  public static updateWindowsOfInterest(metrics: string[]) {
    for (const metric of metrics) {
      for (const key of Object.keys(WINDOW_MAP)) {
        if (metric.includes(key)) {
          Wattson.windowsOfInterest.add(key);
        }
      }
    }
  }
}

function makeWattsonEstimateTrack(
  trace: Trace,
  uri: string,
  queryKey: string,
  yRangeKey: string,
): CounterTrack {
  return CounterTrack.create({
    trace,
    uri,
    sqlSource: `SELECT ts, ${queryKey} AS value FROM _system_state_${queryKey}`,
    unit: 'mW',
    yRangeSharingKey: yRangeKey,
    onInit: async () => {
      await trace.engine.query(
        `INCLUDE PERFETTO MODULE wattson.ui.continuous_estimates;`,
      );
    },
  });
}

async function hasCpuIdleCounters(engine: Engine): Promise<boolean> {
  const result = await engine.query(`
    SELECT EXISTS (
      SELECT 1
      FROM cpu_counter_track
      WHERE type = 'cpu_idle'
    ) AS supported
  `);
  return !!result.firstRow({supported: NUM}).supported;
}

async function hasWattsonMarkersSupport(engine: Engine): Promise<boolean> {
  const result = await engine.query(`
    INCLUDE PERFETTO MODULE wattson.windows;
    SELECT EXISTS (
      SELECT 1 FROM wattson_window_markers
    ) AS supported
  `);
  return !!result.firstRow({supported: NUM}).supported;
}

async function hasWattsonCpuSupport(engine: Engine): Promise<boolean> {
  const result = await engine.query(`
    INCLUDE PERFETTO MODULE wattson.device_infos;
    SELECT
      EXISTS (SELECT 1 FROM _wattson_device) as device,
      EXISTS (SELECT 1 FROM cpu_counter_track WHERE type = 'cpu_frequency') as freq,
      EXISTS (SELECT 1 FROM cpu_counter_track WHERE type = 'cpu_idle') as idle
  `);
  const row = result.firstRow({device: NUM, freq: NUM, idle: NUM});
  return !!row.device && !!row.freq && !!row.idle;
}

async function hasWattsonGpuSupport(engine: Engine): Promise<boolean> {
  const result = await engine.query(`
    INCLUDE PERFETTO MODULE android.gpu.frequency;
    INCLUDE PERFETTO MODULE wattson.gpu.freq_idle;
    INCLUDE PERFETTO MODULE wattson.curves.utils;
    SELECT
      EXISTS (SELECT 1 FROM android_gpu_frequency) as freq,
      EXISTS (SELECT 1 FROM _gpu_power_state) as idle,
      EXISTS (SELECT 1 FROM _gpu_filtered_curves) as has_curves
  `);
  const row = result.firstRow({freq: NUM, idle: NUM, has_curves: NUM});
  return !!row.freq && !!row.idle && !!row.has_curves;
}

async function hasWattsonTpuSupport(engine: Engine): Promise<boolean> {
  const result = await engine.query(`
    INCLUDE PERFETTO MODULE wattson.tpu.freq_idle;
    SELECT EXISTS (
      SELECT 1 FROM _tpu_freq WHERE dur IS NOT NULL
    ) AS supported
  `);
  return !!result.firstRow({supported: NUM}).supported;
}

async function addWattsonMarkersElements(ctx: Trace, group: TrackNode) {
  const uri = `/wattson/markers_window`;
  const track = await SliceTrack.createMaterialized({
    trace: ctx,
    uri,
    dataset: new SourceDataset({
      schema: {
        ts: LONG,
        dur: LONG_NULL,
        name: STR,
      },
      src: '_wattson_markers_window',
    }),
    // Use default details panel
  });
  ctx.tracks.registerTrack({
    uri,
    tags: {
      kinds: [SLICE_TRACK_KIND],
    },
    renderer: track,
  });
  group.addChildInOrder(new TrackNode({uri, name: 'Wattson markers window'}));
}

async function addWattsonCpuElements(
  ctx: Trace,
  group: TrackNode,
  missingEvents: string[],
  hasCpuIdleCounters: boolean,
) {
  const warningDesc = createCpuWarnings(missingEvents, hasCpuIdleCounters);

  // CPUs estimate as part of CPU subsystem
  const estimateSuffix = `${hasCpuIdleCounters ? '' : ' crude'} estimate`;
  const cpuResult = await ctx.engine.query(
    `SELECT cpu FROM cpu WHERE machine_id = 0`,
  );
  const it = cpuResult.iter({cpu: NUM});
  for (; it.valid(); it.next()) {
    const queryKey = `cpu${it.cpu}_mw`;
    const uri = `/wattson/cpu_subsystem_estimate_cpu${it.cpu}`;
    ctx.tracks.registerTrack({
      uri,
      description: () => warningDesc,
      renderer: makeWattsonEstimateTrack(ctx, uri, queryKey, `CpuSubsystem`),
      tags: {
        kinds: [CPUSS_ESTIMATE_TRACK_KIND],
        wattson: `CPU${it.cpu}`,
      },
    });
    group.addChildInOrder(
      new TrackNode({
        uri,
        name: `Cpu${it.cpu}${estimateSuffix}`,
      }),
    );
  }

  const uri = `/wattson/cpu_subsystem_estimate_dsu_scu`;
  ctx.tracks.registerTrack({
    uri,
    renderer: makeWattsonEstimateTrack(ctx, uri, `dsu_scu_mw`, `CpuSubsystem`),
    tags: {
      kinds: [CPUSS_ESTIMATE_TRACK_KIND],
      wattson: 'Dsu_Scu',
    },
  });
  group.addChildInOrder(new TrackNode({uri, name: `DSU/SCU${estimateSuffix}`}));

  // Register selection aggregators.
  // NOTE: The registration order matters because subsequent aggregators
  // (Process, Package) depend on views created by Thread aggregator
  ctx.selection.registerAreaSelectionTab(
    createAggregationTab(ctx, new WattsonThreadSelectionAggregator(ctx)),
  );
  ctx.selection.registerAreaSelectionTab(
    createAggregationTab(ctx, new WattsonProcessSelectionAggregator()),
  );

  if (await isProcessMetadataPresent(ctx.engine)) {
    ctx.selection.registerAreaSelectionTab(
      createAggregationTab(ctx, new WattsonCpuPackageSelectionAggregator()),
    );
  }
}

async function isProcessMetadataPresent(engine: Engine) {
  const packageInfo = await engine.query(`
    INCLUDE PERFETTO MODULE android.process_metadata;
    SELECT COUNT(*) as count FROM android_process_metadata
    WHERE package_name IS NOT NULL
  `);
  return packageInfo.firstRow({count: NUM}).count > 0;
}

async function addWattsonGpuElements(ctx: Trace, group: TrackNode) {
  const id = `/wattson/gpu_subsystem_estimate`;
  ctx.tracks.registerTrack({
    uri: id,
    renderer: makeWattsonEstimateTrack(ctx, id, `gpu_mw`, `GpuSubsystem`),
    tags: {
      kinds: [GPUSS_ESTIMATE_TRACK_KIND],
      wattson: 'Gpu',
    },
  });
  group.addChildInOrder(new TrackNode({uri: id, name: `GPU Estimate`}));

  ctx.selection.registerAreaSelectionTab(
    createAggregationTab(ctx, new WattsonGpuPackageSelectionAggregator()),
  );
}

async function addWattsonTpuElements(ctx: Trace, group: TrackNode) {
  const id = `/wattson/tpu_subsystem_estimate`;
  ctx.tracks.registerTrack({
    uri: id,
    renderer: makeWattsonEstimateTrack(ctx, id, `tpu_mw`, `TpuSubsystem`),
    tags: {
      kinds: [TPUSS_ESTIMATE_TRACK_KIND],
      wattson: 'Tpu',
    },
  });
  group.addChildInOrder(new TrackNode({uri: id, name: `TPU Estimate`}));
}
