// 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 {extractBrushRange, formatNumber} from './chart_utils';
import type {HistogramBucket, HistogramData} from './histogram_loader';
import {EChartView, type EChartEventHandler} from './echart_view';
import {buildChartOption, SELECTION_COLOR} from './chart_option_builder';
import type {ChartThemeColors} from './chart_theme';

const PALETTE_INDEX_KEY = '__pfPaletteIdx';

function resolveBucketColors(
  option: EChartsCoreOption,
  colors: ChartThemeColors,
): EChartsCoreOption {
  const palette = colors.chartColors;
  if (palette.length === 0) return option;
  const series = (option as {series?: ReadonlyArray<Record<string, unknown>>})
    .series;
  if (series === undefined) return option;
  for (const s of series) {
    const data = s.data as
      | ReadonlyArray<{itemStyle?: Record<string, unknown>}>
      | undefined;
    if (data === undefined) continue;
    for (const item of data) {
      const style = item.itemStyle;
      if (style === undefined) continue;
      const idx = style[PALETTE_INDEX_KEY];
      if (typeof idx === 'number') {
        style.color =
          palette[((idx % palette.length) + palette.length) % palette.length];
        delete style[PALETTE_INDEX_KEY];
      }
    }
  }
  return option;
}

export interface HistogramAttrs {
  /**
   * Histogram data to display, or undefined if loading.
   * When undefined, a loading spinner is shown.
   * Use the computeHistogram() utility function to compute this from raw values.
   */
  readonly data: HistogramData | undefined;

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

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

  /**
   * Label for the Y axis. Defaults to 'Count'.
   */
  readonly yAxisLabel?: string;

  /**
   * Callback when brush selection completes (on mouseup).
   * Called with the selected range based on mousedown and mouseup positions.
   */
  readonly onBrush?: (range: {start: number; end: number}) => void;

  /**
   * Selection range to highlight on the chart. Buckets overlapping this
   * range are drawn with a highlight color. The consumer controls this
   * state — typically by feeding the `onBrush` output back in.
   */
  readonly selection?: {readonly start: number; readonly end: number};

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

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

  /**
   * Format function for X axis tick values.
   */
  readonly formatXValue?: (value: number) => string;

  /**
   * Format function for Y axis tick values.
   */
  readonly formatYValue?: (value: number) => string;

  /**
   * Bar color. Defaults to theme primary color.
   */
  readonly barColor?: string;

  /**
   * Per-bucket color override. Called once per bucket; return an index into
   * the theme's chart color palette (`chartColors`) to colour that bar, or
   * undefined to fall back to `barColor`. The index is taken modulo the
   * palette length, so callers don't need to know how many colors exist.
   * The NULL bar (when present) is passed `bucket = undefined`. Selection
   * highlighting still takes precedence over the returned color.
   */
  readonly bucketColor?: (
    bucket: HistogramBucket | undefined,
    index: number,
  ) => number | undefined;

  /**
   * Bar hover color. Defaults to theme accent color.
   */
  readonly barHoverColor?: string;

  /**
   * Use logarithmic scale for Y axis. Useful when count values
   * span multiple orders of magnitude. Defaults to false.
   */
  readonly logScale?: boolean;

  /**
   * When true, X axis (dimension) ticks will be snapped to integer values.
   * Use when the histogram data represents integer-valued quantities.
   * The Y axis (measure) always uses integer ticks since it shows counts.
   */
  readonly integerDimension?: boolean;
}

export class Histogram implements m.ClassComponent<HistogramAttrs> {
  view({attrs}: m.Vnode<HistogramAttrs>) {
    const {data, height, fillParent, className, onBrush} = attrs;

    const nullCount = data?.nullCount ?? 0;
    const hasData =
      data !== undefined && (data.buckets.length > 0 || nullCount > 0);
    const option = hasData ? buildOption(attrs, data) : undefined;

    return m(EChartView, {
      option,
      height,
      fillParent,
      className,
      empty: data !== undefined && !hasData,
      eventHandlers: buildEventHandlers(attrs, data),
      activeBrushType: onBrush !== undefined ? 'lineX' : undefined,
      resolveOption: resolveBucketColors,
    });
  }
}

