import {Component, OnInit, signal} from '@angular/core';
import {Information} from '../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core';
import {MatFormField, MatLabel} from '@angular/material/input';
import {MatOption} from '@angular/material/core';
import {MatSelect} from '@angular/material/select';
import {AlgorithmInformation} from '../information/information.models';
import {UrlConstants} from '../../../constants/UrlConstants';
import {FormsModule} from '@angular/forms';
import {BabylonCanvas, RenderCallback, RenderConfig, SceneEventData} from '../../../shared/components/render-canvas/babylon-canvas.component';
import {FRACTAL2D_FRAGMENT, FRACTAL2D_VERTEX} from './fractal.shader';
import {PointerEventTypes, PointerInfo, ShaderMaterial, Vector2} from '@babylonjs/core';
import {MatButton} from '@angular/material/button';
import {MatIcon} from '@angular/material/icon';
import {NgxSliderModule, Options} from '@angular-slider/ngx-slider';

@Component({
  selector: 'app-fractal',
  imports: [
    Information,
    MatCard,
    MatCardContent,
    MatCardHeader,
    MatCardTitle,
    TranslatePipe,
    MatFormField,
    MatLabel,
    MatOption,
    MatSelect,
    FormsModule,
    BabylonCanvas,
    MatButton,
    MatIcon,
    NgxSliderModule
  ],
  templateUrl: './fractal.component.html',
  styleUrl: './fractal.component.scss',
})
export class FractalComponent implements OnInit {
  algoInformation: AlgorithmInformation = {
    title: 'FRACTAL.EXPLANATION.TITLE',
    entries: [
      {
        name: 'Mandelbrot',
        description: 'FRACTAL.EXPLANATION.MANDELBROT_EXPLANATION',
        link: UrlConstants.MANDELBROT_WIKI
      },
      {
        name: 'Julia',
        description: 'FRACTAL.EXPLANATION.JULIA_EXPLANATION',
        link: UrlConstants.JULIA_WIKI
      },
      {
        name: 'Burning Ship',
        description: 'FRACTAL.EXPLANATION.BURNING_SHIP_EXPLANATION',
        link: UrlConstants.BURNING_SHIP_WIKI
      },
      {
        name: 'Newton',
        description: 'FRACTAL.EXPLANATION.NEWTON_EXPLANATION',
        link: UrlConstants.NEWTON_FRACTAL_WIKI
      }
    ],
    disclaimer: 'FRACTAL.EXPLANATION.DISCLAIMER',
    disclaimerBottom: 'FRACTAL.EXPLANATION.DISCLAIMER_BOTTOM',
    disclaimerListEntry: [
      'FRACTAL.EXPLANATION.DISCLAIMER_1',
      'FRACTAL.EXPLANATION.DISCLAIMER_2',
      'FRACTAL.EXPLANATION.DISCLAIMER_3',
      'FRACTAL.EXPLANATION.DISCLAIMER_4'
    ]
  };

  renderConfig: RenderConfig = {
    mode: '2D',
    initialViewSize: 100,
    vertexShader: FRACTAL2D_VERTEX,
    fragmentShader: FRACTAL2D_FRAGMENT,
    uniformNames: ["worldViewProjection", "time", "targetPosition","center", "zoom", "maxIterations", "algorithm", "colorScheme", "juliaC"]
  };

  // --- State ---
  readonly minZoomValue = 0.2;
  readonly maxZoomValue = 64000;
  private isDragging = false;
  private dragStartPoint: { x: number, y: number } | null = null;

  selectedAlgorithm = 0;
  selectedColorScheme = 0;

  sliderValue = signal(0);
  options: Options = {
    floor: this.minZoomValue,
    ceil: this.maxZoomValue,
    logScale: true,
    step: 0.01,
    showTicks: false,
    hideLimitLabels: true,
    hidePointerLabels: true
  };
  zoom = 0;
  offsetX = 0;
  offsetY = 0;
  maxIterations = 0;

  juliaReal = -0.7;
  juliaImag = 0.27015;

  // --- Render Callback ---
  onRender: RenderCallback = (material: ShaderMaterial) => {
    material.setVector2("center", new Vector2(this.offsetX, this.offsetY));
    material.setFloat("zoom", this.zoom);
    material.setInt("maxIterations", this.maxIterations);
    material.setInt("algorithm", this.selectedAlgorithm);
    material.setInt("colorScheme", this.selectedColorScheme);
    material.setVector2("juliaC", new Vector2(this.juliaReal, this.juliaImag));
  };

  ngOnInit() {
    this.onReset();
  }

