import {Component, Input, OnChanges, OnInit} from '@angular/core';
import {ChartConfiguration, ChartData, DefaultDataPoint} from "chart.js";
import {NgChartsModule} from "ng2-charts";
import {DistributionEntry, ErrorDistribution} from "../../../app/replays";
import {FormsModule} from "@angular/forms";
import {DecimalPipe, NgForOf} from "@angular/common";

@Component({
  selector: 'app-chart-hit-distribution',
  standalone: true,
  imports: [
    NgChartsModule,
    FormsModule,
    DecimalPipe,
    NgForOf
  ],
  templateUrl: './chart-hit-distribution.component.html',
  styleUrl: './chart-hit-distribution.component.css'
})
export class ChartHitDistributionComponent implements OnInit, OnChanges {

  @Input() errorDistribution!: ErrorDistribution;
  @Input() mods!: string[];

  removeOutliers = true;
  groupData = true;
  showPercentages = true;

  public barChartLegend = true;
  public barChartPlugins = [];

  public barChartOptions: ChartConfiguration<'bar'>['options'] = {
    responsive: true,
    scales: {
      x: {
        stacked: true,
      },
      y: {
        stacked: true
      }
    }
  };

  ngOnInit(): void {
  }

  ngOnChanges(): void {
    const showPercentages = this.showPercentages;
    this.barChartOptions = {
      responsive: true,
      //@ts-ignore
      animations: false,
      scales: {
        x: {
          stacked: true,
        },
        y: {
          stacked: true,
          ticks: {
            callback: function(value, index, ticks) {
              return showPercentages ? value + '%' : value;
            }
          }
        }
      }
    };
  }

  calculateStatistics(): Array<{ name: string, value: number }> {
    let { ys} = this.calculateData(this.errorDistribution);

    if(this.removeOutliers) {
      // Calculate IQR
      const percentiles = this.percentile(ys, [25, 75]);
      const iqr = percentiles[1] - percentiles[0];

      // Calculate bounds
      const lowerBound = percentiles[0] - 1.5 * iqr;
      const upperBound = percentiles[1] + 1.5 * iqr;

      // Filter outliers
      ys = ys.filter(y => y > lowerBound && y < upperBound);
    }

    // Assuming data is already without outliers and sorted if necessary
    let sortedData = ys.slice().sort((a, b) => a - b);

    let mean = sortedData.reduce((acc, curr) => acc + curr, 0) / sortedData.length;
    let median = sortedData.length % 2 === 0 ? (sortedData[sortedData.length / 2 - 1] + sortedData[sortedData.length / 2]) / 2 : sortedData[Math.floor(sortedData.length / 2)];
    let variance = sortedData.reduce((acc, curr) => acc + Math.pow(curr - mean, 2), 0) / (sortedData.length);
    let stdDev = Math.sqrt(variance);
    let min = sortedData[0];
    let max = sortedData[sortedData.length - 1];

    let ur = stdDev * 10;
    if(this.mods.includes('DT') || this.mods.includes('NC')) {
      ur /= 1.5;
    }

    if(this.mods.includes('HT')) {
      ur /= 0.75;
    }

    const statistics = {
      'mean': mean,
      'median': median,
      'std. dev': stdDev,
      'unstable rate': ur,
      'min': min,
      'max': max
    };

    return Object.entries(statistics).map(([name, value]) => ({ name, value }));
  }

  percentile(data: number[], percentiles: number[]): number[] {
    // 1. Sort the data
    const sortedData = data.slice().sort((a, b) => a - b);

    // 2. Calculate indices for percentiles
    const indices = percentiles.map(p => (p / 100) * (sortedData.length - 1));

    // 3. Extract values, handling potential fractional indices
    const results = indices.map(index => {
      const lower = Math.floor(index);
      const upper = Math.ceil(index);
      const fraction = index - lower;

      // Basic linear interpolation if index is fractional
      if (fraction > 0) {
        return sortedData[lower] + (sortedData[upper] - sortedData[lower]) * fraction;
      } else {
        return sortedData[lower];
      }
    });

    return results;
  }

