// 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 m from 'mithril';
import type {EChartsCoreOption} from 'echarts/core';
import {extractBrushRect, formatNumber} from './chart_utils';
import {EChartView, type EChartEventHandler} from './echart_view';
import {
  buildAxisOption,
  buildBrushOption,
  buildGridOption,
  buildTooltipOption,
  SELECTION_COLOR,
} from './chart_option_builder';

/**
 * Data provided to a HeatmapChart.
 * The grid is defined by xLabels, yLabels, and a values matrix.
 */
export interface HeatmapData {
  /** Labels for the X axis (columns). */
  readonly xLabels: readonly string[];
  /** Labels for the Y axis (rows). */
  readonly yLabels: readonly string[];
  /**
   * Values as [xIndex, yIndex, value] triples.
   * Missing entries are treated as 0.
   */
  readonly values: ReadonlyArray<readonly [number, number, number]>;
  /** Minimum value in the dataset (for color scale). */
  readonly min: number;
  /** Maximum value in the dataset (for color scale). */
  readonly max: number;
}

export interface HeatmapAttrs {
  /**
   * Heatmap data to display, or undefined if loading.
   * When undefined, a loading spinner is shown.
   */
  readonly data: HeatmapData | undefined;

  /**
   * Height of the chart in pixels. Defaults to 300.
   */
  readonly height?: number;

  /**
   * Label for the X axis.
   */
  readonly xAxisLabel?: string;

  /**
   * Label for the Y axis.
   */
  readonly yAxisLabel?: string;

  /**
   * Fill parent container. Defaults to false.
   */
  readonly fillParent?: boolean;

  /**
   * Custom class name for the container.
   */
  readonly className?: string;

  /**
   * Format function for cell values.
   */
  readonly formatValue?: (value: number) => string;

  /**
   * Callback when brush selection completes (on mouseup).
   * Called with the x and y labels of all cells in the brushed rectangle.
   */
  readonly onBrush?: (selection: {
    xLabels: string[];
    yLabels: string[];
  }) => void;

  /**
   * Selection to highlight on the chart. Cells at the intersection of
   * the selected xLabels and yLabels are drawn with a highlight color.
   * The consumer controls this state — typically by feeding the
   * `onBrush` output back in.
   */
  readonly selection?: {
    readonly xLabels: ReadonlyArray<string>;
    readonly yLabels: ReadonlyArray<string>;
  };
}

export class HeatmapChart implements m.ClassComponent<HeatmapAttrs> {
  view({attrs}: m.CVnode<HeatmapAttrs>) {
    const {data, height = 300, fillParent, className, onBrush} = attrs;

    const isEmpty =
      data !== undefined &&
      (data.values.length === 0 ||
        data.xLabels.length === 0 ||
        data.yLabels.length === 0);
    const option =
      data !== undefined && !isEmpty
        ? buildHeatmapOption(attrs, data)
        : undefined;

    return m(EChartView, {
      option,
      height,
      fillParent,
      className,
      empty: isEmpty,
      eventHandlers: buildHeatmapEventHandlers(attrs, data),
      activeBrushType: onBrush !== undefined ? 'rect' : undefined,
    });
  }
}

