import { Injectable } from '@angular/core';
import {SortData, SortSnapshot} from '../sorting.models';

@Injectable({
  providedIn: 'root'
})
export class SortingService {

  private createSnapshot(array: SortData[]): SortSnapshot {
    return {
      array: array.map(item => ({ ...item }))
    };
  }

  // --- BUBBLE SORT ---
  bubbleSort(array: SortData[]): SortSnapshot[] {
    const snapshots: SortSnapshot[] = [];
    const arr = array.map(item => ({ ...item }));
    const n = arr.length;

    snapshots.push(this.createSnapshot(arr));

    for (let i = 0; i < n - 1; i++) {
      for (let j = 0; j < n - i - 1; j++) {
        arr[j].state = 'comparing';
        arr[j + 1].state = 'comparing';
        snapshots.push(this.createSnapshot(arr));

        if (arr[j].value > arr[j + 1].value) {
          const temp = arr[j].value;
          arr[j].value = arr[j + 1].value;
          arr[j + 1].value = temp;

          snapshots.push(this.createSnapshot(arr));
        }

        arr[j].state = 'unsorted';
        arr[j + 1].state = 'unsorted';
      }
      arr[n - 1 - i].state = 'sorted';
      snapshots.push(this.createSnapshot(arr));
    }

    arr[0].state = 'sorted';
    snapshots.push(this.createSnapshot(arr));

    return snapshots;
  }

  // --- COCKTAIL SORT ---

  cocktailSort(array: SortData[]): SortSnapshot[] {
    const snapshots: SortSnapshot[] = [];
    const arr = array.map(item => ({ ...item }));
    snapshots.push(this.createSnapshot(arr));

    let start = -1;
    let end = array.length-1;
    let changed: boolean;
    do {
      changed = false;
      start += 1;
      for (let i = start; i < end; i++) {
        changed = this.switchValuesIfCorrect(arr, i, snapshots, changed);
      }

      arr[end].state = 'sorted';
      snapshots.push(this.createSnapshot(arr));

      //DONE
      if (!changed) {
        break;
      }

      changed = false;
      end -= 1;
      for (let i = end-1; i >= start; i--) {
        changed = this.switchValuesIfCorrect(arr, i, snapshots, changed);
      }
      arr[start].state = 'sorted';
      snapshots.push(this.createSnapshot(arr));
    } while (changed);
    return snapshots;
  }

  private switchValuesIfCorrect(arr: { value: number; state: "sorted" | "comparing" | "unsorted" }[], i: number, snapshots: SortSnapshot[], changed: boolean) {
    const firstValue = arr[i];
    const secondValue = arr[i + 1];

    firstValue.state = 'comparing';
    secondValue.state = 'comparing';
    snapshots.push(this.createSnapshot(arr));

    if (firstValue.value > secondValue.value) {
      const temp = firstValue.value;
      firstValue.value = secondValue.value;
      secondValue.value = temp;
      changed = true;
    }
    firstValue.state = 'unsorted';
    secondValue.state = 'unsorted';
    snapshots.push(this.createSnapshot(arr));
    return changed;
  }

// --- QUICK SORT ---
  quickSort(array: SortData[]): SortSnapshot[] {
    const snapshots: SortSnapshot[] = [];
    const arr = array.map(item => ({ ...item }));
    snapshots.push(this.createSnapshot(arr));

    this.quickSortHelper(arr, 0, arr.length - 1, snapshots);

    arr.forEach(i => i.state = 'sorted');
    snapshots.push(this.createSnapshot(arr));

    return snapshots;
  }

  private quickSortHelper(arr: SortData[], low: number, high: number, snapshots: SortSnapshot[]) {
    if (low < high) {
      const pi = this.partition(arr, low, high, snapshots);

      this.quickSortHelper(arr, low, pi - 1, snapshots);
      this.quickSortHelper(arr, pi + 1, high, snapshots);
    } else if (low >= 0 && high >= 0 && low === high) {
      arr[low].state = 'sorted';
      snapshots.push(this.createSnapshot(arr));
    }
  }

  private partition(arr: SortData[], low: number, high: number, snapshots: SortSnapshot[]): number {
    const pivot = arr[high];
    arr[high].state = 'comparing';
    snapshots.push(this.createSnapshot(arr));

    let i = (low - 1);

    for (let j = low; j <= high - 1; j++) {
      arr[j].state = 'comparing';
      snapshots.push(this.createSnapshot(arr));

      if (arr[j].value < pivot.value) {
        i++;
        this.swap(arr, i, j);
        snapshots.push(this.createSnapshot(arr));
      }
      arr[j].state = 'unsorted';
    }
    this.swap(arr, i + 1, high);

    arr[high].state = 'unsorted';
    arr[i + 1].state = 'sorted';
    snapshots.push(this.createSnapshot(arr));

    return i + 1;
  }

