import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core';
import {Information} from '../../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core';
import {GenericGridComponent, GridPos} from '../../../../shared/components/generic-grid/generic-grid';
import {AlgorithmInformation} from '../../information/information.models';
import {UrlConstants} from '../../../../constants/UrlConstants';
import {Node} from '../pathfinding.models';
import {SharedFunctions} from '../../../../shared/SharedFunctions';
import {MatButton} from '@angular/material/button';
import {DecimalPipe} from '@angular/common';
import {PathfindingService} from '../service/pathfinding.service';

@Component({
  selector: 'app-labyrinth',
  imports: [
    Information,
    MatCard,
    MatCardContent,
    MatCardHeader,
    MatCardTitle,
    TranslatePipe,
    GenericGridComponent,
    MatButton,
    DecimalPipe
  ],
  templateUrl: './labyrinth.component.html',
  styleUrl: './labyrinth.component.scss',
})
export class LabyrinthComponent implements AfterViewInit {

  protected readonly gridRows = 101;
  protected readonly gridCols = 101;
  protected readonly MAX_GRID_SIZE = 101;
  protected readonly MAX_GRID_PX = 1000;
  protected readonly MIN_GRID_SIZE = 101;
  private readonly pathfindingService = inject(PathfindingService);


  algoInformation: AlgorithmInformation = {
    title: 'LABYRINTH.EXPLANATION.TITLE',
    entries: [
      {
        name: 'Prim’s',
        description: 'LABYRINTH.EXPLANATION.PRIM_EXPLANATION',
        link: UrlConstants.PRIMS_WIKI
      },
      {
        name: 'Kruskal’s',
        description: 'LABYRINTH.EXPLANATION.KRUSKAL_EXPLANATION',
        link: UrlConstants.KRUSKAL_WIKI
      }
    ],
    disclaimer: 'LABYRINTH.EXPLANATION.DISCLAIMER',
    disclaimerBottom: '',
    disclaimerListEntry: ['LABYRINTH.EXPLANATION.DISCLAIMER_1', 'LABYRINTH.EXPLANATION.DISCLAIMER_2', 'LABYRINTH.EXPLANATION.DISCLAIMER_3', 'LABYRINTH.EXPLANATION.DISCLAIMER_4']
  };

  @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;

  grid: Node[][] = [];
  startNode: Node | null = null;
  endNode: Node | null = null;
  animationSpeed = 3;
  mazeAnimationSpeed = 1;
  pathLength = "0";
  executionTime = 0;

  private timeoutIds: number[] = [];
  protected mazeNodesInOrder: Node[] = [];
  readonly isAnimationRunning = signal(false);

  ngAfterViewInit(): void {
    if (this.genericGridComponent) {
      this.genericGridComponent.initializationFn = this.initializeMazeGrid;
      this.genericGridComponent.createNodeFn = this.createMazeNode;
      this.genericGridComponent.getNodeColorFn = this.getMazeColor;
      this.genericGridComponent.applySelectionFn = this.applyNoSelection;
      this.genericGridComponent.gridRows = this.gridRows;
      this.genericGridComponent.gridCols = this.gridCols;
      this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
      this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
      this.genericGridComponent.maxGridPx = 1000;
      this.genericGridComponent.applyGridSize();
      this.genericGridComponent.initializeGrid();
    }
  }

  initializeMazeGrid = (grid: Node[][]): void => {
    this.grid = grid;
    this.createRandom(true);
  };

  createRandom(prim: boolean): void {
    this.isAnimationRunning.set(true);
    this.stopAnimations();
    this.clearPath();
    this.startNode = null;
    this.endNode = null;
    if (prim)
    {
      this.createPrimMaze();
    }
    else{
      this.createKruskalMaze();
    }

    this.cleanupGrid();
    this.genericGridComponent.drawGrid();
  }
// ------- Kuskal -------
  private createKruskalMaze(): void {
    this.initKuskal();
    this.mazeNodesInOrder = [];
    const walls = this.findWallsWithADistanceOfTwoRooms();
    SharedFunctions.shuffleArray(walls);
    for (const wallInfo of walls) {
      const { row, col, roomA, roomB } = wallInfo;

      if (roomA.nodeData !== roomB.nodeData) {
        const wallNode = this.grid[row][col];
        wallNode.isWall = true;
        this.mazeNodesInOrder.push(wallNode);

        const oldId = roomB.nodeData;
        const newId = roomA.nodeData;

        this.mergeSets(oldId, newId);
      }
    }
    this.setRandomStartAndEnd();
    this.animateMazeGeneration();
  }