function buildHeatmapOption(
  attrs: HeatmapAttrs,
  data: HeatmapData,
): EChartsCoreOption {
  const {xAxisLabel, yAxisLabel, formatValue} = attrs;
  const fmtVal = formatValue ?? formatNumber;

  const option: Record<string, unknown> = {
    animation: false,
    grid: buildGridOption({right: 80}),
    xAxis: {
      ...buildAxisOption(
        {
          type: 'category',
          data: [...data.xLabels],
          name: xAxisLabel,
          labelOverflow: 'truncate',
          labelWidth: 60,
        },
        true,
      ),
      splitArea: {show: true},
    },
    yAxis: {
      ...buildAxisOption(
        {
          type: 'category',
          data: [...data.yLabels],
          name: yAxisLabel,
          labelOverflow: 'truncate',
          labelWidth: 80,
        },
        false,
      ),
      splitArea: {show: true},
    },
    tooltip: buildTooltipOption({
      trigger: 'item',
      formatter: (params: {
        value?: [number, number, number];
        name?: string;
      }) => {
        const v = params.value;
        if (v === undefined) return '';
        const xLabel = data.xLabels[v[0]] ?? '';
        const yLabel = data.yLabels[v[1]] ?? '';
        return [`${xLabel} / ${yLabel}`, `Value: ${fmtVal(v[2])}`].join('<br>');
      },
    }),
    visualMap: {
      min: data.min,
      max: data.max,
      calculable: true,
      orient: 'vertical',
      right: 0,
      top: 'center',
      textStyle: {fontSize: 10},
    },
    series: [
      {
        type: 'heatmap',
        data: buildHeatmapSeriesData(data, attrs.selection),
        label: {show: false},
        emphasis: {
          itemStyle: {
            shadowBlur: 10,
            shadowColor: 'rgba(0, 0, 0, 0.5)',
          },
        },
      },
    ],
  };

  if (attrs.onBrush) {
    option.brush = buildBrushOption({
      xAxisIndex: 0,
      yAxisIndex: 0,
      brushType: 'rect',
    });
    option.toolbox = {show: false};
  }

  return option as EChartsCoreOption;
}

function buildHeatmapSeriesData(
  data: HeatmapData,
  selection: HeatmapAttrs['selection'],
): Array<Record<string, unknown>> {
  const xSet = selection !== undefined ? new Set(selection.xLabels) : undefined;
  const ySet = selection !== undefined ? new Set(selection.yLabels) : undefined;

  return data.values.map((triple) => {
    const [xIdx, yIdx, value] = triple;
    const item: Record<string, unknown> = {value: [xIdx, yIdx, value]};
    if (xSet !== undefined && ySet !== undefined) {
      const xLabel = data.xLabels[xIdx];
      const yLabel = data.yLabels[yIdx];
      if (
        xLabel !== undefined &&
        yLabel !== undefined &&
        xSet.has(xLabel) &&
        ySet.has(yLabel)
      ) {
        item.itemStyle = {
          color: SELECTION_COLOR,
          borderColor: 'rgba(0, 120, 212, 0.6)',
          borderWidth: 2,
        };
      }
    }
    return item;
  });
}

function buildHeatmapEventHandlers(
  attrs: HeatmapAttrs,
  data: HeatmapData | undefined,
): ReadonlyArray<EChartEventHandler> {
  if (
    !attrs.onBrush ||
    data === undefined ||
    data.xLabels.length === 0 ||
    data.yLabels.length === 0
  ) {
    return [];
  }
  const onBrush = attrs.onBrush;
  const xLabels = data.xLabels;
  const yLabels = data.yLabels;

  return [
    {
      eventName: 'brushEnd',
      handler: (params) => {
        const range = extractBrushRect(params);
        if (range !== undefined) {
          const xMin = Math.max(0, Math.round(range.xMin));
          const xMax = Math.min(xLabels.length - 1, Math.round(range.xMax));
          const yMin = Math.max(0, Math.round(range.yMin));
          const yMax = Math.min(yLabels.length - 1, Math.round(range.yMax));
          if (xMin <= xMax && yMin <= yMax) {
            const selectedX: string[] = [];
            for (let i = xMin; i <= xMax; i++) {
              selectedX.push(xLabels[i]);
            }
            const selectedY: string[] = [];
            for (let i = yMin; i <= yMax; i++) {
              selectedY.push(yLabels[i]);
            }
            if (selectedX.length > 0 && selectedY.length > 0) {
              onBrush({xLabels: selectedX, yLabels: selectedY});
            }
          }
        }
      },
    },
  ];
}
