import {AfterViewInit, Component, ElementRef, EventEmitter, inject, Input, NgZone, OnDestroy, Output, ViewChild} from '@angular/core';
import {MatSnackBar} from '@angular/material/snack-bar';
import {TranslateService} from '@ngx-translate/core';
import {ArcRotateCamera, Camera, Engine, MeshBuilder, Scene, ShaderLanguage, ShaderMaterial, Vector2, Vector3, WebGPUEngine} from '@babylonjs/core';
import {GpuCapabilityService, GpuTier} from '../../../service/gpu-capability.service';

export interface RenderConfig {
  mode: '2D' | '3D';
  shaderLanguage?: number; //0 GLSL, 1 WGSL
  initialViewSize: number;
  vertexShader?: string;
  fragmentShader?: string;
  uniformNames?: string[];
  uniformBufferNames?: string[];
}

export type RenderCallback = (material: ShaderMaterial, camera: Camera, canvas: HTMLCanvasElement, scene: Scene) => void;

export interface SceneEventData {
  scene: Scene;
  engine: WebGPUEngine | Engine;
  gpuTier: GpuTier;
}

@Component({
  selector: 'app-babylon-canvas',
  imports: [],
  templateUrl: './babylon-canvas.component.html',
  styleUrl: './babylon-canvas.component.scss',
})
export class BabylonCanvas implements AfterViewInit, OnDestroy {
  readonly ngZone = inject(NgZone);
  private readonly snackBar = inject(MatSnackBar);
  private readonly translate = inject(TranslateService);
  private readonly gpuCapability = inject(GpuCapabilityService);

  @ViewChild('renderCanvas', { static: true }) canvasRef!: ElementRef<HTMLCanvasElement>;

  @Input({ required: true }) config!: RenderConfig;
  @Input() renderCallback?: RenderCallback;

  @Output() sceneReady = new EventEmitter<SceneEventData>();
  @Output() sceneResized = new EventEmitter<SceneEventData>();
  @Output() engineUnavailable = new EventEmitter<{ reason: string }>();

  private engine!: WebGPUEngine | Engine;
  private scene!: Scene;
  private shaderMaterial!: ShaderMaterial;
  private camera!: Camera;
  private gpuTier: GpuTier = 'none';

  //Listener
  private readonly resizeHandler = () => this.handleResize();
  private readonly wheelHandler = (evt: WheelEvent) => evt.preventDefault();

  ngAfterViewInit(): void {
    this.initBabylon().then(() => { console.log("Engine initialized"); });
  }

  ngOnDestroy(): void {
    window.removeEventListener('resize', this.resizeHandler);

    const canvas = this.canvasRef?.nativeElement;
    canvas?.removeEventListener('wheel', this.wheelHandler);

    this.engine?.dispose();
  }

  private async initBabylon(): Promise<void> {
    const canvas = this.canvasRef.nativeElement;
    const tier = await this.gpuCapability.detect();
    this.gpuTier = tier;

    if (tier === 'webgpu') {
      await this.initWebGpuEngine(canvas);
      return;
    }

    if (tier === 'webgl') {
      this.showSnackBar('GPU.WEBGL_FALLBACK');
      this.initWebGlEngine(canvas);
      return;
    }

    this.showSnackBar('GPU.NOT_SUPPORTED');
    this.engineUnavailable.emit({reason: 'no_gpu'});
  }

  private async initWebGpuEngine(canvas: HTMLCanvasElement): Promise<void> {
    const tmpEngine = new WebGPUEngine(canvas);

    try {
      await tmpEngine.initAsync();
      this.engine = tmpEngine;
      this.setupScene(canvas);
    } catch {
      this.showSnackBar('GPU.WEBGL_FALLBACK');
      this.gpuTier = 'webgl';
      this.initWebGlEngine(canvas);
    }
  }

  private initWebGlEngine(canvas: HTMLCanvasElement): void {
    this.engine = new Engine(canvas, true);
    this.setupScene(canvas);
  }

  private setupScene(canvas: HTMLCanvasElement): void {
    this.scene = new Scene(this.engine);
    this.setupCamera(canvas);
    this.addListener(canvas);
    this.createShaderMaterial();
    this.createFullScreenRect();
    this.sceneReady.emit({
      scene: this.scene,
      engine: this.engine,
      gpuTier: this.gpuTier
    });
    this.addRenderLoop(canvas);
  }

