import {inject, Injectable} from '@angular/core';
import * as THREE from 'three';
import {RendererService} from './renderer.service';
import {
  getDefaultParticleColor,
  Particle,
  ParticleData,
  ParticleInfo,
  ParticleType,
  supportsColors
} from '../models/particle.model';
import {IntersectionPlaneService, PlaneOrientation} from './intersection-plane.service';
import {deepCopy} from '../../../util/deep-copy.util';

/**
 * Service responsible for managing particles in the scene
 */
@Injectable({
  providedIn: 'root'
})
export class ParticleManagerService {
  private particles: THREE.Mesh[] = [];
  private particleData: ParticleData = {
    particle_name: '',
    display_name: '',
    particle_type: ParticleType.TRAIL,
    lore: '',
    display_item: 'DIRT',
    permission: '',
    package_permission: '',
    frame_delay: 1,
    repeat: 1,
    repeat_delay: 0,
    random_offset: 0,
    stationary: true,
    frames: {
      'frame-0': []
    }
  };
  private currentFrame: string = 'frame-0';
  private frames: string[] = ['frame-0'];
  private selectedColor: string = '#ff0000';
  private selectedParticle: Particle = Particle.DUST;
  private supports_color: boolean = true;
  private selectedVelocity: number = 1;
  private selectedSize: number = 1;

  private onlyIntersecting: boolean = false;
  private readonly rendererService = inject(RendererService);
  private readonly intersectionPlaneService = inject(IntersectionPlaneService);

  constructor() {
    this.intersectionPlaneService.planeChanged$.subscribe(() => {
      if (this.onlyIntersecting) {
        this.clearParticleVisuals();
        this.renderFrameParticles(this.currentFrame);
      }
    });
  }

  /**
   * Adds a particle at the specified position
   */
  addParticle(x: number, y: number, z: number): void {
    const planeSize = this.intersectionPlaneService.stepSize;
    const divisions = Math.max(1, Math.floor(planeSize * this.intersectionPlaneService.getGridDensity()));
    const gridStepPlane = planeSize / divisions;

    if (this.intersectionPlaneService.getGridSnapEnabled()) {
      x = Math.round(x / gridStepPlane) * gridStepPlane;
      y = Math.round(y / gridStepPlane) * gridStepPlane;
      z = Math.round(z / gridStepPlane) * gridStepPlane;
    }
    // Create a visual representation of the particle
    const particleGeometry = new THREE.SphereGeometry(0.03 * this.selectedSize, 16, 16);
    const particleMaterial = new THREE.MeshBasicMaterial({color: this.selectedColor});
    const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);

    particleMesh.position.set(x, y, z);
    this.rendererService.scene.add(particleMesh);
    this.particles.push(particleMesh);

    // Add to particle data
    const hexColor = this.selectedColor.replace('#', '');

    //TODO make this work for more than just type DUST

    const particleInfo: ParticleInfo = {
      particle_type: this.selectedParticle,
      x: x,
      y: y,
      z: z,
      color: hexColor,
      // color_gradient_end: hexColor2,
      extra: this.selectedVelocity,
      size: this.selectedSize
    };

    if (!this.particleData.frames[this.currentFrame]) {
      this.particleData.frames[this.currentFrame] = [];
    }

