import {Injectable} from '@angular/core';
import * as THREE from 'three';
import {RendererService} from './renderer.service';
import {Subject} from 'rxjs';

/**
 * Represents the possible orientations of the intersection plane
 */
export enum PlaneOrientation {
  VERTICAL_ABOVE,
  VERTICAL_BELOW,
  HORIZONTAL_FRONT,
  HORIZONTAL_BEHIND,
  HORIZONTAL_RIGHT,
  HORIZONTAL_LEFT
}

/**
 * Service responsible for managing the intersection plane
 */
@Injectable({
  providedIn: 'root'
})
export class IntersectionPlaneService {
  private intersectionPlane!: THREE.Mesh;
  private planePosition: number = 0; // Position in 1/16th of a block
  private currentOrientation: PlaneOrientation = PlaneOrientation.HORIZONTAL_FRONT;
  private planeLocked: boolean = false;
  private opacity: number = 0.05;

  // Grid overlay
  private gridHelper?: THREE.GridHelper;
  private gridVisible: boolean = true;
  private gridDensity: number = 4;
  private gridSnapEnabled: boolean = true;

  // Emits whenever plane position, orientation, or lock-affecting orientation updates change visuals
  public readonly planeChanged$ = new Subject<void>();
  private lastPlaneSignature: string | null = null;

  constructor(private rendererService: RendererService) {
  }

  public get stepSize() {
    return 5
  }

  /**
   * Creates or updates the grid helper attached to the intersection plane
   * without affecting raycasting/placement.
   */
  private createOrUpdateGrid(): void {
    if (!this.intersectionPlane) return;

    if (this.gridHelper) {
      this.intersectionPlane.remove(this.gridHelper);
      (this.gridHelper.geometry as THREE.BufferGeometry).dispose();
      if (this.gridHelper.material.dispose) {
        this.gridHelper.material.dispose();
      }
      this.gridHelper = undefined;
    }

    const size = this.stepSize;
    const divisions = Math.max(1, Math.floor(size * this.gridDensity));

    this.gridHelper = new THREE.GridHelper(size, divisions, 0x888888, 0xcccccc);

    this.gridHelper.rotation.x = Math.PI / 2;
    this.gridHelper.position.z -= 0.005;

    this.gridHelper.renderOrder = 2;
    const gridMat = this.gridHelper.material as THREE.Material | THREE.Material[];
    if (Array.isArray(gridMat)) {
      gridMat.forEach(material => {
        material.transparent = true;
        material.depthWrite = false;
        if (material.opacity !== undefined) {
          material.opacity = 0.25;
        }
      });
    } else {
      gridMat.transparent = true;
      gridMat.depthWrite = false;
      gridMat.opacity = 0.25;
    }

    this.gridHelper.raycast = () => {
    };

    this.gridHelper.visible = this.gridVisible;
    this.intersectionPlane.add(this.gridHelper);
  }

  public setGridVisible(visible: boolean): void {
    this.gridVisible = visible;
    if (this.gridHelper) this.gridHelper.visible = visible;
  }

  public getGridVisible(): boolean {
    return this.gridVisible;
  }

  public setGridDensity(density: number): void {
    this.gridDensity = Math.max(1, Math.min(64, Math.floor(density)));
    if (this.intersectionPlane) {
      this.createOrUpdateGrid();
    }
  }

  public getGridDensity(): number {
    return this.gridDensity;
  }

  public setGridSnapEnabled(enabled: boolean): void {
    this.gridSnapEnabled = enabled;
  }

  public getGridSnapEnabled(): boolean {
    return this.gridSnapEnabled;
  }

  /**
   * Creates the intersection plane and adds it to the scene
   */
  createIntersectionPlane(): THREE.Mesh {
    const planeGeometry = new THREE.PlaneGeometry(5, 5);
    const planeMaterial = new THREE.MeshBasicMaterial({
      color: 0x00AA00,
      transparent: true,
      opacity: 0.05,
      side: THREE.DoubleSide
    });

    this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial);
    this.intersectionPlane.position.z = 0;
    // Center the plane vertically with the player (player is about 2 blocks tall)
    this.intersectionPlane.position.y = 1;

    // Add grid overlay as a child so it follows rotation/position
    this.createOrUpdateGrid();

    this.rendererService.scene.add(this.intersectionPlane);
    this.intersectionPlane.renderOrder = 1;