  private showSnackBar(translationKey: string): void {
    const message = this.translate.instant(translationKey);
    this.snackBar.open(message, 'OK', {duration: 8000, horizontalPosition: 'center', verticalPosition: 'top'});
  }

  private addListener(canvas: HTMLCanvasElement) {
    canvas.addEventListener('wheel', this.wheelHandler, {passive: false});
    window.addEventListener('resize', this.resizeHandler);
  }

  private setupCamera(canvas: HTMLCanvasElement) {
    if (this.config.mode === '3D') {
      this.setup3dCamera(canvas);
      return;
    }

    this.setup2dCamera(canvas);
  }

  private setup2dCamera(canvas: HTMLCanvasElement) {
    const cam = new ArcRotateCamera("Camera2D", -Math.PI / 2, Math.PI / 2, 10, Vector3.Zero(), this.scene);
    cam.mode = Camera.ORTHOGRAPHIC_CAMERA;

    const aspect = canvas.width / canvas.height;
    const viewSize = this.config?.initialViewSize ?? 10;
    cam.orthoLeft = -viewSize * aspect / 2;
    cam.orthoRight = viewSize * aspect / 2;
    cam.orthoTop = viewSize / 2;
    cam.orthoBottom = -viewSize / 2;

    this.camera = cam;
  }

  private setup3dCamera(canvas: HTMLCanvasElement) {
    const cam = new ArcRotateCamera("Camera", 0, Math.PI / 2, 4, Vector3.Zero(), this.scene);
    cam.wheelPrecision = 100;
    cam.pinchPrecision = 200;
    cam.minZ = 0.1;
    cam.maxZ = 100;
    cam.lowerRadiusLimit = 1.5;
    cam.upperRadiusLimit = 20;
    cam.radius = this.config?.initialViewSize ?? 1;
    cam.attachControl(canvas, true);
    this.camera = cam;
  }

  private createFullScreenRect() {
    if (!this.config.vertexShader || !this.config.fragmentShader) {
      return;
    }
    const plane = MeshBuilder.CreatePlane("plane", {size: 100}, this.scene);
    if (this.config.mode === '3D') {
      plane.parent = this.camera;
      plane.position.z = 1;
    } else {
      plane.lookAt(this.camera.position);
    }
    plane.alwaysSelectAsActiveMesh = true;

    plane.material = this.shaderMaterial;
  }

  private createShaderMaterial() {
    if (!this.config.vertexShader || !this.config.fragmentShader || !this.config.uniformNames) {
      return;
    }

    this.shaderMaterial = new ShaderMaterial(
      "shaderMaterial",
      this.scene,
      {
        vertexSource: this.config.vertexShader,
        fragmentSource: this.config.fragmentShader
      },
      {
        attributes: ["position", "uv"],
        uniforms: ["resolution", "cameraPosition", ...this.config.uniformNames],
        uniformBuffers: this.config.uniformBufferNames ?? [],
        shaderLanguage: this.config.shaderLanguage ?? ShaderLanguage.GLSL
      }
    );
    this.shaderMaterial.disableDepthWrite = true;
    this.shaderMaterial.backFaceCulling = false;
  }

  private addRenderLoop(canvas: HTMLCanvasElement) {
    this.engine.runRenderLoop(() => {

      // callback call to call specific uniforms
      if (this.renderCallback) {
        this.renderCallback(this.shaderMaterial, this.camera, canvas, this.scene);
      }

      // default uniforms which maybe each scene has
      if (this.shaderMaterial) {
        this.shaderMaterial.setVector2("resolution", new Vector2(canvas.width, canvas.height));
        this.shaderMaterial.setVector3("cameraPosition", this.camera.position);
      }

      this.scene.render();
    });
  }

  private handleResize(): void {
    if (this.engine) {
      this.engine.resize();
    }

    if (this.config.mode === '2D' && this.camera instanceof ArcRotateCamera) {
      const viewSize = this.config?.initialViewSize ?? 10;
      this.camera.orthoLeft = -viewSize / 2;
      this.camera.orthoRight = viewSize / 2;
      this.camera.orthoTop = viewSize / 2;
      this.camera.orthoBottom = -viewSize / 2;
    }

    this.sceneResized?.emit({
      scene: this.scene,
      engine: this.engine,
      gpuTier: this.gpuTier
    });
  }
}
