import { Component, Input, SimpleChanges } from '@angular/core';
import * as d3 from 'd3';

export interface IRadar {
  axis: string;
  value: number;
}

export interface IRadarData {
  data: IRadar[];
  label: string;
  color: string;
}

export type RadarDataList = IRadarData[];

export interface RadarChartOptions {
  w: number;
  h: number;
  margin: { top: number; right: number; bottom: number; left: number };
  maxValue: number;
  levels: number;
  labelFactor: number;
  opacityArea: number;
  dotRadius: number;
  opacityCircles: number;
  strokeWidth: number;
  roundStrokes: boolean;
  roundTension: number;
  animationDuration: number;
  color: d3.ScaleOrdinal<string, string>;
}

@Component({
  selector: 'radar-chart',
  standalone: true,
  imports: [],
  templateUrl: './radar-chart.component.html',
  styleUrls: ['./radar-chart.component.less'],
})
export class RadarChartComponent {
  width = 0;
  height = 0;

  @Input() radarChartOptions: Partial<RadarChartOptions> = {
    maxValue: 250,
    levels: 3,
    labelFactor: 1.25,
    opacityArea: 0.1,
    dotRadius: 4,
    opacityCircles: 0.1,
    strokeWidth: 3,
    roundStrokes: true,
    roundTension: 0.6,
  };

  @Input() data: RadarDataList = [
    {
      data: [
        { axis: 'Java', value: 180 },
        { axis: 'Python', value: 220 },
        { axis: 'JavaScript', value: 210 },
        { axis: 'Web', value: 150 },
        { axis: 'Bash', value: 100 },
        { axis: 'C++', value: 100 },
      ],
      label: 'Skills',
      color: '#E48DDA',
    },
    {
      data: [
        { axis: 'Java', value: 180 },
        { axis: 'Python', value: 180 },
        { axis: 'JavaScript', value: 180 },
        { axis: 'Web', value: 180 },
        { axis: 'Bash', value: 180 },
        { axis: 'C++', value: 180 },
      ],
      label: 'Progress',
      color: '#2953E8',
    },
  ];

  constructor() {
    this.setRect();
    this.setOptions();
  }

  ngAfterViewInit() {
    this.renderRadarChart('.radarChart', this.data, this.radarChartOptions);
  }

  ngOnChanges(changes: SimpleChanges) {
    if (changes['data']) {
      this.renderRadarChart('.radarChart', this.data, this.radarChartOptions);
    }
  }

  private setRect() {
    this.width = 305;
    this.height = 305;
  }

  private setOptions() {
    this.radarChartOptions = {
      ...this.radarChartOptions,
      w: this.width,
      h: this.height,
    };
  }

  private calcParams(cfg: RadarChartOptions, data: RadarDataList) {
    const maxValue = Math.max(cfg.maxValue, d3.max(data, i => d3.max(i.data.map(o => o.value!))) as number);
    const allAxis = data[0].data.map(i => i.axis);
    const total = allAxis.length;
    const offsetSliceLength = total % 2 === 0 ? 0.5 : 0;
    const radius = Math.min(cfg.w / 2, cfg.h / 2);
    const angleSlice = (Math.PI * 2) / total;
    const rScale = d3.scaleLinear().range([0, radius]).domain([0, maxValue]);

    return { radius, total, allAxis, angleSlice, offsetSliceLength, rScale, maxValue };
  }

