// 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 type {Engine} from '../../../trace_processor/engine';
import {
  NUM,
  STR_NULL,
  type QueryResult,
} from '../../../trace_processor/query_result';
import {
  ChartSource,
  SQLChartLoader,
  type QueryConfig,
  type ChartLoaderResult,
  inFilter,
} from './chart_sql_source';
import type {ChartAggregation} from './chart_utils';
import type {TreemapData, TreemapNode} from './treemap';

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

  /**
   * SQL query that provides the raw data.
   * Must include label and size columns (and optionally group column).
   */
  readonly query: string;

  /** Column name for leaf node labels. */
  readonly labelColumn: string;

  /** Column name for rectangle size (measure). */
  readonly sizeColumn: string;

  /**
   * Optional column for parent grouping (creates 2-level hierarchy).
   * When omitted, all nodes are at the top level.
   */
  readonly groupColumn?: string;
}

/**
 * Per-use configuration for the treemap loader.
 */
export interface TreemapLoaderConfig {
  /** Aggregation function to apply to the size column. Defaults to 'SUM'. */
  readonly aggregation?: ChartAggregation;

  /**
   * Maximum number of leaf nodes to return per group.
   * Nodes are sorted by size descending. Defaults to no limit.
   */
  readonly limit?: number;

  /** Filter to only include specific labels. */
  readonly labelFilter?: ReadonlyArray<string | number>;

  /** Filter to only include specific groups. */
  readonly groupFilter?: ReadonlyArray<string | number>;
}

/** Result returned by the treemap loader. */
export type TreemapLoaderResult = ChartLoaderResult<TreemapData>;

/**
 * SQL-based treemap loader with async loading and caching.
 *
 * Creates 1 or 2 level hierarchy from SQL data. When groupColumn is
 * provided, creates parent nodes for each group with children for labels.
 * Uses QuerySlot for caching and request deduplication.
 */
export class SQLTreemapLoader extends SQLChartLoader<
  TreemapLoaderConfig,
  TreemapData
> {
  private readonly labelColumn: string;
  private readonly sizeColumn: string;
  private readonly groupColumn: string | undefined;

  constructor(opts: SQLTreemapLoaderOpts) {
    const schema: Record<string, 'text' | 'real'> = {
      [opts.labelColumn]: 'text',
      [opts.sizeColumn]: 'real',
    };
    if (opts.groupColumn !== undefined) {
      schema[opts.groupColumn] = 'text';
    }
    super(opts.engine, new ChartSource({query: opts.query, schema}));
    this.labelColumn = opts.labelColumn;
    this.sizeColumn = opts.sizeColumn;
    this.groupColumn = opts.groupColumn;
  }

  protected buildQueryConfig(config: TreemapLoaderConfig): QueryConfig {
    const aggregation = config.aggregation ?? 'SUM';
    const dimensions =
      this.groupColumn !== undefined
        ? [
            {column: this.groupColumn, alias: '_group'},
            {column: this.labelColumn, alias: '_label'},
          ]
        : [{column: this.labelColumn, alias: '_label'}];

    const filters = [
      ...inFilter(this.labelColumn, config.labelFilter),
      ...(this.groupColumn !== undefined
        ? inFilter(this.groupColumn, config.groupFilter)
        : []),
    ];

    return {
      type: 'aggregated',
      dimensions,
      measures: [{column: this.sizeColumn, aggregation}],
      filters,
      limitPerGroup: this.groupColumn !== undefined ? config.limit : undefined,
      limit: this.groupColumn === undefined ? config.limit : undefined,
      orderDirection: 'desc',
    };
  }

  protected parseResult(queryResult: QueryResult): TreemapData {
    if (this.groupColumn !== undefined) {
      return this.parseGrouped(queryResult);
    }
    return this.parseFlat(queryResult);
  }

  private parseGrouped(queryResult: QueryResult): TreemapData {
    const groupMap = new Map<string, TreemapNode[]>();
    const iter = queryResult.iter({
      _group: STR_NULL,
      _label: STR_NULL,
      _value: NUM,
    });

    for (; iter.valid(); iter.next()) {
      const groupName = iter._group ?? '(uncategorized)';
      const labelName = iter._label ?? '(null)';

      let children = groupMap.get(groupName);
      if (children === undefined) {
        children = [];
        groupMap.set(groupName, children);
      }
      children.push({name: labelName, value: iter._value});
    }

    const nodes: TreemapNode[] = [];
    for (const [groupName, children] of groupMap) {
      nodes.push({
        name: groupName,
        value: children.reduce((sum, c) => sum + c.value, 0),
        children,
      });
    }
    return {nodes};
  }

  private parseFlat(queryResult: QueryResult): TreemapData {
    const nodes: TreemapNode[] = [];
    const iter = queryResult.iter({_label: STR_NULL, _value: NUM});

    for (; iter.valid(); iter.next()) {
      nodes.push({name: iter._label ?? '(null)', value: iter._value});
    }
    return {nodes};
  }
}
