import { identity, isArray, last } from '@antv/util';
import { Continuous } from './continuous';
import { LinearOptions, Transform } from '../types';
import { Base } from './base';
import { createInterpolateValue } from '../utils';
import { d3Ticks } from '../tick-methods/d3-ticks';
import { d3LinearNice } from '../utils/d3-linear-nice';

/**
 * Linear 比例尺
 *
 * 构造可创建一个在输入和输出之间具有线性关系的比例尺
 */
export class Linear extends Continuous<LinearOptions> {
  protected getDefaultOptions(): LinearOptions {
    return {
      domain: [0, 1],
      range: [0, 1],
      unknown: undefined,
      nice: false,
      clamp: false,
      round: false,
      interpolate: createInterpolateValue,
      tickMethod: d3Ticks,
      tickCount: 5,
    };
  }

  protected removeUnsortedValues(breaksDomain: number[], breaksRange: number[], reverse: boolean) {
    let pre = -Infinity;
    const deleteIndices = breaksRange.reduce((acc, current, i) => {
      if (i === 0) return acc;
      const value = pre > 0 ? pre : current;
      if (pre > 0 && (reverse ? current > pre : current < pre)) {
        acc.push(i);
      } else {
        const diff = (value - breaksRange[i - 1]) * (reverse ? -1 : 1);
        if (diff < 0) {
          if (pre < 0) pre = breaksRange[i - 1];
          acc.push(i);
        } else {
          pre = -Infinity;
        }
      }
      return acc;
    }, [] as number[]);

    deleteIndices
      .slice()
      .reverse()
      .forEach((index) => {
        breaksDomain.splice(index, 1);
        breaksRange.splice(index, 1);
      });

    return { breaksDomain, breaksRange };
  }

  protected transformDomain(options: LinearOptions): { breaksDomain: number[]; breaksRange: number[] } {
    const RANGE_LIMIT = [0.2, 0.8];
    const DEFAULT_GAP = 0.03;
    const { domain = [], range = [1, 0], breaks = [], tickCount = 5, nice } = options;
    const [min, max] = [Math.min(...domain), Math.max(...domain)];
    let niceDomainMin = min;
    let niceDomainMax = max;
    if (nice && breaks.length < 2) {
      const niceDomain = this.chooseNice()(min, max, tickCount) as number[];
      niceDomainMin = niceDomain[0];
      niceDomainMax = niceDomain[niceDomain.length - 1];
    }
    const domainMin = Math.min(niceDomainMin, min);
    let domainMax = Math.max(niceDomainMax, max);
    const sortedBreaks = breaks.filter(({ end }) => end < domainMax).sort((a, b) => a.start - b.start);
    const breaksDomain = d3Ticks(domainMin, domainMax, tickCount, sortedBreaks);
    if (last(breaksDomain) < domainMax) {
      const nicest = d3LinearNice(0, domainMax - last(breaksDomain), 3);
      breaksDomain.push(last(breaksDomain) + last(nicest));
      domainMax = last(breaksDomain);
    }
    const [r0, r1] = [range[0], last(range)] as number[];
    const diffDomain = domainMax - domainMin;
    const diffRange = Math.abs(r1 - r0);
    const reverse = r0 > r1;
    // Calculate the new range based on breaks.
    const breaksRange = breaksDomain.map((d) => {
      const ratio = (d - domainMin) / diffDomain;
      return reverse ? r0 - ratio * diffRange : r0 + ratio * diffRange;
    });
    // Compress the range scale according to breaks.
    const [MIN, MAX] = RANGE_LIMIT;
    sortedBreaks.forEach(({ start, end, gap = DEFAULT_GAP, compress = 'middle' }) => {
      const startIndex = breaksDomain.indexOf(start);
      const endIndex = breaksDomain.indexOf(end);
      let value = (breaksRange[startIndex] + breaksRange[endIndex]) / 2;
      if (compress === 'start') value = breaksRange[startIndex];
      if (compress === 'end') value = breaksRange[endIndex];
      const halfSpan = (gap * diffRange) / 2;
      // Calculate the new start and end values based on the center and scaled span.
      let startValue = reverse ? value + halfSpan : value - halfSpan;
      let endValue = reverse ? value - halfSpan : value + halfSpan;

      // Ensure the new start and end values are within the defined limits.
      if (startValue < MIN) {
        endValue += MIN - startValue;
        startValue = MIN;
      }
      if (endValue > MAX) {
        startValue -= endValue - MAX;
        endValue = MAX;
      }
      if (startValue > MAX) {
        endValue -= startValue - MAX;
        startValue = MAX;
      }
      if (endValue < MIN) {
        startValue += MIN - endValue;
        endValue = MIN;
      }

      breaksRange[startIndex] = startValue;
      breaksRange[endIndex] = endValue;
    });

    return this.removeUnsortedValues(breaksDomain, breaksRange, reverse);
  }

  protected transformBreaks(options: LinearOptions): LinearOptions {
    const { domain, breaks = [] } = options;
    if (!isArray(options.breaks)) return options;
    const domainMax = Math.max(...(domain as number[]));
    const filteredBreaks = breaks.filter(({ end }) => end < domainMax);
    const optWithFilteredBreaks = { ...options, breaks: filteredBreaks };
    const { breaksDomain, breaksRange } = this.transformDomain(optWithFilteredBreaks);
    return {
      ...options,
      domain: breaksDomain,
      range: breaksRange,
      breaks: filteredBreaks,
      tickMethod: () => [...breaksDomain],
    };
  }

  protected chooseTransforms(): Transform[] {
    return [identity, identity];
  }

  public clone(): Base<LinearOptions> {
    return new Linear(this.options);
  }
}