  private renderRadarChart(id: string, data: RadarDataList, options?: Partial<RadarChartOptions>) {
    const cfg: RadarChartOptions = {
      w: this.width,
      h: this.height,
      margin: { top: 20, right: 20, bottom: 20, left: 20 },
      levels: 3,
      maxValue: 0,
      labelFactor: 1.25,
      opacityArea: 0.15,
      dotRadius: 4,
      opacityCircles: 0.05,
      strokeWidth: 2,
      roundStrokes: true,
      roundTension: 0.6,
      animationDuration: 1000,
      color: d3.scaleOrdinal(data.map(i => i.color)),
    };

    if (options) {
      Object.assign(cfg, options);
    }

    d3.select(id).select('svg').remove();
    if (!data.length) return;

    const { allAxis } = this.calcParams(cfg, data);
    const svg = this.initSvg('.radarChart');
    const g = this.appendWrapperG(svg, allAxis.length);
    this.appendFilter(g);
    this.appendAxisGrid(g, data, cfg);
    const radarLine = this.getRadarLine(cfg);
    const zeroRadarLine = this.getRadarLine(cfg, true);

    const blobWrapper = g.selectAll('.radarWrapper').data(data).enter().append('g').attr('class', 'radarWrapper');

    blobWrapper
      .append('path')
      .attr('class', 'radarArea')
      .attr('d', d => zeroRadarLine(d.data))
      .style('fill', (_, i) => cfg.color(i.toString()))
      .style('fill-opacity', cfg.opacityArea)
      .on('mouseover', function () {
        d3.selectAll('.radarArea').transition().duration(200).style('fill-opacity', 0.1);
        d3.select(this).transition().duration(200).style('fill-opacity', 0.7);
      })
      .on('mouseout', () => {
        d3.selectAll('.radarArea').transition().duration(200).style('fill-opacity', cfg.opacityArea);
      })
      .transition()
      .duration(cfg.animationDuration)
      .attr('d', d => radarLine(d.data));

    blobWrapper
      .append('path')
      .attr('class', 'radarStroke')
      .attr('d', d => zeroRadarLine(d.data))
      .transition()
      .duration(cfg.animationDuration)
      .attr('d', d => radarLine(d.data))
      .style('stroke-width', `${cfg.strokeWidth}px`)
      .style('stroke', (_, i) => cfg.color(i.toString()))
      .style('fill', 'none');
  }

  private initSvg(queryId: string) {
    return d3
      .select(queryId)
      .append('svg')
      .attr('width', '100%')
      .attr('height', '100%')
      .attr('viewBox', `0 0 ${this.size} ${this.size}`)
      .attr('preserveAspectRatio', 'xMinYMin')
      .attr('class', `radar${queryId}`);
  }

  private appendWrapperG(svg: d3.Selection<SVGSVGElement, unknown, HTMLElement, any>, allAxis: number) {
    return svg.append('g').attr('transform', `translate(${this.size / 2}, ${this.size / 2})`);
  }

  private appendFilter(g: d3.Selection<SVGGElement, unknown, HTMLElement, any>) {
    const defs = g.append('defs');
    const feGaussianBlur = defs.append('filter').attr('id', 'glow');
    feGaussianBlur.append('feGaussianBlur').attr('stdDeviation', '2.5').attr('result', 'coloredBlur');
    const feMerge = feGaussianBlur.append('feMerge');
    feMerge.append('feMergeNode').attr('in', 'coloredBlur');
    feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
  }

  private appendAxisGrid(
    g: d3.Selection<SVGGElement, unknown, HTMLElement, any>,
    data: RadarDataList,
    cfg: RadarChartOptions
  ) {
    const { allAxis } = this.calcParams(cfg, data);
    const axisGrid = g.append('g').attr('class', 'axisWrapper');

    const axis = axisGrid.selectAll('.axis').data(allAxis).enter().append('g').attr('class', 'axis');
    this.renderSeparatorLine(axis, cfg);
    this.renderLabelText(axis, cfg);
    this.renderRadarNumber(axis, cfg, data);
  }

  private getRadarLine(cfg: RadarChartOptions, zero?: true) {
    const { angleSlice, rScale, offsetSliceLength } = this.calcParams(cfg, this.data);
    const radarLine = d3
      .lineRadial<IRadar>()
      .curve(d3.curveLinearClosed)
      .radius(d => rScale(zero ? 0 : d.value))
      .angle((_, i) => angleSlice * (i + offsetSliceLength));

    if (cfg.roundStrokes) {
      radarLine.curve(d3.curveCardinalClosed.tension(cfg.roundTension));
    }
    return radarLine;
  }

