// Copyright (C) 2026 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 {QuerySlot, type QueryResult} from '../../../base/query_slot';
import type {Engine} from '../../../trace_processor/engine';
import {
  NUM,
  STR,
  type QueryResult as TPQueryResult,
} from '../../../trace_processor/query_result';
import type {AggregateFunction} from '../datagrid/model';
import {sqlAggregateExpr} from '../datagrid/sql_utils';
import type {HeatmapData} from './heatmap';
import {validateColumnName} from './chart_utils';

/**
 * Configuration for heatmap loaders.
 */
export interface HeatmapLoaderConfig {
  /** Aggregation function for cell values. Defaults to 'SUM'. */
  readonly aggregation?: AggregateFunction;

  /** Maximum number of X-axis categories. */
  readonly xLimit?: number;

  /** Maximum number of Y-axis categories. */
  readonly yLimit?: number;
}

/**
 * Configuration for SQLHeatmapLoader.
 */
export interface SQLHeatmapLoaderOpts {
  /** The trace processor engine to run queries against. */
  readonly engine: Engine;

  /** SQL query that provides the data. */
  readonly query: string;

  /** Column name for the X axis (columns of the heatmap). */
  readonly xColumn: string;

  /** Column name for the Y axis (rows of the heatmap). */
  readonly yColumn: string;

  /** Column name containing the numeric values to aggregate. */
  readonly valueColumn: string;
}

/**
 * SQL-based heatmap loader.
 *
 * Groups data by two categorical dimensions (x, y) and aggregates a
 * numeric value, producing a grid suitable for the HeatmapChart widget.
 */
export class SQLHeatmapLoader {
  private readonly engine: Engine;
  private readonly query: string;
  private readonly xColumn: string;
  private readonly yColumn: string;
  private readonly valueColumn: string;
  private readonly querySlot = new QuerySlot<HeatmapData>();

  constructor(opts: SQLHeatmapLoaderOpts) {
    validateColumnName(opts.xColumn);
    validateColumnName(opts.yColumn);
    validateColumnName(opts.valueColumn);
    this.engine = opts.engine;
    this.query = opts.query;
    this.xColumn = opts.xColumn;
    this.yColumn = opts.yColumn;
    this.valueColumn = opts.valueColumn;
  }

  use(config: HeatmapLoaderConfig): QueryResult<HeatmapData> {
    const agg = config.aggregation ?? 'SUM';
    const xLimit = config.xLimit ?? 20;
    const yLimit = config.yLimit ?? 20;
    const aggExpr = sqlAggregateExpr(agg, this.valueColumn);

    const sql = `
WITH _src AS (
  SELECT
    CAST(${this.xColumn} AS TEXT) AS _x,
    CAST(${this.yColumn} AS TEXT) AS _y,
    ${aggExpr} AS _val
  FROM (${this.query})
  WHERE ${this.valueColumn} IS NOT NULL
  GROUP BY _x, _y
),
_top_x AS (
  SELECT _x, SUM(_val) AS _total
  FROM _src
  GROUP BY _x
  ORDER BY _total DESC
  LIMIT ${xLimit}
),
_top_y AS (
  SELECT _y, SUM(_val) AS _total
  FROM _src
  GROUP BY _y
  ORDER BY _total DESC
  LIMIT ${yLimit}
)
SELECT _x, _y, _val
FROM _src
WHERE _x IN (SELECT _x FROM _top_x)
  AND _y IN (SELECT _y FROM _top_y)
ORDER BY _x, _y`.trim();

    return this.querySlot.use({
      key: {sql},
      retainOn: ['sql'],
      queryFn: async () => {
        const queryResult = await this.engine.query(sql);
        return parseHeatmapResult(queryResult);
      },
    });
  }

  dispose(): void {
    this.querySlot.dispose();
  }
}

function parseHeatmapResult(queryResult: TPQueryResult): HeatmapData {
  const xSet = new Map<string, number>();
  const ySet = new Map<string, number>();
  const rawValues: Array<{x: string; y: string; val: number}> = [];

  const iter = queryResult.iter({
    _x: STR,
    _y: STR,
    _val: NUM,
  });

  for (; iter.valid(); iter.next()) {
    rawValues.push({x: iter._x, y: iter._y, val: iter._val});
    if (!xSet.has(iter._x)) {
      xSet.set(iter._x, xSet.size);
    }
    if (!ySet.has(iter._y)) {
      ySet.set(iter._y, ySet.size);
    }
  }

  const xLabels = Array.from(xSet.keys());
  const yLabels = Array.from(ySet.keys());

  let min = Infinity;
  let max = -Infinity;
  const values: Array<readonly [number, number, number]> = rawValues.map(
    ({x, y, val}) => {
      min = Math.min(min, val);
      max = Math.max(max, val);
      const xIdx = xSet.get(x) ?? 0;
      const yIdx = ySet.get(y) ?? 0;
      return [xIdx, yIdx, val] as const;
    },
  );

  if (min === Infinity) {
    min = 0;
    max = 0;
  }

  return {xLabels, yLabels, values, min, max};
}
