// 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 {EChartView, type EChartEventHandler} from './echart_view';
import type {LegendPosition} from './common';
import {
  buildChartOption,
  buildLegendOption,
  buildSelectionMarkArea,
} from './chart_option_builder';

/**
 * A single data point in a line chart series.
 */
export interface LineChartPoint {
  /** X-axis value (typically time or sequential index) */
  readonly x: number;
  /** Y-axis value */
  readonly y: number;
}

/**
 * A single series (line) in the chart.
 */
export interface LineChartSeries {
  /** Display name for this series (shown in legend) */
  readonly name: string;
  /** Data points for this series, sorted by x value */
  readonly points: readonly LineChartPoint[];
  /** Optional custom color for this series */
  readonly color?: string;
}

/**
 * Data provided to a LineChart.
 */
export interface LineChartData {
  /** The series to display */
  readonly series: readonly LineChartSeries[];
}

export interface LineChartAttrs {
  /**
   * Line chart data to display, or undefined if loading.
   * When undefined, a loading spinner is shown.
   */
  readonly data: LineChartData | undefined;

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

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

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

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

  /**
   * Selection range to highlight on the chart. When provided, a shaded
   * region is drawn over the specified X range. The consumer controls this
   * state — typically by feeding the `onBrush` output back in.
   */
  readonly selection?: {readonly start: number; readonly end: number};

  /**
   * Vertical markers drawn at specific X values. Each marker renders as a
   * thin vertical line spanning the plot area with a small dot at the top.
   * Useful for annotating point-in-time events (e.g. LMK kills) on top of
   * a time series.
   */
  readonly markers?: ReadonlyArray<{
    readonly x: number;
    readonly color?: string;
    readonly label?: string;
  }>;

  /**
   * 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;

  /**
   * Use logarithmic scale for Y axis. Defaults to false.
   */
  readonly logScale?: boolean;

  /**
   * When true, X axis ticks will be snapped to integer values.
   */
  readonly integerX?: boolean;

  /**
   * When true, Y axis ticks will be snapped to integer values.
   */
  readonly integerY?: boolean;

  /**
   * Minimum interval between Y axis ticks. Use this to align ticks with a
   * display unit — e.g. pass 1024 so ticks land on whole MB boundaries when
   * data is in KB.
   */
  readonly yAxisMinInterval?: number;

  /**
   * Show legend. Defaults to true when multiple series.
   */
  readonly showLegend?: boolean;

  /**
   * Where the legend sits relative to the chart. Defaults to 'top'.
   */
  readonly legendPosition?: LegendPosition;

  /**
   * Show data points as circles. Defaults to true.
   */
  readonly showPoints?: boolean;

  /**
   * Line width in pixels. Defaults to 2.
   */
  readonly lineWidth?: number;

  /**
   * Explicit minimum value for X axis. When set, the axis starts at this value.
   */
  readonly xAxisMin?: number;

  /**
   * Explicit maximum value for X axis. When set, the axis ends at this value.
   */
  readonly xAxisMax?: number;

  /**
   * When true, axis ranges are computed from data min/max instead of
   * always including zero. Defaults to false.
   */
  readonly scaleAxes?: boolean;

  /**
   * Show grid lines. 'horizontal' draws lines parallel to the X axis,
   * 'vertical' draws lines parallel to the Y axis, 'both' shows both.
   * Defaults to no grid lines.
   */
  readonly gridLines?: 'horizontal' | 'vertical' | 'both';

  /**
   * When true, series are stacked and shown as filled areas.
   * The total height is the sum of all series values. Defaults to false.
   * Note: When stacked, all series must be aligned to the same X values.
   */
  readonly stacked?: boolean;

  /**
   * Callback when a series is clicked. Called with the series name.
   */
  readonly onSeriesClick?: (seriesName: string) => void;
}

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

    const isEmpty =
      data !== undefined &&
      (data.series.length === 0 ||
        data.series.every((s) => s.points.length === 0));
    const option =
      data !== undefined && !isEmpty ? buildLineOption(attrs, data) : undefined;

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

