import * as THREE from "three";

export interface SceneNode {
  readonly object: THREE.Object3D;
  setPosition(position: THREE.Vector3): void;
  setVisible(visible: boolean): void;
  setScaleScalar(scale: number): void;
  setRotationX(radians: number): void;
  setRotationY(radians: number): void;
  setRotationZ(radians: number): void;
  setRenderOrder(order: number): void;
  add(...children: SceneNode[]): void;
  clear(): void;
  lookAt(target: THREE.Vector3): void;
  getWorldPosition(target?: THREE.Vector3): THREE.Vector3;
  traverse(visitor: (child: THREE.Object3D) => void): void;
  setOpacity(opacity: number): void;
  setColor(color: THREE.ColorRepresentation): void;
  setEmissive(color: THREE.ColorRepresentation, intensity?: number): void;
}

class ThreeSceneNode implements SceneNode {
  constructor(public readonly object: THREE.Object3D) {}

  setPosition(position: THREE.Vector3) {
    this.object.position.copy(position);
  }

  setVisible(visible: boolean) {
    this.object.visible = visible;
  }

  setScaleScalar(scale: number) {
    this.object.scale.setScalar(scale);
  }

  setRotationX(radians: number) {
    this.object.rotation.x = radians;
  }

  setRotationY(radians: number) {
    this.object.rotation.y = radians;
  }

  setRotationZ(radians: number) {
    this.object.rotation.z = radians;
  }

  setRenderOrder(order: number) {
    this.object.renderOrder = order;
  }

  add(...children: SceneNode[]) {
    this.object.add(...children.map((child) => child.object));
  }

  clear() {
    if ("clear" in this.object && typeof this.object.clear === "function") {
      this.object.clear();
    }
  }

  lookAt(target: THREE.Vector3) {
    this.object.lookAt(target);
  }

  getWorldPosition(target = new THREE.Vector3()) {
    return this.object.getWorldPosition(target);
  }

  traverse(visitor: (child: THREE.Object3D) => void) {
    this.object.traverse(visitor);
  }

  setOpacity(opacity: number) {
    const visible = opacity > 0.02;
    this.object.visible = visible;
    this.object.traverse((child) => {
      if (!("material" in child)) {
        return;
      }
      const materials = Array.isArray(child.material) ? child.material : [child.material];
      for (const material of materials) {
        if (!("opacity" in material)) {
          continue;
        }
        material.transparent = true;
        material.opacity = opacity;
        material.needsUpdate = true;
      }
    });
  }

  setColor(color: THREE.ColorRepresentation) {
    this.object.traverse((child) => {
      if (!("material" in child)) {
        return;
      }
      const materials = Array.isArray(child.material) ? child.material : [child.material];
      for (const material of materials) {
        if ("color" in material) {
          material.color.set(color);
          material.needsUpdate = true;
        }
      }
    });
  }

  setEmissive(color: THREE.ColorRepresentation, intensity = 1) {
    this.object.traverse((child) => {
      if (!("material" in child)) {
        return;
      }
      const materials = Array.isArray(child.material) ? child.material : [child.material];
      for (const material of materials) {
        if ("emissive" in material) {
          material.emissive.set(color);
          if ("emissiveIntensity" in material) {
            material.emissiveIntensity = intensity;
          }
          material.needsUpdate = true;
        }
      }
    });
  }
}

export function createSceneNode<T extends THREE.Object3D>(object: T): SceneNode {
  return new ThreeSceneNode(object);
}

export function rawObject(node: SceneNode) {
  return node.object;
}

export function addToRawScene(scene: THREE.Scene, ...nodes: SceneNode[]) {
  scene.add(...nodes.map((node) => node.object));
}

export function registerSelectableTarget(
  selectableTargets: Map<THREE.Object3D, unknown>,
  node: SceneNode,
  selectable: unknown,
) {
  selectableTargets.set(node.object, selectable);
}

export function registerSelectableDescendants(
  selectableTargets: Map<THREE.Object3D, unknown>,
  node: SceneNode,
  selectable: unknown,
  predicate: (child: THREE.Object3D) => boolean,
) {
  node.traverse((child) => {
    if (predicate(child)) {
      selectableTargets.set(child, selectable);
    }
  });
}
