// 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 {canvasSave, drawDoubleHeadedArrow} from '../../base/canvas_utils';
import {Size2D} from '../../base/geom';
import {QuerySlot, SerialTaskQueue} from '../../base/query_slot';
import {Duration, time} from '../../base/time';
import {TimeScale} from '../../base/time_scale';
import {drawVerticalLineAtTime} from '../../base/vertical_line_helper';
import {asSchedSqlId} from '../../components/sql_utils/core_types';
import {
  getSched,
  getSchedWakeupInfo,
  SchedWakeupInfo,
} from '../../components/sql_utils/sched';
import {CanvasColors} from '../../public/canvas_colors';
import {Selection, TrackEventSelection} from '../../public/selection';
import {Trace} from '../../public/trace';
import {Overlay, TrackBounds} from '../../public/track';
import {CPU_SLICE_URI_PREFIX, uriForSchedTrack} from './common';

const MARGIN = 3;
const DIAMOND_SIZE = 8;
const ARROW_HEIGHT = 12;

export class WakerOverlay implements Overlay {
  private readonly trace: Trace;
  private readonly queue = new SerialTaskQueue();
  private readonly wakeupSlot = new QuerySlot<SchedWakeupInfo | undefined>(
    this.queue,
  );

  constructor(trace: Trace) {
    this.trace = trace;
  }

  render(
    canvasCtx: CanvasRenderingContext2D,
    timescale: TimeScale,
    size: Size2D,
    renderedTracks: ReadonlyArray<TrackBounds>,
    colors: CanvasColors,
  ): void {
    const selection = this.trace.selection.selection;

    // Get out if selection is not a CPU slice.
    if (!this.cpuSliceTrackSelected(selection)) {
      return;
    }

    // Declaratively fetch wakeup info - QuerySlot handles caching and scheduling
    const result = this.wakeupSlot.use({
      key: {eventId: selection.eventId},
      queryFn: () => this.loadWakeupInfo(selection),
    });

    const wakeup = result.data;
    if (!wakeup?.wakeupTs) {
      return;
    }

    // Draw the vertical line at the wakeup timestamp
    drawVerticalLineAtTime(
      canvasCtx,
      timescale,
      wakeup.wakeupTs,
      size.height,
      colors.COLOR_TIMELINE_OVERLAY,
    );

    // Draw the marker on the waker CPU track
    if (wakeup.wakerCpu !== undefined) {
      this.drawWakerMarker(
        canvasCtx,
        timescale,
        renderedTracks,
        wakeup.wakeupTs,
        wakeup.wakerCpu,
        colors.COLOR_TIMELINE_OVERLAY,
      );
    }

    this.drawLatencyArrow(
      canvasCtx,
      timescale,
      renderedTracks,
      wakeup.wakeupTs,
      selection.trackUri,
      selection.ts,
      colors.COLOR_TIMELINE_OVERLAY,
      colors.COLOR_BACKGROUND,
      colors.COLOR_TEXT,
    );
  }

  private cpuSliceTrackSelected(
    selection: Selection,
  ): selection is TrackEventSelection {
    return (
      selection.kind === 'track_event' &&
      selection.trackUri.startsWith(CPU_SLICE_URI_PREFIX)
    );
  }

  private async loadWakeupInfo(
    selection: TrackEventSelection,
  ): Promise<SchedWakeupInfo | undefined> {
    const sched = await getSched(
      this.trace.engine,
      asSchedSqlId(selection.eventId),
    );
    if (!sched) return undefined;
    const cache = await getSchedWakeupInfo(this.trace.engine, sched);
    return cache;
  }

  private drawWakerMarker(
    canvasCtx: CanvasRenderingContext2D,
    timescale: TimeScale,
    renderedTracks: ReadonlyArray<TrackBounds>,
    wakeupTs: time,
    wakerCpu: number,
    color: string,
  ): void {
    const wakerCpuTrackUri = uriForSchedTrack(wakerCpu);
    const wakerTrack = renderedTracks.find(
      (track) => wakerCpuTrackUri === track.node.uri,
    );

    if (!wakerTrack) return;

    const bounds = wakerTrack.verticalBounds;
    const trackHeight = bounds.bottom - bounds.top;
    const rectHeight = trackHeight - 2 * MARGIN;
    const wakeupPosPx = Math.floor(timescale.timeToPx(wakeupTs));

    using _ = canvasSave(canvasCtx);
    canvasCtx.translate(0, bounds.top);
    canvasCtx.beginPath();
    const yCenter = MARGIN + rectHeight / 2;
    canvasCtx.moveTo(wakeupPosPx, yCenter + DIAMOND_SIZE);
    canvasCtx.fillStyle = color;
    canvasCtx.lineTo(wakeupPosPx + DIAMOND_SIZE * 0.75, yCenter);
    canvasCtx.lineTo(wakeupPosPx, yCenter - DIAMOND_SIZE);
    canvasCtx.lineTo(wakeupPosPx - DIAMOND_SIZE * 0.75, yCenter);
    canvasCtx.fill();
    canvasCtx.closePath();
  }

  private drawLatencyArrow(
    canvasCtx: CanvasRenderingContext2D,
    timescale: TimeScale,
    renderedTracks: ReadonlyArray<TrackBounds>,
    wakeupTs: time,
    wakedTrackUri: string,
    wakedSliceTs: time,
    arrowColor: string,
    backgroundColor: string,
    textColor: string,
  ): void {
    const wakedTrack = renderedTracks.find(
      (track) => wakedTrackUri === track.node.uri,
    );

    if (!wakedTrack) return;

    const bounds = wakedTrack.verticalBounds;
    const trackHeight = bounds.bottom - bounds.top;
    const rectHeight = trackHeight - 2 * MARGIN;
    const wakeupPosPx = timescale.timeToPx(wakeupTs);
    const wakedSliceStartPx = timescale.timeToPx(wakedSliceTs);
    const latencyWidthPx = wakedSliceStartPx - wakeupPosPx;

    using _ = canvasSave(canvasCtx);
    canvasCtx.translate(0, bounds.top);

    // Draw the double-headed arrow
    drawDoubleHeadedArrow(
      canvasCtx,
      wakeupPosPx,
      MARGIN + rectHeight,
      latencyWidthPx,
      latencyWidthPx >= 20, // Only draw arrow heads if width is sufficient
      arrowColor,
    );

    // Draw latency text if space permits
    const latency = wakedSliceTs - wakeupTs;
    const displayText = Duration.humanise(latency);
    const measured = canvasCtx.measureText(displayText);
    if (latencyWidthPx >= measured.width + 2) {
      const textX = wakeupPosPx + latencyWidthPx / 2;
      const textY = MARGIN + rectHeight - 1;
      const textBgY = MARGIN + rectHeight - ARROW_HEIGHT;

      // Semi-transparent background for text
      canvasCtx.globalAlpha = 0.7;
      canvasCtx.fillStyle = backgroundColor;
      canvasCtx.fillRect(
        textX - measured.width / 2 - 1,
        textBgY,
        measured.width + 2,
        ARROW_HEIGHT - 1, // Height adjusted to fit within arrow bounds
      );
      canvasCtx.globalAlpha = 1.0;

      // Latency text
      canvasCtx.textBaseline = 'bottom';
      canvasCtx.fillStyle = textColor;
      canvasCtx.textAlign = 'center';
      canvasCtx.fillText(displayText, textX, textY);
    }
  }
}
