import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common';

export interface GridPos { row: number; col: number }

@Component({
  selector: 'app-generic-grid',
  imports: [CommonModule],
  templateUrl: './generic-grid.html',
  styleUrl: './generic-grid.scss',
})
export class GenericGridComponent implements AfterViewInit {
  @ViewChild('gridCanvas', { static: true })
  canvas!: ElementRef<HTMLCanvasElement>;

  @Input() gridRows: number = 10;
  @Input() gridCols: number = 10;
  @Input() nodeSize: number = 10; // Default node size, can be overridden by computeNodeSize
  @Input() maxGridPx: number = 500; // Max pixels for grid dimension
  @Input() minGridSize: number = 5;
  @Input() maxGridSize: number = 50;
  @Input() drawNodeBorderColor: string = '#ccc';
  @Input() backgroundColor: string = 'lightgray';

  // Callbacks from parent component
  @Input() createNodeFn!: (row: number, col: number) => any;
  @Input() getNodeColorFn!: (node: any) => string;
  @Input() applySelectionFn!: (pos: GridPos, grid: any[][]) => void;
  @Input() initializationFn!: (grid: any[][]) => void;

  @Output() gridChange = new EventEmitter<any[][]>();
  @Output() nodeClick = new EventEmitter<GridPos>();

  private ctx!: CanvasRenderingContext2D;
  grid: any[][] = [];

  isDrawing = false;
  protected lastCell: GridPos | null = null;

  ngAfterViewInit(): void {
    this.ctx = this.getContextOrThrow();
    this.setupCanvasListeners();
    this.applyGridSize();
  }

  setupCanvasListeners(): void {
    const el = this.canvas.nativeElement;
    el.addEventListener('mousedown', (e) => this.onMouseDown(e));
    el.addEventListener('mousemove', (e) => this.onMouseMove(e));
    el.addEventListener('mouseup', () => this.onMouseUp());
    el.addEventListener('mouseleave', () => this.onMouseUp());

    el.addEventListener('touchstart', (e) => {
      if (e.cancelable) e.preventDefault();
      this.onMouseDown(e as never);
    }, { passive: false });

    el.addEventListener('touchmove', (e) => {
      if (e.cancelable) e.preventDefault();
      this.onMouseMove(e as never);
    }, { passive: false });

    el.addEventListener('touchend', () => {
      this.onMouseUp();
    });
  }

  applyGridSize(): void {
    this.gridRows = this.clampGridSize(this.gridRows);
    this.gridCols = this.clampGridSize(this.gridCols);
    this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
    this.resizeCanvas();
    if (this.gridRows === this.grid.length && this.gridCols === this.grid[0]?.length) {
      this.drawGrid();
      return;
    }
    this.initializeGrid();
  }

  initializeGrid(): void {
    this.grid = this.createEmptyGrid();
    if (this.initializationFn) {
      this.initializationFn(this.grid);
    }
    this.drawGrid();
    this.gridChange.emit(this.grid);
  }

  createEmptyGrid(): any[][] {
    const grid: any[][] = [];
    for (let row = 0; row < this.gridRows; row++) {
      const currentRow: any[] = [];
      for (let col = 0; col < this.gridCols; col++) {
        currentRow.push(this.createNodeFn(row, col));
      }
      grid.push(currentRow);
    }
    return grid;
  }

