import React, { PureComponent } from 'react';
import { kebabCase, groupBy, flatMap, uniqueId, values } from 'lodash';
import {
  AreaSeries,
  LinearGradient,
  LineSeries,
  XYChart,
  CrossHair,
  WithTooltip,
} from '@data-ui/xy-chart';
import { chartTheme } from '@data-ui/theme';
import { WithLegend, Margin, Dimension } from '@superset-ui/core';
import { createSelector } from 'reselect';
import { Dataset, PlainObject } from 'encodable';
import DefaultTooltipRenderer from './DefaultTooltipRenderer';
import createMarginSelector, { DEFAULT_MARGIN } from '../../utils/createMarginSelector';
import convertScaleToDataUIScale from '../../utils/convertScaleToDataUIScaleShape';
import createXYChartLayoutWithTheme from '../../utils/createXYChartLayoutWithTheme';
import createRenderLegend from '../legend/createRenderLegend';
import { LegendHooks } from '../legend/types';
import {
  lineEncoderFactory,
  LineEncoder,
  LineEncoding,
  LineEncodingConfig,
  LineChannelOutputs,
} from './Encoder';
import DefaultLegendItemMarkRenderer from './DefaultLegendItemMarkRenderer';

export interface TooltipProps {
  encoder: LineEncoder;
  allSeries: Series[];
  datum: SeriesValue;
  series: {
    [key: string]: SeriesValue;
  };
  theme: typeof chartTheme;
}

const defaultProps = {
  className: '',
  encoding: {},
  LegendItemMarkRenderer: DefaultLegendItemMarkRenderer,
  margin: DEFAULT_MARGIN,
  theme: chartTheme,
  TooltipRenderer: DefaultTooltipRenderer,
};

/** Part of formData that is needed for rendering logic in this file */
export type FormDataProps = {
  margin?: Margin;
  theme?: typeof chartTheme;
  encoding?: Partial<LineEncoding>;
};

export type HookProps = {
  TooltipRenderer?: React.ComponentType<TooltipProps>;
} & LegendHooks<LineEncodingConfig>;

type Props = {
  className?: string;
  width: string | number;
  height: string | number;
  data: Dataset;
} & HookProps &
  FormDataProps &
  Readonly<typeof defaultProps>;

export interface Series {
  key: string;
  fill: LineChannelOutputs['fill'];
  stroke: LineChannelOutputs['stroke'];
  strokeDasharray: LineChannelOutputs['strokeDasharray'];
  strokeWidth: LineChannelOutputs['strokeWidth'];
  values: SeriesValue[];
}

export interface SeriesValue {
  x: number | Date;
  y: number;
  data: PlainObject;
  parent: Series;
}

const CIRCLE_STYLE = { strokeWidth: 1.5 };

export default class LineChart extends PureComponent<Props> {
  private createEncoder = lineEncoderFactory.createSelector();

  private createAllSeries = createSelector(
    (input: { encoder: LineEncoder; data: Dataset }) => input.encoder,
    input => input.data,
    (encoder, data) => {
      const { channels } = encoder;
      const fieldNames = encoder.getGroupBys();

      const groups = groupBy(data, row => fieldNames.map(f => `${f}=${row[f]}`).join(','));

      const allSeries = values(groups).map(seriesData => {
        const firstDatum = seriesData[0];
        // eslint-disable-next-line @typescript-eslint/no-unsafe-return
        const key = fieldNames.map(f => firstDatum[f]).join(',');
        const series: Series = {
          key: key.length === 0 ? channels.y.getTitle() : key,
          fill: channels.fill.encodeDatum(firstDatum, false),
          stroke: channels.stroke.encodeDatum(firstDatum, '#222'),
          strokeDasharray: channels.strokeDasharray.encodeDatum(firstDatum, ''),
          strokeWidth: channels.strokeWidth.encodeDatum(firstDatum, 1),
          values: [],
        };

        series.values = seriesData
          .map(v => ({
            x: channels.x.getValueFromDatum<Date | number>(v),
            y: channels.y.getValueFromDatum<number>(v),
            data: v,
            parent: series,
          }))
          .sort((a: SeriesValue, b: SeriesValue) => {
            const aTime = a.x instanceof Date ? a.x.getTime() : a.x;
            const bTime = b.x instanceof Date ? b.x.getTime() : b.x;

            return aTime - bTime;
          });

        return series;
      });

      return allSeries;
    },
  );

