/* @jsx React.createElement */
// ─────────────────────────────────────────────────────────────────────
// METASPEAK Custom 3D Environment Loader
// ─────────────────────────────────────────────────────────────────────
// Lets the user upload their own .glb / .gltf scene to replace the
// default stage. Major capabilities:
//
//   1. Replace flow — uploading a new env disposes the previous one
//      cleanly. No overlap.
//
//   2. Hide default stage — when an environment is loaded, the
//      default Stage's visible meshes are hidden (floor, walls,
//      default screen) while its LIGHTS are preserved so the user's
//      environment is properly lit by the existing setup.
//
//   3. LED screen mesh designation — user picks which mesh inside
//      their GLB acts as the "LED screen". Auto-detected by name
//      patterns (screen, led, display, monitor, tv, billboard) on
//      load, with a manual dropdown picker as fallback. The picked
//      mesh's material is replaced with one that shares the default
//      stage's screen texture, so all kiosk image content (Cloudinary,
//      Wikimedia, Pixabay) renders ON the user's stage screen.
//
//   4. Mesh highlight — hovering an option in the picker briefly
//      pulses that mesh's emissive glow blue, so the user can verify
//      they're picking the right mesh in scenes with cryptic naming.
//
//   5. Transform — scale / position (X, Y, Z) / rotation (Y) sliders
//      for the whole environment. Y is critical for floor alignment
//      with avatar feet.
//
//   6. Material multipliers — global roughness / metalness / emissive
//      multipliers preserve the artist's original material design
//      while letting the user nudge for kiosk lighting.
//
// Persistence:
//   - GLB binary cached to IndexedDB at key 'env:main' (reuses anim store)
//   - Transform + material settings are tweaks (localStorage-backed)
//   - Screen mesh selection saved as `envScreenMesh` tweak