  private initKuskal() {
    let roomId = 0;

    for (let row = 0; row < this.gridRows; row++) {
      for (let col = 0; col < this.gridCols; col++) {
        const node = this.grid[row][col];
        node.isStart = false;
        node.isEnd = false;

        if (row % 2 === 0 && col % 2 === 0) {
          node.isWall = false;
          node.nodeData = roomId++;
        } else {
          node.isWall = true;
        }
      }
    }
  }

  private mergeSets(oldId: number, newId: number): void {
    for (let r = 0; r < this.gridRows; r += 2) {
      for (let c = 0; c < this.gridCols; c += 2) {
        if (this.grid[r][c].nodeData === oldId) {
          this.grid[r][c].nodeData = newId;
        }
      }
    }
  }

  private findWallsWithADistanceOfTwoRooms() {
    const walls: { row: number, col: number, roomA: Node, roomB: Node }[] = [];
    for (let row = 0; row < this.gridRows; row++) {
      for (let col = 0; col < this.gridCols; col++) {
        if (row % 2 === 0 && col % 2 !== 0 && col > 0 && col < this.gridCols - 1) {
          walls.push({
            row, col,
            roomA: this.grid[row][col - 1],
            roomB: this.grid[row][col + 1]
          });
        }
        if (row % 2 !== 0 && col % 2 === 0 && row > 0 && row < this.gridRows - 1) {
          walls.push({
            row, col,
            roomA: this.grid[row - 1][col],
            roomB: this.grid[row + 1][col]
          });
        }
      }
    }
    return walls;
  }

  private setRandomStartAndEnd(): void {
    const lastRow = Math.floor((this.gridRows - 1) / 2) * 2;
    const lastCol = Math.floor((this.gridCols - 1) / 2) * 2;

    const corners = [
      { r: 0, c: 0 },
      { r: 0, c: lastCol },
      { r: lastRow, c: 0 },
      { r: lastRow, c: lastCol }
    ];

    const startIndex = Math.floor(Math.random() * corners.length);
    let endIndex = Math.floor(Math.random() * corners.length);
    while (endIndex === startIndex) {
      endIndex = Math.floor(Math.random() * corners.length);
    }

    const start = corners[startIndex];
    const end = corners[endIndex];

    this.startNode = this.grid[start.r][start.c];
    this.startNode.isStart = true;
    this.startNode.isWall = false;

    this.endNode =this.grid[end.r][end.c];
    this.endNode.isEnd = true;
    this.endNode.isWall = false;
  }

// ------- PRIM -------
  private createPrimMaze(): void {
    this.initPrim();
    this.mazeNodesInOrder = [];
    const frontier: Node[] = [];

    const {startRow, startCol, startNode} = this.findStartNode();
    this.mazeNodesInOrder.push(startNode);

    this.getNeighborWalls(startRow, startCol, frontier);

    while (frontier.length > 0) {
      const randomIndex = SharedFunctions.randomIntFromInterval(0, frontier.length - 1);
      const lastIndex = frontier.length - 1;
      [frontier[randomIndex], frontier[lastIndex]] = [frontier[lastIndex], frontier[randomIndex]];

      const wallNode = frontier.pop()!;
      const target = wallNode.linkedNode;

      if (!target || target.isVisited) {
        continue;
      }

      wallNode.isVisited = true;
      target.isVisited = true;

      this.mazeNodesInOrder.push(wallNode, target);

      this.getNeighborWalls(target.row, target.col, frontier);
    }

    this.setRandomStartAndEnd();
    this.animateMazeGeneration();
  }

  private initPrim() {
    for (let row = 0; row < this.grid.length; row++) {
      for (let col = 0; col < this.grid[row].length; col++) {
        this.grid[row][col].isWall = true;
        this.grid[row][col].isStart = false;
        this.grid[row][col].isEnd = false;
      }
    }
  }

  private cleanupGrid() {
    for (let row = 0; row < this.grid.length; row++) {
      for (let col = 0; col < this.grid[row].length; col++) {
        this.grid[row][col].isVisited = false;
        this.grid[row][col].linkedNode = null;
      }
    }
  }

  private findStartNode() {
    const startRow: number = SharedFunctions.randomEventIntFromInterval(this.gridRows - 1);
    const startCol: number = SharedFunctions.randomEventIntFromInterval(this.gridCols - 1);

    const startNode = this.grid[startRow][startCol];
    startNode.isWall = false;
    startNode.isVisited = true;
    return {startRow, startCol, startNode};
  }

