// Copyright (C) 2024 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use size 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 {AsyncLimiter} from '../../base/async_limiter';
import {AsyncDisposableStack} from '../../base/disposable_stack';
import type {Size2D} from '../../base/geom';
import {
  Duration,
  Time,
  TimeSpan,
  type duration,
  type time,
} from '../../base/time';
import {TimeScale} from '../../base/time_scale';
import type {TraceImpl} from '../../core/trace_impl';
import {LONG, NUM} from '../../trace_processor/query_result';
import {escapeSearchQuery} from '../../trace_processor/query_utils';
import {createVirtualTable} from '../../trace_processor/sql_utils';
import {calculateResolution} from '../../base/resolution';
import {COLOR_HIGHLIGHT} from '../../frontend/css_constants';

interface SearchSummary {
  tsStarts: BigInt64Array;
  tsEnds: BigInt64Array;
  count: Uint8Array;
}

/**
 * This component is drawn on top of the timeline and creates small yellow
 * rectangles that highlight the time span of search results (similarly to what
 * Chrome does on the scrollbar when you Ctrl+F and type a search term).
 * It reacts to changes in SearchManager and queries the quantized ranges of the
 * search results.
 */
export class SearchOverviewTrack implements AsyncDisposable {
  private readonly trash = new AsyncDisposableStack();
  private readonly trace: TraceImpl;
  private readonly limiter = new AsyncLimiter();
  private initialized = false;
  private previousResolution: duration | undefined;
  private previousSpan: TimeSpan | undefined;
  private previousSearchGeneration = 0;
  private searchSummary: SearchSummary | undefined;

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

  render(ctx: CanvasRenderingContext2D, size: Size2D) {
    this.maybeUpdate(size.width);
    this.renderSearchOverview(ctx, size);
  }

  private async initialize() {
    const engine = this.trace.engine;
    this.trash.use(
      await createVirtualTable({
        engine,
        name: 'search_summary_window',
        using: '__intrinsic_window(0, 0, 1)',
      }),
    );
    this.trash.use(
      await createVirtualTable({
        engine,
        name: 'search_summary_sched_span',
        using: 'span_join(sched PARTITIONED cpu, search_summary_window)',
      }),
    );
    this.trash.use(
      await createVirtualTable({
        engine,
        name: 'search_summary_slice_span',
        using: 'span_join(slice PARTITIONED track_id, search_summary_window)',
      }),
    );
  }

  private async update(
    search: string,
    start: time,
    end: time,
    resolution: duration,
  ): Promise<SearchSummary> {
    if (!this.initialized) {
      this.initialized = true;
      await this.initialize();
    }
    const searchLiteral = escapeSearchQuery(search);

    const resolutionScalingFactor = 10n;
    const quantum = resolution * resolutionScalingFactor;
    start = Time.quantFloor(start, quantum);

    const windowDur = Duration.max(Time.diff(end, start), 1n);
    const engine = this.trace.engine;
    // Regneerate the search window table based on the new quantums.
    // TODO(lalitm): this is a big hack. When searching is rewritten, we need to
    // remove this.
    await engine.query(`
      drop table search_summary_window;
      create virtual table search_summary_window
        using __intrinsic_window(${start}, ${windowDur}, ${quantum});
    `);

    const utidRes = await engine.query(`select utid from thread join process
      using(upid) where thread.name glob ${searchLiteral}
      or process.name glob ${searchLiteral}`);

    const utids = [];
    for (const it = utidRes.iter({utid: NUM}); it.valid(); it.next()) {
      utids.push(it.utid);
    }

    const res = await engine.query(`
        select
          (quantum_ts * ${quantum} + ${start}) as tsStart,
          ((quantum_ts+1) * ${quantum} + ${start}) as tsEnd,
          min(count(*), 255) as count
          from (
              select
              quantum_ts
              from search_summary_sched_span
              where utid in (${utids.join(',')})
            union all
              select
              quantum_ts
              from search_summary_slice_span
              where name glob ${searchLiteral}
          )
          group by quantum_ts
          order by quantum_ts;`);

    const numRows = res.numRows();
    const summary: SearchSummary = {
      tsStarts: new BigInt64Array(numRows),
      tsEnds: new BigInt64Array(numRows),
      count: new Uint8Array(numRows),
    };

    const it = res.iter({tsStart: LONG, tsEnd: LONG, count: NUM});
    for (let row = 0; it.valid(); it.next(), ++row) {
      summary.tsStarts[row] = it.tsStart;
      summary.tsEnds[row] = it.tsEnd;
      summary.count[row] = it.count;
    }
    return summary;
  }