(function() {

const SCREEN_MESH_PATTERNS = [
  /\bscreen\b/i,
  /\bled\b/i,
  /\bdisplay\b/i,
  /\bmonitor\b/i,
  /\btv\b/i,
  /\bbillboard\b/i,
  /\bvideowall\b/i,
];

class EnvironmentManager {
  constructor(scene, opts = {}) {
    this.scene = scene;
    this.stage = opts.stage || null;          // for hide/show + screen texture sharing
    this.root = new THREE.Group();
    this.root.name = 'UserEnvironment';
    scene.add(this.root);

    // Loaded GLB content
    this.gltf = null;
    this.modelRoot = null;
    this.materials = [];
    this.originals = new Map();
    this.fileName = null;

    // Mesh inventory — { name, uuid, mesh } for picker dropdown
    this.meshes = [];
    // The mesh currently designated as the LED screen
    this.screenMesh = null;
    // Cache of the screen mesh's ORIGINAL material so we can restore
    // it if user picks a different mesh later
    this.screenMeshOriginalMat = null;

    // Loader (reuse global if already on window)
    this.loader = window.GLTFLoader ? new window.GLTFLoader() : null;

    // Highlight state — the mesh currently being "hover-highlighted"
    // in the panel picker, plus its original emissive for restore
    this.highlightedMesh = null;
    this.highlightOriginalEmissive = null;
    this.highlightOriginalIntensity = 0;
    this.highlightT = 0;
  }

  /**
   * Load a GLB/GLTF buffer as the user environment.
   * @returns {Promise<{materials: number, meshes: number, screenCandidates: string[], allMeshes: string[]}>}
   */
  async load(buffer, fileName = 'environment.glb') {
    if (!this.loader) throw new Error('GLTFLoader not available');
    this.unload();

    return new Promise((resolve, reject) => {
      this.loader.parse(buffer, '', (gltf) => {
        try {
          this.gltf = gltf;
          this.modelRoot = gltf.scene || gltf.scenes?.[0];
          if (!this.modelRoot) throw new Error('GLTF has no scene');
          this.fileName = fileName;
          this.root.add(this.modelRoot);

          // Walk the tree once: collect meshes (for picker) + materials
          // (for override sliders) + record originals for both.
          this.materials = [];
          this.originals.clear();
          this.meshes = [];
          let meshCount = 0;
          this.modelRoot.traverse((obj) => {
            if (obj.isMesh) {
              meshCount++;
              obj.castShadow = true;
              obj.receiveShadow = true;
              this.meshes.push({
                name: obj.name || `mesh_${meshCount}`,
                uuid: obj.uuid,
                mesh: obj,
              });
              const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
              mats.forEach(mat => {
                if (!mat || this.originals.has(mat)) return;
                this.materials.push(mat);
                this.originals.set(mat, {
                  roughness: mat.roughness ?? 1,
                  metalness: mat.metalness ?? 0,
                  emissiveIntensity: mat.emissiveIntensity ?? 0,
                });
              });
            }
          });

          // Auto-detect screen-like mesh by name pattern
          const screenCandidates = this.meshes
            .filter(m => SCREEN_MESH_PATTERNS.some(p => p.test(m.name)))
            .map(m => m.name);

          // Hide default stage's visible meshes (preserve its lights so
          // the user's GLB is well-lit out of the box).
          if (this.stage?.setVisibleMeshes) {
            this.stage.setVisibleMeshes(false);
          }

          console.log(
            `[Environment] loaded "${fileName}" — ${meshCount} meshes, ` +
            `${this.materials.length} materials, ` +
            `${screenCandidates.length} screen candidate${screenCandidates.length === 1 ? '' : 's'}`
          );
          resolve({
            materials: this.materials.length,
            meshes: meshCount,
            screenCandidates,
            allMeshes: this.meshes.map(m => m.name),
          });
        } catch (e) { reject(e); }
      }, reject);
    });
  }

  /**
   * Remove the loaded environment, dispose GPU resources, and restore
   * the default stage's visibility.
   */
  unload() {
    this.clearHighlight();
    // Restore screen mesh's original material before disposing scene
    if (this.screenMesh && this.screenMeshOriginalMat) {
      this.screenMesh.material = this.screenMeshOriginalMat;
    }
    this.screenMesh = null;
    this.screenMeshOriginalMat = null;

    if (this.modelRoot) {
      this.root.remove(this.modelRoot);
      this.modelRoot.traverse((obj) => {
        if (obj.isMesh) {
          obj.geometry?.dispose?.();
          const mats = Array.isArray(obj.material) ? obj.material : [obj.material];
          mats.forEach(mat => {
            if (!mat) return;
            for (const k in mat) {
              const v = mat[k];
              if (v && v.isTexture) v.dispose();
            }
            mat.dispose?.();
          });
        }
      });
    }
    this.gltf = null;
    this.modelRoot = null;
    this.materials = [];
    this.originals.clear();
    this.meshes = [];
    this.fileName = null;

    // Restore default stage visibility
    if (this.stage?.setVisibleMeshes) {
      this.stage.setVisibleMeshes(true);
    }
  }

  isLoaded() { return !!this.modelRoot; }

  setTransform({ posX = 0, posY = 0, posZ = 0, scale = 1, rotY = 0 }) {
    this.root.position.set(posX, posY, posZ);
    this.root.scale.setScalar(scale);
    this.root.rotation.set(0, rotY * Math.PI / 180, 0);
  }

  applyMaterialOverrides({ roughness = 1, metalness = 1, emissive = 1 } = {}) {
    for (const mat of this.materials) {
      const orig = this.originals.get(mat);
      if (!orig) continue;
      mat.roughness = Math.min(1, Math.max(0, orig.roughness * roughness));
      mat.metalness = Math.min(1, Math.max(0, orig.metalness * metalness));
      mat.emissiveIntensity = Math.max(0, orig.emissiveIntensity * emissive);
      mat.needsUpdate = true;
    }
  }

  /**
   * Designate a mesh (by name) as the LED screen. The mesh's material
   * is swapped for a basic material that shares the default stage's
   * screen canvas texture, so all setScreenImage() calls render here.
   *
   * @param {string|null} meshName   null to clear designation
   * @returns {boolean} true if assigned, false if name not found
   */
  setScreenMesh(meshName) {
    // Clear any current screen designation first (restore original mat)
    if (this.screenMesh && this.screenMeshOriginalMat) {
      this.screenMesh.material = this.screenMeshOriginalMat;
      this.screenMesh = null;
      this.screenMeshOriginalMat = null;
    }
    if (!meshName) return true;
    const entry = this.meshes.find(m => m.name === meshName);
    if (!entry) {
      console.warn(`[Environment] screen mesh "${meshName}" not found`);
      return false;
    }
    if (!this.stage?.screenTexture) {
      console.warn('[Environment] no stage screen texture available — cannot assign');
      return false;
    }
    this.screenMeshOriginalMat = entry.mesh.material;
    // MeshBasicMaterial: unaffected by lighting (LED screens emit their
    // own light). toneMapped=false keeps color accuracy. DoubleSide so
    // paper-thin plane meshes render from any angle.
    const screenMat = new THREE.MeshBasicMaterial({
      map: this.stage.screenTexture,
      toneMapped: false,
      side: THREE.DoubleSide,
    });
    entry.mesh.material = screenMat;
    this.screenMesh = entry.mesh;
    console.log(`[Environment] screen assigned to mesh "${meshName}"`);
    return true;
  }

  /**
   * Briefly highlight a mesh by pulsing its emissive. Used by the
   * picker's hover preview. Pulsing is driven by update().
   */
  highlightMesh(meshName) {
    this.clearHighlight();
    if (!meshName) return;
    const entry = this.meshes.find(m => m.name === meshName);
    if (!entry) return;
    const mat = Array.isArray(entry.mesh.material) ? entry.mesh.material[0] : entry.mesh.material;
    if (!mat || !mat.emissive) return;
    this.highlightedMesh = entry.mesh;
    this.highlightOriginalEmissive = mat.emissive.clone();
    this.highlightOriginalIntensity = mat.emissiveIntensity ?? 1;
    this.highlightT = 0;
    mat.emissive.setHex(0x4ba3c7);   // vivid blue
    mat.emissiveIntensity = 1.5;
    mat.needsUpdate = true;
  }

  clearHighlight() {
    if (!this.highlightedMesh) return;
    const mat = Array.isArray(this.highlightedMesh.material) ? this.highlightedMesh.material[0] : this.highlightedMesh.material;
    if (mat && this.highlightOriginalEmissive) {
      mat.emissive.copy(this.highlightOriginalEmissive);
      mat.emissiveIntensity = this.highlightOriginalIntensity;
      mat.needsUpdate = true;
    }
    this.highlightedMesh = null;
    this.highlightOriginalEmissive = null;
  }

  /**
   * Frame update — call from the render loop. Pulses the highlight if
   * one is active.
   */
  update(dt) {
    if (this.highlightedMesh) {
      this.highlightT += dt;
      const mat = Array.isArray(this.highlightedMesh.material) ? this.highlightedMesh.material[0] : this.highlightedMesh.material;
      if (mat && mat.emissive) {
        // Pulse between 0.5 and 2.5 at ~3 Hz
        const pulse = 1.5 + 1.0 * Math.sin(this.highlightT * Math.PI * 6);
        mat.emissiveIntensity = pulse;
        mat.needsUpdate = true;
      }
    }
  }

  /**
   * Public accessor for the picker UI — returns sorted mesh names
   * with screen-candidate ones first, marked.
   */
  getMeshList() {
    return this.meshes
      .map(m => ({
        name: m.name,
        isScreenCandidate: SCREEN_MESH_PATTERNS.some(p => p.test(m.name)),
      }))
      .sort((a, b) => {
        if (a.isScreenCandidate !== b.isScreenCandidate) return b.isScreenCandidate - a.isScreenCandidate;
        return a.name.localeCompare(b.name);
      });
  }
}

window.MetaspeakEnvironment = { EnvironmentManager, SCREEN_MESH_PATTERNS };

})();