  onAlgorithmChange(algoName: string): void {
    switch(algoName) {
      case 'Mandelbrot': this.selectedAlgorithm = 0; break;
      case 'Julia': this.selectedAlgorithm = 1; break;
      case 'Burning Ship': this.selectedAlgorithm = 2; break;
      case 'Newton': this.selectedAlgorithm = 3; break;
    }
    this.onReset()
  }

  onColorChanged(schemeName: string): void {
    switch(schemeName) {
      case 'Blue-Gold': this.selectedColorScheme = 0; break;
      case 'Greyscale': this.selectedColorScheme = 1; break;
      case 'Fire': this.selectedColorScheme = 2; break;
      case 'Rainbow': this.selectedColorScheme = 3; break;
    }
  }

  onReset(): void {

    this.offsetY = 0.0;
    this.zoom = 0.2
    this.maxIterations = 100;

    switch(this.selectedAlgorithm) {
      case 0: this.offsetX = -0.5; break;
      case 1: this.offsetX = 0; break;
      case 2: this.offsetX = -1.75; this.zoom = 8; this.offsetY = -0.03;break;
      case 3: this.offsetX = 0; break;
      default: this.offsetX = 0.0;
    }
  }

  onSceneReady(event: SceneEventData): void {
    event.scene.onPointerObservable.add((pointerInfo) => {
      switch (pointerInfo.type) {

        case PointerEventTypes.POINTERDOWN:
          this.onPointerDown(pointerInfo);
          break;

        case PointerEventTypes.POINTERUP:
          this.onPointerUp();
          break;

        case PointerEventTypes.POINTERMOVE:
          this.onPointerMove(pointerInfo);
          break;

        case PointerEventTypes.POINTERWHEEL:
          this.onPointerWheel(pointerInfo);
          break;
      }
    });
  }

  private onPointerDown(info: PointerInfo): void {
    if (info.event.button !== 0) {
      return;
    }
    this.isDragging = true;
    this.dragStartPoint = { x: info.event.clientX, y: info.event.clientY };
  }

  private onPointerUp(): void {
      this.isDragging = false;
      this.dragStartPoint = null;
  }

  private onPointerMove(info: PointerInfo): void {
    if (!this.isDragging || !this.dragStartPoint) {
      return;
    }

    const event = info.event as PointerEvent;

    const deltaX = event.clientX - this.dragStartPoint.x;
    const deltaY = event.clientY - this.dragStartPoint.y;

    const element = event.target as HTMLElement;
    const height = element.clientHeight;

    const scaleFactor = 1 / (height * this.zoom);

    this.offsetX += deltaX * scaleFactor;
    this.offsetY += deltaY * scaleFactor;

    this.dragStartPoint = { x: event.clientX, y: event.clientY };
  }

  private onPointerWheel(info: PointerInfo): void {
    const event = info.event as WheelEvent;

    event.preventDefault();

    const element = event.target as HTMLElement;
    const rect = element.getBoundingClientRect();

    const mouseXPixels = -(event.clientX - rect.left - rect.width / 2);
    const mouseYPixels = -(event.clientY - rect.top - rect.height / 2);

    const mouseXView = mouseXPixels / rect.height;
    const mouseYView = mouseYPixels / rect.height;

    const mouseXWorld = mouseXView / this.zoom + this.offsetX;
    const mouseYWorld = mouseYView / this.zoom + this.offsetY;

    const zoomFactor = 1.1;
    if (event.deltaY < 0) {
      this.zoom *= zoomFactor;
    } else {
      this.zoom /= zoomFactor;
    }

    this.zoom = Math.max(Math.min(this.zoom, this.maxZoomValue), this.minZoomValue);
    this.sliderValue.set(this.zoom);
    const optimalIterations = this.getIterationsForZoom(this.zoom);
    this.maxIterations = Math.min(optimalIterations, 3000);

    this.offsetX = mouseXWorld - mouseXView / this.zoom;
    this.offsetY = mouseYWorld - mouseYView / this.zoom;
  }

  onSliderChange(newValue: number): void {

    this.zoom = newValue;
    this.zoom = Math.max(Math.min(this.zoom, this.maxZoomValue), this.minZoomValue);
    this.maxIterations = this.getIterationsForZoom(this.zoom);
  }

  private getIterationsForZoom(zoom: number): number {
    const baseIterations = 100;
    const factor = 200;

    if (zoom <= 1) {
      return baseIterations;
    }

    return Math.floor(baseIterations + Math.log10(zoom) * factor);
  }
}