    this.particleData.frames[this.currentFrame].push(particleInfo);
  }

  /**
   * Clears all particle visuals from the scene
   */
  clearParticleVisuals(): void {
    for (const particle of this.particles) {
      this.rendererService.scene.remove(particle);
    }
    this.particles = [];
  }

  /**
   * Renders particles for a specific frame
   */
  renderFrameParticles(frameId: string): void {
    if (!this.particleData.frames[frameId]) return;

    const filter = this.onlyIntersecting;
    const orientation = this.intersectionPlaneService.getCurrentOrientation();
    const offset16 = this.intersectionPlaneService.getPlanePosition();
    const planePos = offset16 / 16; // convert from 1/16th units to world units
    const epsilon = 0.02; // tolerance for intersection

    const isOnPlane = (p: ParticleInfo) => {
      if (!filter) return true;
      switch (orientation) {
        case PlaneOrientation.VERTICAL_ABOVE:
        case PlaneOrientation.VERTICAL_BELOW:
          // Horizontal plane at y = 0.8 +/- planePos
          return Math.abs(p.y - (0.8 + (orientation === PlaneOrientation.VERTICAL_BELOW ? planePos : -planePos))) <= epsilon;
        case PlaneOrientation.HORIZONTAL_FRONT:
          return Math.abs(p.z - planePos) <= epsilon;
        case PlaneOrientation.HORIZONTAL_BEHIND:
          return Math.abs(p.z + planePos) <= epsilon;
        case PlaneOrientation.HORIZONTAL_RIGHT:
          return Math.abs(p.x - planePos) <= epsilon;
        case PlaneOrientation.HORIZONTAL_LEFT:
          return Math.abs(p.x + planePos) <= epsilon;
      }
    };

    for (const particleInfo of this.particleData.frames[frameId]) {
      if (!isOnPlane(particleInfo)) {
        continue;
      }
      const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);

      const color = this.getColor(particleInfo);
      const particleMaterial = new THREE.MeshBasicMaterial({color});
      const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);

      particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
      this.rendererService.scene.add(particleMesh);
      this.particles.push(particleMesh);
    }
  }

  /**
   * Removes a particle from a specific frame
   */
  removeParticle(frameId: string, index: number): void {
    if (this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index) {
      this.particleData.frames[frameId].splice(index, 1);

      // Update visuals if this is the current frame
      if (frameId === this.currentFrame) {
        this.clearParticleVisuals();
        this.renderFrameParticles(frameId);
      }
    }
  }

  highlightParticle(frameId: string, index: number): void {
    if (!(this.particleData.frames[frameId] && this.particleData.frames[frameId].length > index)) {
      return;
    }
    const particleInfo = this.particleData.frames[frameId][index];
    const color = this.getColor(particleInfo);
    const particleMaterial = new THREE.MeshBasicMaterial({color});
    const particleGeometry = new THREE.SphereGeometry(0.03 * (particleInfo.size ?? 1), 16, 16);
    const particleMesh = new THREE.Mesh(particleGeometry, particleMaterial);
    particleMesh.position.set(particleInfo.x, particleInfo.y, particleInfo.z);
    this.rendererService.scene.add(particleMesh);
    this.particles.push(particleMesh);
    this.animatePulse(particleMesh, 3, () => {
      this.rendererService.scene.remove(particleMesh);
      this.clearParticleVisuals();
      this.renderFrameParticles(this.currentFrame);
    });
  }

  private animatePulse(mesh: THREE.Mesh, cycles: number, onComplete: () => void): void {
    const duration = 300;
    const maxScale = 0.08 / 0.03;
    const startTime = performance.now();

    const animate = (time: number) => {
      const elapsed = (time - startTime) % duration;
      const t = elapsed / (duration / 2);
      const scaleFactor = t <= 1 ? 1 + (maxScale - 1) * t : maxScale - (maxScale - 1) * (t - 1);
      mesh.scale.setScalar(scaleFactor);

      if (time - startTime >= duration * cycles) {
        onComplete();
      } else {
        requestAnimationFrame(animate);
      }
    };

    requestAnimationFrame(animate);
  }

  private getColor(particleInfo: ParticleInfo) {
    if (particleInfo.color) {
      const r = parseInt(particleInfo.color.substring(0, 2), 16) / 255;
      const g = parseInt(particleInfo.color.substring(2, 4), 16) / 255;
      const b = parseInt(particleInfo.color.substring(4, 6), 16) / 255;
      return new THREE.Color(r, g, b);
    } else {
      return new THREE.Color(255, 0, 0);
    }
  }

  /**
   * Sets the selected color for new particles
   */
  setSelectedColor(color: string): void {
    this.selectedColor = color;
  }

  public get supportsColor(): boolean {
    return this.supports_color;
  }

  /**
   * Gets the selected color
   */
  getSelectedColor(): string {
    return this.selectedColor;
  }

  public get particle(): Particle {
    return this.selectedParticle;
  }

  public set particle(selectedParticle: Particle) {
    this.selectedParticle = selectedParticle;
    this.supports_color = supportsColors(selectedParticle);
    if (!this.supports_color) {
      const defaultParticleColor = getDefaultParticleColor(selectedParticle);
      if (defaultParticleColor) {
        this.selectedColor = defaultParticleColor;
      }
    }
  }

  public get size(): number {
    return this.selectedSize;
  }

  public set size(selectedSize: number) {
    this.selectedSize = selectedSize;
  }

  public get velocity(): number {
    return this.selectedVelocity;
  }

  public set velocity(velocity: number) {
    this.selectedVelocity = velocity;
  }

  /**
   * Gets the particle data
   */
  getParticleData(): ParticleData {
    return this.particleData;
  }

  /**
   * Sets the particle data
   */
  setParticleData(data: ParticleData): void {
    this.particleData = data;
  }

  /**
   * Gets the current frame
   */
  getCurrentFrame(): string {
    return this.currentFrame;
  }

  /**
   * Sets the current frame
   */
  setCurrentFrame(frameId: string): void {
    this.currentFrame = frameId;
  }

  /**
   * Gets all frames
   */
  getFrames(): string[] {
    return this.frames;
  }

  /**
   * Sets all frames
   */
  setFrames(frames: string[]): void {
    this.frames = frames;
  }

  /**
   * Generates JSON output of the particle data
   */
  generateJson(): string {
    const particleData = deepCopy(this.particleData)
    if (this.particleData.package_permission) {
      particleData.package_permission = 'apart.set.' + this.particleData.package_permission.toLowerCase().replace(' ', '-');
    } else {
      particleData.package_permission = 'apart.set.none';
    }
    particleData.permission = 'apart.particle.' + this.particleData.permission.toLowerCase().replace(' ', '-');
    return JSON.stringify(this.particleData, null, 2);
  }

  public get onlyIntersectingParticles(): boolean {
    return this.onlyIntersecting;
  }

  public set onlyIntersectingParticles(value: boolean) {
    this.onlyIntersecting = value;
    this.clearParticleVisuals();
    this.renderFrameParticles(this.currentFrame);
  }

  public loadParticleData(data: string): void {
    this.particleData = JSON.parse(data);
    for (let framesKey in this.particleData.frames) {
      this.particleData.frames[framesKey].forEach((particleInfo: ParticleInfo) => {
        if (!particleInfo.color) {
          particleInfo.color = getDefaultParticleColor(particleInfo.particle_type);
        }
      })
    }
    this.setCurrentFrame('frame-0');
    this.frames = Object.keys(this.particleData.frames);
  }
}