  private maybeUpdate(timelineWidth: number) {
    const searchManager = this.trace.search;
    const timeline = this.trace.timeline;
    if (!searchManager.hasResults) {
      return;
    }
    const newSpan = timeline.visibleWindow;
    const newSearchGeneration = searchManager.searchGeneration;
    const maybeNewResolution = calculateResolution(newSpan, timelineWidth);
    if (!maybeNewResolution.ok) {
      return;
    }
    const newResolution = maybeNewResolution.value;
    const newTimeSpan = newSpan.toTimeSpan();
    if (
      this.previousSpan?.containsSpan(newTimeSpan.start, newTimeSpan.end) &&
      this.previousResolution === newResolution &&
      this.previousSearchGeneration === newSearchGeneration
    ) {
      return;
    }

    // TODO(hjd): We should restrict this to the start of the trace but
    // that is not easily available here.
    // N.B. Timestamps can be negative.
    const {start, end} = newTimeSpan.pad(newTimeSpan.duration);
    this.previousSpan = new TimeSpan(start, end);
    this.previousResolution = newResolution;
    this.previousSearchGeneration = newSearchGeneration;
    const search = searchManager.searchText;
    if (search === '') {
      this.searchSummary = {
        tsStarts: new BigInt64Array(0),
        tsEnds: new BigInt64Array(0),
        count: new Uint8Array(0),
      };
      return;
    }

    this.limiter.schedule(async () => {
      const summary = await this.update(
        searchManager.searchText,
        start,
        end,
        newResolution,
      );
      this.searchSummary = summary;
    });
  }

  private renderSearchOverview(
    ctx: CanvasRenderingContext2D,
    size: Size2D,
  ): void {
    const visibleWindow = this.trace.timeline.visibleWindow;
    const timescale = new TimeScale(visibleWindow, {
      left: 0,
      right: size.width,
    });

    if (!this.searchSummary) return;

    for (let i = 0; i < this.searchSummary.tsStarts.length; i++) {
      const tStart = Time.fromRaw(this.searchSummary.tsStarts[i]);
      const tEnd = Time.fromRaw(this.searchSummary.tsEnds[i]);
      if (!visibleWindow.overlaps(tStart, tEnd)) {
        continue;
      }
      const rectStart = Math.max(timescale.timeToPx(tStart), 0);
      const rectEnd = timescale.timeToPx(tEnd);
      ctx.fillStyle = COLOR_HIGHLIGHT;
      ctx.fillRect(
        Math.floor(rectStart),
        0,
        Math.ceil(rectEnd - rectStart),
        size.height,
      );
    }
    const results = this.trace.search.searchResults;
    if (results === undefined) {
      return;
    }
    const index = this.trace.search.resultIndex;
    if (index !== -1 && index < results.tses.length) {
      const start = results.tses[index];
      if (start !== -1n) {
        const triangleStart = Math.max(
          timescale.timeToPx(Time.fromRaw(start)),
          0,
        );
        ctx.fillStyle = '#000';
        ctx.beginPath();
        ctx.moveTo(triangleStart, size.height);
        ctx.lineTo(triangleStart - 3, 0);
        ctx.lineTo(triangleStart + 3, 0);
        ctx.lineTo(triangleStart, size.height);
        ctx.fill();
        ctx.closePath();
      }
    }
  }

  async [Symbol.asyncDispose](): Promise<void> {
    return await this.trash.asyncDispose();
  }
}
