import {
  Component,
  ElementRef,
  HostListener,
  computed,
  inject,
  input,
  output,
  viewChild
} from '@angular/core';
import { CommonModule } from '@angular/common';

import {
  ThemeGridEditorItem,
  ThemeGridRect,
  ThemeLayoutContainerDefinition
} from '../../../domain/models/theme.model';

type DragMode = 'move' | 'resize';

interface DragState {
  key: string;
  mode: DragMode;
  startClientX: number;
  startClientY: number;
  startGrid: ThemeGridRect;
}

@Component({
  selector: 'app-theme-grid-editor',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './theme-grid-editor.component.html',
  styleUrl: './theme-grid-editor.component.scss'
})
export class ThemeGridEditorComponent {
  readonly container = input.required<ThemeLayoutContainerDefinition>();
  readonly items = input.required<ThemeGridEditorItem[]>();
  readonly selectedKey = input<string | null>(null);
  readonly disabled = input(false);

  readonly itemChanged = output<{ key: string; grid: ThemeGridRect }>();
  readonly itemSelected = output<string>();

  readonly canvasRef = viewChild.required<ElementRef<HTMLElement>>('canvasRef');
  readonly frameStyle = computed(() => ({
    '--theme-grid-columns': `${this.container().columns}`,
    '--theme-grid-rows': `${this.container().rows}`,
    gridTemplateColumns: this.container().templateColumns ?? `repeat(${this.container().columns}, minmax(0, 1fr))`,
    gridTemplateRows: this.container().templateRows ?? `repeat(${this.container().rows}, minmax(0, 1fr))`
  }));
  readonly gridCells = computed(() => Array.from({ length: this.container().columns * this.container().rows }, (_, index) => index));

  private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
  private dragState: DragState | null = null;

  @HostListener('document:pointermove', ['$event'])
  onPointerMove(event: PointerEvent): void {
    if (!this.dragState || this.disabled()) {
      return;
    }

    const deltaColumns = this.gridDelta('columns', this.dragState.startClientX, event.clientX);
    const deltaRows = this.gridDelta('rows', this.dragState.startClientY, event.clientY);
    const nextGrid = { ...this.dragState.startGrid };

    if (this.dragState.mode === 'move') {
      nextGrid.x = this.clamp(deltaColumns + this.dragState.startGrid.x, 0, this.container().columns - nextGrid.w);
      nextGrid.y = this.clamp(deltaRows + this.dragState.startGrid.y, 0, this.container().rows - nextGrid.h);
    } else {
      nextGrid.w = this.clamp(deltaColumns + this.dragState.startGrid.w, 1, this.container().columns - nextGrid.x);
      nextGrid.h = this.clamp(deltaRows + this.dragState.startGrid.h, 1, this.container().rows - nextGrid.y);
    }

    this.itemChanged.emit({
      key: this.dragState.key,
      grid: nextGrid
    });
  }

  @HostListener('document:pointerup')
  @HostListener('document:pointercancel')
  onPointerUp(): void {
    this.dragState = null;
  }

  @HostListener('document:keydown.escape')
  onEscape(): void {
    this.dragState = null;
  }

  itemStyle(item: ThemeGridEditorItem): Record<string, string> {
    return {
      gridColumn: `${item.grid.x + 1} / span ${item.grid.w}`,
      gridRow: `${item.grid.y + 1} / span ${item.grid.h}`
    };
  }

  selectItem(key: string): void {
    this.itemSelected.emit(key);
  }

  startMove(event: PointerEvent, item: ThemeGridEditorItem): void {
    this.startDrag(event, item, 'move');
  }

  startResize(event: PointerEvent, item: ThemeGridEditorItem): void {
    this.startDrag(event, item, 'resize');
  }

  private startDrag(event: PointerEvent, item: ThemeGridEditorItem, mode: DragMode): void {
    if (this.disabled()) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    this.itemSelected.emit(item.key);
    this.dragState = {
      key: item.key,
      mode,
      startClientX: event.clientX,
      startClientY: event.clientY,
      startGrid: { ...item.grid }
    };
  }

  private clamp(value: number, minimum: number, maximum: number): number {
    return Math.min(Math.max(value, minimum), maximum);
  }

  private gridDelta(axis: 'columns' | 'rows', startClientPosition: number, currentClientPosition: number): number {
    const canvasRect = this.canvasRef().nativeElement.getBoundingClientRect();
    const linePositions = this.measureGridLines(axis, canvasRect);
    const startOffset = startClientPosition - (axis === 'columns' ? canvasRect.left : canvasRect.top);
    const currentOffset = currentClientPosition - (axis === 'columns' ? canvasRect.left : canvasRect.top);

    return this.nearestGridLineIndex(currentOffset, linePositions) - this.nearestGridLineIndex(startOffset, linePositions);
  }

  private measureGridLines(axis: 'columns' | 'rows', canvasRect: DOMRect): number[] {
    const canvas = this.canvasRef().nativeElement;
    const computedStyles = getComputedStyle(canvas);
    const expectedTracks = axis === 'columns' ? this.container().columns : this.container().rows;
    const availableSize = axis === 'columns' ? canvasRect.width : canvasRect.height;
    const template = axis === 'columns' ? computedStyles.gridTemplateColumns : computedStyles.gridTemplateRows;
    const trackSizes = template
      .split(/\s+/)
      .map((track) => Number.parseFloat(track))
      .filter((size) => Number.isFinite(size) && size > 0);

    if (trackSizes.length !== expectedTracks) {
      return Array.from({ length: expectedTracks + 1 }, (_, index) => (availableSize / expectedTracks) * index);
    }

    const totalTrackSize = trackSizes.reduce((sum, size) => sum + size, 0);
    const scale = totalTrackSize > 0 ? availableSize / totalTrackSize : 1;
    const linePositions = [0];

    for (const trackSize of trackSizes) {
      const previousLinePosition = linePositions[linePositions.length - 1] ?? 0;

      linePositions.push(previousLinePosition + trackSize * scale);
    }

    linePositions[linePositions.length - 1] = availableSize;

    return linePositions;
  }

  private nearestGridLineIndex(offset: number, linePositions: number[]): number {
    const clampedOffset = this.clamp(offset, linePositions[0] ?? 0, linePositions.at(-1) ?? 0);

    let nearestIndex = 0;
    let nearestDistance = Number.POSITIVE_INFINITY;

    for (let index = 0; index < linePositions.length; index++) {
      const distance = Math.abs(linePositions[index] - clampedOffset);

      if (distance < nearestDistance) {
        nearestDistance = distance;
        nearestIndex = index;
      }
    }

    return nearestIndex;
  }
}