function buildOption(
  attrs: HistogramAttrs,
  data: HistogramData,
): EChartsCoreOption {
  const {
    xAxisLabel,
    yAxisLabel = 'Count',
    formatXValue = (v: number) => formatNumber(v),
    formatYValue,
    barColor,
    barHoverColor,
    logScale = false,
  } = attrs;
  const fmtY = formatYValue ?? formatNumber;
  const nullCount = data.nullCount ?? 0;
  const totalWithNull = data.totalCount + nullCount;

  const categories = data.buckets.map((b) => formatXValue(b.start));
  const seriesData: number[] = data.buckets.map((b) => b.count);
  const bucketCount = data.buckets.length;

  // Append NULL bar when there are null values
  if (nullCount > 0) {
    categories.push('NULL');
    seriesData.push(nullCount);
  }

  const option = buildChartOption({
    xAxis: {
      type: 'category',
      data: categories,
      name: xAxisLabel,
    },
    yAxis: {
      type: logScale ? 'log' : 'value',
      name: yAxisLabel,
      formatter:
        formatYValue !== undefined
          ? (v) => formatYValue(v as number)
          : undefined,
      minInterval: 1,
    },
    tooltip: {
      trigger: 'axis' as const,
      axisPointer: {type: 'shadow' as const},
      formatter: (params: Array<{dataIndex?: number}>) => {
        const p = Array.isArray(params) ? params[0] : params;
        const idx = p?.dataIndex;
        if (idx === undefined || idx < 0 || idx >= categories.length) {
          return '';
        }

        // NULL bar
        if (idx >= bucketCount) {
          const pct =
            totalWithNull > 0
              ? ((nullCount / totalWithNull) * 100).toFixed(1)
              : '0';
          return [`NULL`, `Count: ${fmtY(nullCount)}`, `${pct}%`].join('<br>');
        }

        // Regular bucket
        const bucket = data.buckets[idx];
        const pct =
          totalWithNull > 0
            ? ((bucket.count / totalWithNull) * 100).toFixed(1)
            : '0';
        return [
          `Range: ${formatXValue(bucket.start)} - ${formatXValue(bucket.end)}`,
          `Count: ${fmtY(bucket.count)}`,
          `${pct}%`,
        ].join('<br>');
      },
    },
    brush: attrs.onBrush ? {xAxisIndex: 0, brushType: 'lineX'} : undefined,
  });

  // Build per-bar styles, highlighting buckets that overlap the selection.
  const sel = attrs.selection;
  const {bucketColor} = attrs;
  const styledData = seriesData.map((count, idx) => {
    const item: Record<string, unknown> = {value: count};
    const bucket = idx < data.buckets.length ? data.buckets[idx] : undefined;
    const selected =
      sel !== undefined &&
      bucket !== undefined &&
      bucket.end > sel.start &&
      bucket.start < sel.end;
    if (selected) {
      item.itemStyle = {color: SELECTION_COLOR};
    } else {
      const paletteIdx = bucketColor?.(bucket, idx);
      if (paletteIdx !== undefined) {
        // Stashed here as a sentinel; resolveBucketColors swaps it for the
        // real color once the theme palette is known at render time.
        item.itemStyle = {[PALETTE_INDEX_KEY]: paletteIdx};
      }
    }
    return item;
  });

  // Add series on top of the base option
  (option as Record<string, unknown>).series = [
    {
      type: 'bar',
      data: styledData,
      barWidth: '100%',
      barCategoryGap: '0%',
      itemStyle: barColor !== undefined ? {color: barColor} : undefined,
      emphasis:
        barHoverColor !== undefined
          ? {itemStyle: {color: barHoverColor}}
          : undefined,
    },
  ];

  return option;
}

function buildEventHandlers(
  attrs: HistogramAttrs,
  data: HistogramData | undefined,
): ReadonlyArray<EChartEventHandler> {
  if (!attrs.onBrush || data === undefined || data.buckets.length === 0) {
    return [];
  }
  const onBrush = attrs.onBrush;
  const buckets = data.buckets;

  return [
    {
      eventName: 'brushEnd',
      handler: (params) => {
        // For category axes, coordRange returns category indices
        const range = extractBrushRange(params);
        if (range !== undefined) {
          const [startIdx, endIdx] = range;
          const minIdx = Math.max(0, startIdx);
          const maxIdx = Math.min(buckets.length - 1, endIdx);
          if (minIdx <= maxIdx && minIdx < buckets.length) {
            onBrush({
              start: buckets[minIdx].start,
              end: buckets[maxIdx].end,
            });
          }
        }
      },
    },
  ];
}
