// Copyright (C) 2025 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 {PerfettoPlugin} from '../../public/plugin';
import type {Trace} from '../../public/trace';
import {STR_NULL} from '../../trace_processor/query_result';
import GpuPlugin from '../dev.perfetto.Gpu';
import TraceProcessorTrackPlugin from '../dev.perfetto.TraceProcessorTrack';
import {TraceProcessorCounterTrack} from '../dev.perfetto.TraceProcessorTrack/trace_processor_counter_track';

export default class implements PerfettoPlugin {
  static readonly id = 'com.android.AndroidGpu';
  static readonly dependencies = [GpuPlugin, TraceProcessorTrackPlugin];

  async onTraceLoad(ctx: Trace): Promise<void> {
    // Only apply to Android traces.
    const meta = await ctx.engine.query(`
      select extract_metadata('android_build_fingerprint') as fingerprint
    `);
    const fingerprint = meta.firstRow({fingerprint: STR_NULL}).fingerprint;
    if (fingerprint === null) return;

    // Find gpu_counter tracks registered by the GPU plugin and check if
    // they need RateDelta treatment using metadata from their tags.
    const gpuCounterTracks = ctx.tracks
      .getAllTracks()
      .filter((t) => t.tags?.type === 'gpu_counter');
    console.log(gpuCounterTracks);

    for (const track of gpuCounterTracks) {
      const trackIds = track.tags?.trackIds;
      if (trackIds === undefined || trackIds.length !== 1) continue;
      const trackId = trackIds[0];

      const name = String(track.tags?.name ?? '');
      const unit = String(track.tags?.unit ?? '');
      const description = String(track.tags?.description ?? '');

      // If it's not a rate delta, just keep the track as-is.
      if (!isRateDelta(name, description, unit)) continue;

      // Register a new track with cumulative-sum SQL + rate mode.
      const newUri = `/android_gpu_counter_${trackId}`;
      ctx.tracks.registerTrack({
        uri: newUri,
        description: description || undefined,
        tags: track.tags,
        renderer: new CumulativeSumCounterTrack(
          ctx,
          newUri,
          unit,
          trackId,
          name,
        ),
      });

      // Re-point the existing TrackNode to our new track.
      const node = ctx.defaultWorkspace.getTrackByUri(track.uri);
      if (node !== undefined) {
        node.uri = newUri;
      }
    }
  }
}

// A counter track whose SQL source converts already-delta values into a
// running sum. When combined with yMode 'rate', the base counter track
// will compute (cumsum[t+1] - cumsum[t]) / dt which equals delta / dt,
// giving us the per-second rate we want.
class CumulativeSumCounterTrack extends TraceProcessorCounterTrack {
  constructor(
    trace: Trace,
    uri: string,
    unit: string,
    tid: number,
    trackName: string,
  ) {
    super({
      trace,
      uri,
      yMode: 'rate',
      unit,
      trackId: tid,
      trackName,
      rootTable: 'counter',
      sqlSource: `
      select
        id,
        ts,
        sum(value) over (order by ts) - value as value,
        arg_set_id
      from counter
      where track_id = ${tid}
    `,
    });
  }
}

// Extremely hacky function which determines whether a gpu_counter track should
// use RateDelta interpolation. RateDelta means the raw counter values are
// already deltas and we want to display them as per-second rates. Tracks that
// are NOT RateDelta (i.e. they represent instantaneous values) are left
// unchanged.
//
// This exists entirely because:
//  1) When the protos were designed, a field indicating whether or not a
//     was delta encoded was *not* added. This meant it was possible for some
//     counters to be delta and others to be monotontic without anyone at
//     analysis time knowing about it.
//  2) GPU counters were first added to Perfetto in 2019 timeframe. It's now
//     far too late for us to do something better in the trace processor level
//     about this as it would break backcompat for lots of people who are
//     relying on the existing visualization.
//  3) Because of how AGI used to visualize these counters, there's a general
//     expectation that this is the "correct" way to do it. For that reason,
//     it's also the case that if we inconsistent with AGI, people will think
//     we are doing it wrong even if we are more faithful. To reduce the drift
//     and keep things consistent, we have to bow to the "correct" way to do it
//     even if it is very hacky.
//
// Ideally at some point, we actually add some indication to the proto if this
// is a delta and that way, we can undo the deltafication in trace processor
// and allowing us to slowly get rid of this hack with time.
function isRateDelta(name: string, description: string, unit: string): boolean {
  // Percentage and "per" unit counters are instantaneous.
  if (unit === '%' || unit.includes('/')) {
    return false;
  }

  // Arm GPU counters with "per" in name are instantaneous.
  if (name.includes(' per ')) return false;

  // PowerVR-style counters with certain description patterns are instantaneous.
  if (
    description.includes('Current ') ||
    description.includes(' per ') ||
    description.includes(' over ') ||
    name.includes('Utilization') ||
    description.includes('Percentage')
  ) {
    return false;
  }

  // Qualcomm-style counters are instantaneous.
  if (
    description.includes('during a given sample period') ||
    name.includes('Average')
  ) {
    return false;
  }

  // Everything else: values are deltas, display as rate.
  return true;
}