  private calculateTotalErrors(distribution: { countMiss: number; count50: number; count100: number; count300: number }): number {
    return distribution.countMiss + distribution.count50 + distribution.count100 + distribution.count300;
  }

  doRemoveOutliers(data: ErrorDistribution): ErrorDistribution {
    let {errorDetails, ys} = this.calculateData(data);

    // Calculate IQR
    const q1 = ys[Math.floor((ys.length / 4))];
    const q3 = ys[Math.floor((ys.length * 3) / 4)];
    const iqr = q3 - q1;

    // Calculate bounds
    const lowerBound = q1 - 1.5 * iqr;
    const upperBound = q3 + 1.5 * iqr;

    // Filter outliers
    const filteredDetails = errorDetails.filter(detail => detail.x > lowerBound && detail.x < upperBound);

    // Convert back to ErrorDistribution format
    const filteredData: ErrorDistribution = {};
    filteredDetails.forEach(detail => {
      //@ts-ignore
      if (data[detail.x]) {
        //@ts-ignore
        filteredData[detail.x] = data[detail.x];
      }
    });

    return filteredData;
  }

  private calculateData(data: ErrorDistribution) {
    const errorDetails = Object.entries(data).map(([key, distribution]) => ({
      x: Number.parseInt(key),
      y: this.calculateTotalErrors(distribution),
    }));

    let ys = [];
    for (let key in data) {
      let distributionEntry = data[key];
      for (let i = 0; i < distributionEntry.countMiss; i++) {
        ys.push(Number.parseInt(key));
      }

      for (let i = 0; i < distributionEntry.count50; i++) {
        ys.push(Number.parseInt(key));
      }

      for (let i = 0; i < distributionEntry.count100; i++) {
        ys.push(Number.parseInt(key));
      }

      for (let i = 0; i < distributionEntry.count300; i++) {
        ys.push(Number.parseInt(key));
      }
    }
    return {errorDetails, ys};
  }

  buildChartData(): ChartData<"bar", DefaultDataPoint<"bar">, any> {
    let errorDistribution= this.removeOutliers ? this.doRemoveOutliers(this.errorDistribution) : this.errorDistribution;
    let errorDistributionArray = Object.entries(errorDistribution);

    let barChartData: ChartConfiguration<'bar'>['data'] = {
      labels: [],
      datasets: [
        { data: [], label: 'Miss' + (this.showPercentages ? ' (%)' : ''), backgroundColor: 'rgba(255,0,0,0.66)', borderRadius: 5 },
        { data: [], label: '50' + (this.showPercentages ? ' (%)' : ''), backgroundColor: 'rgba(187,129,33,0.66)', borderRadius: 5 },
        { data: [], label: '100' + (this.showPercentages ? ' (%)' : ''), backgroundColor: 'rgba(219,255,0,0.8)', borderRadius: 5 },
        { data: [], label: '300' + (this.showPercentages ? ' (%)' : ''), backgroundColor: 'rgba(0,255,41,0.66)', borderRadius: 5 }
      ],
    }

    if (errorDistributionArray.length >= 1) {
      const sortedEntries = errorDistributionArray
        .sort((a, b) => parseInt(a[0]) - parseInt(b[0]));

      const chartData = this.generateChartData(sortedEntries);
      barChartData.labels = chartData.labels;
      for (let i = 0; i < 4; i++) {
        barChartData.datasets[i].data = chartData.datasets[i];
      }
    }

    return barChartData;
  }

  private getChartRange(entries: [string, DistributionEntry][]): [number, number] {
    const keys = entries.map(([key, _]) => parseInt(key));

    const minKey = Math.min(...keys);
    const maxKey = Math.max(...keys);

    return [minKey, maxKey];
  }