  drawGrid(): void {
    if (!this.ctx || !this.grid.length) return;

    const width = this.canvas.nativeElement.width;
    const height = this.canvas.nativeElement.height;
    const size = this.nodeSize;

    this.ctx.fillStyle = this.backgroundColor;
    this.ctx.fillRect(0, 0, width, height);
    this.ctx.fillStyle = 'black';

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

        const color = this.getNodeColorFn(node);

        if (color !== this.backgroundColor) {
          if (this.ctx.fillStyle !== color) {
            this.ctx.fillStyle = color;
          }

          const x = col * this.nodeSize;
          const y = row * this.nodeSize;

          this.ctx.fillRect(x, y, this.nodeSize, this.nodeSize);

        }
      }
    }

    if (size > 2) {
      this.drawGridLines(width, height);
    }

  }

  private drawGridLines(width: number, height: number): void {
    this.ctx.beginPath();
    this.ctx.strokeStyle = this.drawNodeBorderColor;
    this.ctx.lineWidth = 1;

    for (let col = 0; col <= this.gridCols; col++) {
      const x = col * this.nodeSize;
      this.ctx.moveTo(x, 0);
      this.ctx.lineTo(x, height);
    }

    for (let row = 0; row <= this.gridRows; row++) {
      const y = row * this.nodeSize;
      this.ctx.moveTo(0, y);
      this.ctx.lineTo(width, y);
    }

    this.ctx.stroke();
  }

  drawNode(node: any): void {
    this.ctx.fillStyle = this.getNodeColorFn(node);
    this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);

    if (this.nodeSize > 4) {
      this.ctx.strokeStyle = this.drawNodeBorderColor;
      this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
    }
  }

  private getContextOrThrow(): CanvasRenderingContext2D {
    const ctx = this.canvas.nativeElement.getContext('2d');
    if (!ctx) {
      throw new Error('CanvasRenderingContext2D not available.');
    }
    return ctx;
  }

  private clampGridSize(value: number): number {
    const parsed = Math.floor(Number(value));
    const safe = Number.isFinite(parsed) ? parsed : this.minGridSize; // Use minGridSize as fallback
    return Math.min(Math.max(this.minGridSize, safe), this.maxGridSize);
  }

  private computeNodeSize(rows: number, cols: number): number {
    const sizeByWidth = Math.floor(this.maxGridPx / cols);
    const sizeByHeight = Math.floor(this.maxGridPx / rows);
    return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
  }

  private resizeCanvas(): void {
    const el = this.canvas.nativeElement;
    el.width = this.gridCols * this.nodeSize;
    el.height = this.gridRows * this.nodeSize;
  }

  onMouseDown(event: MouseEvent | TouchEvent): void {
    this.isDrawing = true;
    this.lastCell = null;
    const pos = this.getGridPosition(event);
    if (pos) {
      this.handleInteraction(pos);
    }
  }

  onMouseMove(event: MouseEvent | TouchEvent): void {
    if (!this.isDrawing) {
      return;
    }
    const pos = this.getGridPosition(event);
    if (pos && !this.isSameCell(pos, this.lastCell)) {
      this.handleInteraction(pos);
    }
  }

  onMouseUp(): void {
    this.isDrawing = false;
    this.lastCell = null;
  }

  private handleInteraction(pos: GridPos): void {
    this.applySelectionFn(pos, this.grid);
    this.drawNode(this.grid[pos.row][pos.col]);
    this.lastCell = pos;
    this.nodeClick.emit(pos);
    this.gridChange.emit(this.grid);
  }

  private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
    const canvas = this.canvas.nativeElement;
    const rect = canvas.getBoundingClientRect();

    let clientX, clientY;
    if (event instanceof MouseEvent) {
      clientX = event.clientX;
      clientY = event.clientY;
    } else if (event instanceof TouchEvent && event.touches.length > 0) {
      clientX = event.touches[0].clientX;
      clientY = event.touches[0].clientY;
    } else {
      return null;
    }

    const scaleX = canvas.width / rect.width;
    const scaleY = canvas.height / rect.height;

    const x = (clientX - rect.left) * scaleX;
    const y = (clientY - rect.top) * scaleY;

    const col = Math.floor(x / this.nodeSize);
    const row = Math.floor(y / this.nodeSize);

    if (!this.isValidPosition(row, col)) {
      return null;
    }

    return { row, col };
  }

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

  private isSameCell(a: GridPos, b: GridPos | null): boolean {
    return !!b && a.row === b.row && a.col === b.col;
  }
}