  visualize(algorithm: string): void {
    this.stopAnimations();
    this.clearPath();

    const startTime = performance.now();
    let result;

    switch (algorithm) {
      case 'dijkstra': result = this.pathfindingService.dijkstra(
        this.grid,
        this.grid[this.startNode!.row][this.startNode!.col],
        this.grid[this.endNode!.row][this.endNode!.col]
      );
        break;
      case 'astar': result = this.pathfindingService.aStar(
        this.grid,
        this.grid[this.startNode!.row][this.startNode!.col],
        this.grid[this.endNode!.row][this.endNode!.col]
      );
        break;
    }

    if (!result)
    {
      return;
    }

    const endTime = performance.now();
    const lengthOfShortestPath = result.nodesInShortestPathOrder.length;
    if (lengthOfShortestPath === 0)
    {
      this.pathLength = "∞"
    }
    else
    {
      this.pathLength = result.nodesInShortestPathOrder.length + "";
    }
    this.executionTime = endTime - startTime;

    this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
  }

  createMazeNode = (row: number, col: number): Node => {
    return {
      row,
      col,
      isStart: false,
      isEnd: false,
      isWall: false,
      isVisited: false,
      isPath: false,
      nodeData: Infinity,
      linkedNode: null,
      hScore: 0,
      fScore: Infinity,
    };
  };

  getMazeColor = (node: Node): string => {
    if (node.isStart) return 'green';
    if (node.isEnd) return 'red';
    if (node.isPath) return 'gold';
    if (node.isVisited) return 'skyblue';
    if (node.isWall) return 'black';
    return 'lightgray';
  };

  applyNoSelection = (pos: GridPos, grid: Node[][]): void => {
    this.grid = grid;
    //dont need a selection for the maze case
  }

  // --- Animation (adapted to use genericGridComponent for redraw) ---
  private stopAnimations(): void {
    for (const id of this.timeoutIds) {
      clearTimeout(id);
    }
    this.timeoutIds = [];
  }

  private clearPath(): void {
    for (let row = 0; row < this.gridRows; row++) {
      for (let col = 0; col < this.gridCols; col++) {
        const node = this.grid[row][col];
        node.isVisited = false;
        node.isPath = false;
        node.nodeData = Infinity;
        node.linkedNode = null;
      }
    }
    this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component
  }

  private animateAlgorithm(visited: Node[], path: Node[]): void {
    for (let i = 0; i <= visited.length; i++) {
      if (i === visited.length) {
        const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
        this.timeoutIds.push(id);
        return;
      }

      const node = visited[i];
      const id = globalThis.setTimeout(() => {
        if (!node.isStart && !node.isEnd) {
          node.isVisited = true;
          this.genericGridComponent?.drawNode(node);
        }
      }, this.animationSpeed * i);

      this.timeoutIds.push(id);
    }
  }

  private animateShortestPath(path: Node[]): void {
    for (let i = 0; i < path.length; i++) {
      const node = path[i];
      const id = globalThis.setTimeout(() => {
        if (!node.isStart && !node.isEnd) {
          node.isPath = true;
          this.genericGridComponent?.drawNode(node); // Redraw single node
        }
      }, this.animationSpeed * i);

      this.timeoutIds.push(id);
    }
  }
  private animateMazeGeneration(): void {
    for (let i = 0; i < this.mazeNodesInOrder.length; i++) {
      const id = globalThis.setTimeout(() => {
        const node = this.mazeNodesInOrder[i];
        node.isWall = false;
        this.genericGridComponent?.drawNode(node);
        if (i === this.mazeNodesInOrder.length - 1) {
          this.cleanupGrid();
          if (this.startNode) {
            this.genericGridComponent?.drawNode(this.startNode);
          }

          if (this.endNode) {
            this.genericGridComponent?.drawNode(this.endNode);
          }
        }
        if (i == this.mazeNodesInOrder.length - 1) {
          this.isAnimationRunning.set(false);
        }
      }, this.mazeAnimationSpeed * i);

      this.timeoutIds.push(id);
    }
  }


  //utility
  private getNeighborWalls(row: number, col: number, frontier: Node[]): void{

    const directions = [
      [0, 2], [0, -2], [2, 0], [-2, 0]
    ];

    for (const [dr, dc] of directions) {
      const nextRow = row + dr;
      const nextCol = col + dc;


      if (this.isValid(nextRow, nextCol) && this.grid[nextRow][nextCol].isWall && !this.grid[nextRow][nextCol].isVisited) {
        const wallRow = row + dr / 2;
        const wallCol = col + dc / 2;

        const node = this.grid[wallRow][wallCol];
        node.linkedNode = this.grid[nextRow][nextCol];
        frontier.push(node);
      }
    }
  }

  isValid = (row: number, col: number): boolean => {
    return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
  };
}