  private renderSeparatorLine(axis: d3.Selection<SVGGElement, any, SVGGElement, unknown>, cfg: RadarChartOptions) {
    const { angleSlice, rScale, maxValue, offsetSliceLength } = this.calcParams(cfg, this.data);
    axis
      .append('line')
      .attr('x1', 0)
      .attr('y1', 0)
      .attr('x2', (_, i) => rScale(maxValue) * Math.cos(angleSlice * (i + offsetSliceLength) - Math.PI / 2))
      .attr('y2', (_, i) => rScale(maxValue) * Math.sin(angleSlice * (i + offsetSliceLength) - Math.PI / 2))
      .attr('class', 'line')
      .style('stroke', 'rgba(151, 151, 151, 0.17)')
      .style('stroke-width', '1px');
  }

  private renderLabelText(axis: d3.Selection<SVGGElement, any, SVGGElement, unknown>, cfg: RadarChartOptions) {
    const { angleSlice, rScale, maxValue, offsetSliceLength } = this.calcParams(cfg, this.data);
    axis
      .append('text')
      .attr('class', 'legend')
      .style('font-size', '17px')
      .attr('text-anchor', 'middle')
      .attr('fill', '#5A607F')
      .attr('dy', '0.3em')
      .attr(
        'x',
        (d, i) => rScale(maxValue * cfg.labelFactor) * Math.cos(angleSlice * (i + offsetSliceLength) - Math.PI / 2)
      )
      .attr(
        'y',
        (_, i) => rScale(maxValue * cfg.labelFactor) * Math.sin(angleSlice * (i + offsetSliceLength) - Math.PI / 2)
      )
      .text(d => d + ':');
  }

  private renderRadarNumber(
    axis: d3.Selection<SVGGElement, any, SVGGElement, unknown>,
    cfg: RadarChartOptions,
    data: RadarDataList
  ) {
    const { angleSlice, rScale, maxValue, offsetSliceLength } = this.calcParams(cfg, this.data);
    axis
      .append('text')
      .attr('class', 'legend')
      .style('font-size', '17px')
      .attr('text-anchor', 'middle')
      .attr('fill', '#5A607F')
      .attr('dy', '0.3em')
      .attr(
        'x',
        (d, i) => rScale(maxValue * cfg.labelFactor) * Math.cos(angleSlice * (i + offsetSliceLength) - Math.PI / 2)
      )
      .attr(
        'y',
        (_, i) => rScale(maxValue * cfg.labelFactor) * Math.sin(angleSlice * (i + offsetSliceLength) - Math.PI / 2) + 21
      )
      .text((_, i) => `${data[0].data[i].value} /${maxValue}`)
      .call(this.colorWrap, cfg);
  }

  private colorWrap(text: d3.Selection<SVGTextElement, any, SVGGElement, unknown>, cfg: RadarChartOptions) {
    text.each(function () {
      const text = d3.select(this);
      const [cur, total] = text.text().split(' ');
      const y = text.attr('y');
      const x = text.attr('x');
      const actX = Number(x) - 14;
      const dy = parseFloat(text.attr('dy'));

      let tspanCur = text
        .text(null)
        .append('tspan')
        .attr('x', actX)
        .attr('y', y)
        .attr('dy', dy + 'em')
        .attr('fill', cfg.color('0'))
        .text(cur);

      const { width } = tspanCur.node()!.getBoundingClientRect();
      text
        .append('tspan')
        .attr('x', actX)
        .attr('y', y)
        .attr('dx', width + 5 * (4 - cur.length + 0.5))
        .attr('dy', dy + 'em')
        .attr('fill', '#5A607F')
        .text(total);
    });
  }

  get dim() {
    return Math.min(this.width, this.height);
  }

  get size() {
    return this.dim + this.dim / 2;
  }

  get hide() {
    return this.data.length === 0;
  }
}