  // --- HEAP SORT ---
  heapSort(array: SortData[]): SortSnapshot[] {
    const snapshots: SortSnapshot[] = [];
    const arr = array.map(item => ({ ...item }));
    const n = arr.length;

    snapshots.push(this.createSnapshot(arr));

    for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
      this.heapify(arr, n, i, snapshots);
    }

    for (let i = n - 1; i > 0; i--) {
      arr[0].state = 'comparing';
      arr[i].state = 'comparing';
      snapshots.push(this.createSnapshot(arr));

      this.swap(arr, 0, i);

      arr[0].state = 'unsorted';
      arr[i].state = 'sorted';
      snapshots.push(this.createSnapshot(arr));

      this.heapify(arr, i, 0, snapshots);
    }
    arr[0].state = 'sorted';
    snapshots.push(this.createSnapshot(arr));

    return snapshots;
  }

  private heapify(arr: SortData[], n: number, i: number, snapshots: SortSnapshot[]) {
    let largest = i;
    const l = 2 * i + 1;
    const r = 2 * i + 2;

    if (l < n) {
      arr[l].state = 'comparing';
      arr[largest].state = 'comparing';
      snapshots.push(this.createSnapshot(arr));

      if (arr[l].value > arr[largest].value) {
        largest = l;
      }
      arr[l].state = 'unsorted';
      arr[largest].state = 'unsorted';
    }

    if (r < n) {
      arr[r].state = 'comparing';
      arr[largest].state = 'comparing';
      snapshots.push(this.createSnapshot(arr));

      if (arr[r].value > arr[largest].value) {
        largest = r;
      }
      arr[r].state = 'unsorted';
      arr[largest].state = 'unsorted';
    }

    if (largest !== i) {
      this.swap(arr, i, largest);
      snapshots.push(this.createSnapshot(arr));

      this.heapify(arr, n, largest, snapshots);
    }
  }

  // --- TIM SORT ---
  timSort(array: SortData[]): SortSnapshot[] {
    const snapshots: SortSnapshot[] = [];
    const arr = array.map(item => ({ ...item }));
    const n = arr.length;
    const RUN = 32;

    snapshots.push(this.createSnapshot(arr));

    // Step 1: Sort small runs with insertion sort
    for (let i = 0; i < n; i += RUN) {
      const end = Math.min(i + RUN - 1, n - 1);
      this.insertionSortRange(arr, i, end, snapshots);
    }

    // Step 2: Merge the sorted runs
    for (let size = RUN; size < n; size *= 2) {
      for (let left = 0; left < n; left += 2 * size) {
        const mid = Math.min(left + size - 1, n - 1);
        const right = Math.min(left + 2 * size - 1, n - 1);
        if (mid < right) {
          this.mergeRanges(arr, left, mid, right, snapshots);
        }
      }
    }

    arr.forEach(item => item.state = 'sorted');
    snapshots.push(this.createSnapshot(arr));

    return snapshots;
  }

  private insertionSortRange(arr: SortData[], left: number, right: number, snapshots: SortSnapshot[]): void {
    for (let i = left + 1; i <= right; i++) {
      const tempValue = arr[i].value;
      arr[i].state = 'comparing';
      snapshots.push(this.createSnapshot(arr));

      let j = i - 1;
      while (j >= left && arr[j].value > tempValue) {
        arr[j].state = 'comparing';
        arr[j + 1].value = arr[j].value;
        arr[j].state = 'unsorted';
        snapshots.push(this.createSnapshot(arr));
        j--;
      }

      arr[j + 1].value = tempValue;
      arr[i].state = 'unsorted';
      snapshots.push(this.createSnapshot(arr));
    }
  }

  private mergeRanges(arr: SortData[], left: number, mid: number, right: number, snapshots: SortSnapshot[]): void {
    const leftPart = arr.slice(left, mid + 1).map(item => ({ ...item }));
    const rightPart = arr.slice(mid + 1, right + 1).map(item => ({ ...item }));

    let i = 0;
    let j = 0;
    let k = left;

    while (i < leftPart.length && j < rightPart.length) {
      // Highlight the write target and the right-side source (arr[mid+1+j] is still
      // untouched in the array since k never overtakes it during a merge)
      const rightSourceIdx = mid + 1 + j;
      arr[k].state = 'comparing';
      arr[rightSourceIdx].state = 'comparing';
      snapshots.push(this.createSnapshot(arr));

      arr[rightSourceIdx].state = 'unsorted';

      if (leftPart[i].value <= rightPart[j].value) {
        arr[k].value = leftPart[i].value;
        i++;
      } else {
        arr[k].value = rightPart[j].value;
        j++;
      }

      arr[k].state = 'unsorted';
      k++;
      snapshots.push(this.createSnapshot(arr));
    }

    while (i < leftPart.length) {
      arr[k].value = leftPart[i].value;
      i++;
      k++;
    }

    while (j < rightPart.length) {
      arr[k].value = rightPart[j].value;
      j++;
      k++;
    }
  }

  private swap(arr: SortData[], i: number, j: number) {
    const temp = arr[i].value;
    arr[i].value = arr[j].value;
    arr[j].value = temp;
  }
}