    return this.intersectionPlane;
  }

  /**
   * Determines the plane orientation based on camera position
   */
  private determinePlaneOrientation(camera: THREE.Camera): PlaneOrientation {
    // Check if camera is looking from above or below first
    const verticalAngle = Math.atan2(
      camera.position.y,
      Math.sqrt(camera.position.x * camera.position.x + camera.position.z * camera.position.z)
    );

    // Threshold angle for considering the camera to be above/below (about 45 degrees)
    const verticalThreshold = Math.PI / 4;

    if (verticalAngle > verticalThreshold) {
      return PlaneOrientation.VERTICAL_ABOVE;
    } else if (verticalAngle < -verticalThreshold) {
      return PlaneOrientation.VERTICAL_BELOW;
    } else {
      // Calculate the angle between camera and player (in the XZ plane)
      const cameraAngle = Math.atan2(
        camera.position.x,
        camera.position.z
      );

      // Determine which quadrant the camera is in with a 45-degree offset
      const quadrant = Math.floor((cameraAngle + Math.PI + Math.PI / 4) / (Math.PI / 2)) % 4;

      switch (quadrant) {
        case 0:
          return PlaneOrientation.HORIZONTAL_FRONT;
        case 1:
          return PlaneOrientation.HORIZONTAL_RIGHT;
        case 2:
          return PlaneOrientation.HORIZONTAL_BEHIND;
        case 3:
          return PlaneOrientation.HORIZONTAL_LEFT;
        default:
          return PlaneOrientation.HORIZONTAL_FRONT;
      }
    }
  }

  /**
   * Updates the plane orientation based on camera position
   */
  public updatePlaneOrientation(camera: THREE.Camera): void {
    if (!this.intersectionPlane) return;

    if (!this.planeLocked) {
      this.currentOrientation = this.determinePlaneOrientation(camera);
    }

    this.updateIntersectionPlaneOrientation()

    //Restrict plane position to the new bounds and update it
    this.planePosition = Math.max(this.getMinOffset(), Math.min(this.getMaxOffset(), this.planePosition));
    this.updatePlanePosition(this.planePosition);
  }

  /**
   * Updates the plane position based on slider value
   */
  public updatePlanePosition(value: number): void {
    this.planePosition = value;
    // Convert from 1/16th block to Three.js units
    const position = (this.planePosition / 16)

    this.intersectionPlane.position.y = 1;
    this.intersectionPlane.position.x = 0;
    this.intersectionPlane.position.z = 0;

    // Position based on the current orientation
    switch (this.currentOrientation) {
      case PlaneOrientation.VERTICAL_ABOVE:
        this.intersectionPlane.position.y = position;
        break;
      case PlaneOrientation.VERTICAL_BELOW:
        this.intersectionPlane.position.y = position;
        break;
      case PlaneOrientation.HORIZONTAL_FRONT:
        this.intersectionPlane.position.z = position;
        break;
      case PlaneOrientation.HORIZONTAL_BEHIND:
        this.intersectionPlane.position.z = -position;
        break;
      case PlaneOrientation.HORIZONTAL_RIGHT:
        this.intersectionPlane.position.x = position;
        break;
      case PlaneOrientation.HORIZONTAL_LEFT:
        this.intersectionPlane.position.x = -position;
        break;
    }

    // Notify listeners only if signature changed to avoid spamming during animation frames
    const signature = `${this.currentOrientation}|${this.planePosition}`;
    if (signature !== this.lastPlaneSignature) {
      this.lastPlaneSignature = signature;
      this.planeChanged$.next();
    }
  }

  /**
   * Updates the plane material color
   */
  private updatePlaneMaterial(color: number): void {
    this.intersectionPlane.material = new THREE.MeshBasicMaterial({
      color: color,
      transparent: true,
      opacity: this.opacity,
      side: THREE.DoubleSide
    });
  }

  public set currentOpacity(opacity: number) {
    this.opacity = opacity;
  }

  public get currentOpacity(): number {
    return this.opacity;
  }

  /**
   * Gets the intersection plane
   */
  public getIntersectionPlane(): THREE.Mesh {
    return this.intersectionPlane;
  }

  /**
   * Gets the current plane position
   */
  public getPlanePosition(): number {
    return this.planePosition;
  }

  public getMaxOffset(): number {
    switch (this.currentOrientation) {
      case PlaneOrientation.VERTICAL_ABOVE:
      case PlaneOrientation.VERTICAL_BELOW:
        return 16;
      case PlaneOrientation.HORIZONTAL_FRONT:
      case PlaneOrientation.HORIZONTAL_BEHIND:
        return 8;
      case PlaneOrientation.HORIZONTAL_RIGHT:
      case PlaneOrientation.HORIZONTAL_LEFT:
        return 8;
    }
  }

  public getMinOffset(): number {
    return this.getMaxOffset() * -1;
  }

  /**
   * Gets the current plane orientation
   */
  public getCurrentOrientation(): PlaneOrientation {
    return this.currentOrientation;
  }

  /**
   * Sets the plane orientation manually
   */
  public setPlaneOrientation(orientation: PlaneOrientation): void {
    this.currentOrientation = orientation;
    this.updateIntersectionPlaneOrientation();
    this.updatePlanePosition(this.planePosition);
  }

  private updateIntersectionPlaneOrientation() {
    switch (this.currentOrientation) {
      case PlaneOrientation.VERTICAL_ABOVE:
        this.intersectionPlane.rotation.x = -Math.PI / 2;
        this.intersectionPlane.rotation.y = 0;
        this.updatePlaneMaterial(0xAA0000);
        break;
      case PlaneOrientation.VERTICAL_BELOW:
        this.intersectionPlane.rotation.x = Math.PI / 2;
        this.intersectionPlane.rotation.y = 0;
        this.updatePlaneMaterial(0xAA0000);
        break;
      case PlaneOrientation.HORIZONTAL_FRONT:
        this.intersectionPlane.rotation.x = 0;
        this.intersectionPlane.rotation.y = 0;
        this.updatePlaneMaterial(0x00AA00);
        break;
      case PlaneOrientation.HORIZONTAL_BEHIND:
        this.intersectionPlane.rotation.x = 0;
        this.intersectionPlane.rotation.y = Math.PI;
        this.updatePlaneMaterial(0x00AA00);
        break;
      case PlaneOrientation.HORIZONTAL_RIGHT:
        this.intersectionPlane.rotation.x = 0;
        this.intersectionPlane.rotation.y = Math.PI / 2;
        this.updatePlaneMaterial(0x0000AA);
        break;
      case PlaneOrientation.HORIZONTAL_LEFT:
        this.intersectionPlane.rotation.x = 0;
        this.intersectionPlane.rotation.y = -Math.PI / 2;
        this.updatePlaneMaterial(0x0000AA);
        break;
    }
  }

  /**
   * Gets whether the plane orientation is locked
   */
  public isPlaneLocked(): boolean {
    return this.planeLocked;
  }

  /**
   * Sets whether the plane orientation is locked
   */
  public setPlaneLocked(locked: boolean): void {
    this.planeLocked = locked;
  }
}