  private generateChartData(entries: [string, DistributionEntry][]): { labels: string[], datasets: number[][] } {
    const range = this.getChartRange(entries);
    const labels: string[] = [];
    const datasets: number[][] = Array(4).fill(0).map(() => []);

    const defaultValues: DistributionEntry = {
      countMiss: 0,
      count50: 0,
      count100: 0,
      count300: 0,
    };

    const entriesMap = new Map<number, DistributionEntry>(entries.map(([key, value]) => [parseInt(key), value]));

    if(this.groupData) {
      let totalHits = 0;
      for (let key = range[0]; key <= range[1]; key++) {
        const entry = entriesMap.get(key) || { ...defaultValues };
        totalHits += entry.countMiss + entry.count50 + entry.count100 + entry.count300;
      }

      for (let key = range[0]; key <= range[1]; key += 2) {
        const endKey = key + 2 <= range[1] ? key + 2 : key + 1;
        labels.push(`${key}ms to ${endKey}ms`);

        const currentEntry = entriesMap.get(key) || { ...defaultValues };
        const nextEntry = key + 1 <= range[1] ? (entriesMap.get(key + 1) || { ...defaultValues }) : defaultValues;

        const sumEntry: DistributionEntry = {
          countMiss: currentEntry.countMiss + nextEntry.countMiss,
          count50: currentEntry.count50 + nextEntry.count50,
          count100: currentEntry.count100 + nextEntry.count100,
          count300: currentEntry.count300 + nextEntry.count300,
        };

        datasets[0].push(this.showPercentages ? ((sumEntry.countMiss / totalHits) * 100) : sumEntry.countMiss);
        datasets[1].push(this.showPercentages ? ((sumEntry.count50 / totalHits) * 100) : sumEntry.count50);
        datasets[2].push(this.showPercentages ? ((sumEntry.count100 / totalHits) * 100) : sumEntry.count100);
        datasets[3].push(this.showPercentages ? ((sumEntry.count300 / totalHits) * 100) : sumEntry.count300);
      }

      // Handling the case for an odd last key if needed
      if (range[1] % 2 !== range[0] % 2) {
        const lastEntry = entriesMap.get(range[1]) || { ...defaultValues };
        labels.push(`${range[1]}ms to ${range[1] + 1}ms`);
        datasets[0].push(this.showPercentages ? ((lastEntry.countMiss / totalHits) * 100) : lastEntry.countMiss);
        datasets[1].push(this.showPercentages ? ((lastEntry.count50 / totalHits) * 100) : lastEntry.count50);
        datasets[2].push(this.showPercentages ? ((lastEntry.count100 / totalHits) * 100) : lastEntry.count100);
        datasets[3].push(this.showPercentages ? ((lastEntry.count300 / totalHits) * 100) : lastEntry.count300);
      }
    } else {
// Calculate totalHits as the sum of all counts across entriesMap before the loop
      let totalHits = 0;
      for (let key = range[0]; key <= range[1]; key++) {
        const entry = entriesMap.get(key) || { ...defaultValues };
        totalHits += entry.countMiss + entry.count50 + entry.count100 + entry.count300;
      }

// Then iterate through the range to populate your datasets
      for (let key = range[0]; key <= range[1]; key++) {
        labels.push(`${key}ms`);

        const currentEntry = entriesMap.get(key) || { ...defaultValues };

        // Calculate percentage based on totalHits for the entire entriesMap, then push data
        datasets[0].push(this.showPercentages ? ((currentEntry.countMiss / totalHits) * 100) : currentEntry.countMiss);
        datasets[1].push(this.showPercentages ? ((currentEntry.count50 / totalHits) * 100) : currentEntry.count50);
        datasets[2].push(this.showPercentages ? ((currentEntry.count100 / totalHits) * 100) : currentEntry.count100);
        datasets[3].push(this.showPercentages ? ((currentEntry.count300 / totalHits) * 100) : currentEntry.count300);
      }
    }

    return { labels, datasets };
  }


}