function buildLineOption(
  attrs: LineChartAttrs,
  data: LineChartData,
): EChartsCoreOption {
  const {
    xAxisLabel,
    yAxisLabel,
    formatXValue,
    formatYValue,
    logScale = false,
    integerX = false,
    integerY = false,
    yAxisMinInterval,
    showLegend,
    showPoints = true,
    lineWidth = 2,
    gridLines,
    stacked = false,
  } = attrs;
  const fmtX = formatXValue ?? formatNumber;
  const fmtY = formatYValue ?? formatNumber;

  const displayLegend = showLegend ?? data.series.length > 1;

  const series = data.series.map((s, i) => {
    const base: Record<string, unknown> = {
      type: 'line',
      name: s.name,
      data: s.points.map((p) => [p.x, p.y]),
      lineStyle:
        s.color !== undefined
          ? {width: lineWidth, color: s.color}
          : {width: lineWidth},
      itemStyle: s.color !== undefined ? {color: s.color} : undefined,
      showSymbol: showPoints,
      symbolSize: 6,
      triggerLineEvent: true,
      emphasis: {focus: 'series', itemStyle: {borderWidth: 2}},
      stack: stacked ? 'total' : undefined,
      areaStyle: stacked ? {} : undefined,
    };

    // Render selection highlight on the first series only.
    if (i === 0 && attrs.selection !== undefined) {
      base.markArea = buildSelectionMarkArea([
        [{xAxis: attrs.selection.start}, {xAxis: attrs.selection.end}],
      ]);
    }
    return base;
  });

  const option = buildChartOption({
    xAxis: {
      // Nasty ECharts quirk: when stacking, the xAxis must be type 'category'
      // or 'time'. Since we want to support x-values at irregular intervals, we
      // use 'time' type which allows numeric timestamps, and override the label
      // formatter to show numbers.
      type: stacked ? 'time' : 'value',
      name: xAxisLabel,
      formatter: (v) => fmtX(v as number),
      minInterval: integerX ? 1 : undefined,
      min: attrs.xAxisMin,
      max: attrs.xAxisMax,
      scale: attrs.scaleAxes,
      showSplitLine: gridLines === 'vertical' || gridLines === 'both',
    },
    yAxis: {
      type: logScale ? 'log' : 'value',
      name: yAxisLabel,
      formatter:
        formatYValue !== undefined
          ? (v) => formatYValue(v as number)
          : undefined,
      minInterval: yAxisMinInterval ?? (integerY ? 1 : undefined),
      scale: attrs.scaleAxes,
      showSplitLine: gridLines === 'horizontal' || gridLines === 'both',
    },
    tooltip: {
      trigger: 'axis' as const,
      formatter: (
        params: Array<{
          seriesName?: string;
          data?: [number, number];
          marker?: string;
        }>,
      ) => {
        if (!Array.isArray(params) || params.length === 0) return '';
        const xVal = params[0].data?.[0];
        const header = xVal !== undefined ? `X: ${fmtX(xVal)}` : '';
        const lines = params.map(
          (p) =>
            `${p.marker ?? ''} ${p.seriesName ?? ''}: ${fmtY(p.data?.[1] ?? 0)}`,
        );
        return [header, ...lines].join('<br>');
      },
    },
    brush: attrs.onBrush ? {xAxisIndex: 0, brushType: 'lineX'} : undefined,
    legend: displayLegend
      ? buildLegendOption(attrs.legendPosition)
      : {show: false},
  });

  (option as Record<string, unknown>).series = series;
  return option;
}

function buildLineEventHandlers(
  attrs: LineChartAttrs,
  data: LineChartData | undefined,
): ReadonlyArray<EChartEventHandler> {
  const handlers: EChartEventHandler[] = [];

  if (
    attrs.onBrush &&
    data !== undefined &&
    data.series.length > 0 &&
    data.series.some((s) => s.points.length > 0)
  ) {
    const onBrush = attrs.onBrush;
    handlers.push({
      eventName: 'brushEnd',
      handler: (params) => {
        const range = extractBrushRange(params);
        if (range !== undefined) {
          const [start, end] = range;
          onBrush({start, end});
        }
      },
    });
  }

  if (attrs.onSeriesClick) {
    const onSeriesClick = attrs.onSeriesClick;
    handlers.push({
      eventName: 'click',
      handler: (params) => {
        const p = params as {seriesName?: string};
        if (p.seriesName !== undefined) {
          onSeriesClick(p.seriesName);
        }
      },
    });
  }

  return handlers;
}