  private createMargin = createMarginSelector();

  static defaultProps = defaultProps;

  // eslint-disable-next-line class-methods-use-this
  renderSeries(allSeries: Series[]) {
    const filledSeries = flatMap(
      allSeries
        .filter(({ fill }) => fill)
        .map(series => {
          const gradientId = uniqueId(kebabCase(`gradient-${series.key}`));

          return [
            <LinearGradient
              key={`${series.key}-gradient`}
              id={gradientId}
              from={series.stroke}
              to="#fff"
            />,
            <AreaSeries
              key={`${series.key}-fill`}
              seriesKey={series.key}
              data={series.values}
              interpolation="linear"
              fill={`url(#${gradientId})`}
              stroke={series.stroke}
              strokeWidth={series.strokeWidth}
            />,
          ];
        }),
    );

    const unfilledSeries = allSeries
      .filter(({ fill }) => !fill)
      .map(series => (
        <LineSeries
          key={series.key}
          seriesKey={series.key}
          interpolation="linear"
          data={series.values}
          stroke={series.stroke}
          strokeDasharray={series.strokeDasharray}
          strokeWidth={series.strokeWidth}
        />
      ));

    return filledSeries.concat(unfilledSeries);
  }

  renderChart = (dim: Dimension) => {
    const { width, height } = dim;
    const { data, margin, theme, TooltipRenderer, encoding } = this.props;

    const encoder = this.createEncoder(encoding);
    const { channels } = encoder;

    encoder.setDomainFromDataset(data);

    const allSeries = this.createAllSeries({ encoder, data });

    const layout = createXYChartLayoutWithTheme({
      width,
      height,
      margin: this.createMargin(margin),
      theme,
      xEncoder: channels.x,
      yEncoder: channels.y,
    });

    return layout.renderChartWithFrame((chartDim: Dimension) => (
      <WithTooltip
        renderTooltip={({
          datum,
          series,
        }: {
          datum: SeriesValue;
          series: {
            [key: string]: SeriesValue;
          };
        }) => (
          <TooltipRenderer
            encoder={encoder}
            allSeries={allSeries}
            datum={datum}
            series={series}
            theme={theme}
          />
        )}
      >
        {({
          onMouseLeave,
          onMouseMove,
          tooltipData,
        }: {
          onMouseLeave: (...args: unknown[]) => void;
          onMouseMove: (...args: unknown[]) => void;
          tooltipData: { datum: { y?: number } };
        }) => (
          <XYChart
            showYGrid
            snapTooltipToDataX
            width={chartDim.width}
            height={chartDim.height}
            ariaLabel="LineChart"
            eventTrigger="container"
            margin={layout.margin}
            renderTooltip={null}
            theme={theme}
            tooltipData={tooltipData}
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            xScale={convertScaleToDataUIScale(channels.x.definition.scale as any)}
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            yScale={convertScaleToDataUIScale(channels.y.definition.scale as any)}
            onMouseMove={onMouseMove}
            onMouseLeave={onMouseLeave}
          >
            {layout.renderXAxis()}
            {layout.renderYAxis()}
            {this.renderSeries(allSeries)}
            <CrossHair
              fullHeight
              showCircle
              showMultipleCircles
              strokeDasharray=""
              showHorizontalLine={false}
              circleFill={(d: SeriesValue) =>
                d.y === tooltipData.datum.y ? d.parent.stroke : '#fff'
              }
              circleSize={(d: SeriesValue) => (d.y === tooltipData.datum.y ? 6 : 4)}
              circleStroke={(d: SeriesValue) =>
                d.y === tooltipData.datum.y ? '#fff' : d.parent.stroke
              }
              circleStyles={CIRCLE_STYLE}
              stroke="#ccc"
            />
          </XYChart>
        )}
      </WithTooltip>
    ));
  };

  render() {
    const { className, data, width, height, encoding } = this.props;

    return (
      <WithLegend
        className={`superset-chart-line ${className}`}
        width={width}
        height={height}
        position="top"
        renderLegend={createRenderLegend(this.createEncoder(encoding), data, this.props)}
        renderChart={this.renderChart}
      />
    );
  }
}
