import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { RectAreaLightUniformsLib } from "three/examples/jsm/lights/RectAreaLightUniformsLib.js";

const SCENE_SETTINGS_URL = "./assets/preview3d_scenes.json";

const DEFAULT_DARK_SCENE = {
  renderer: { exposure: 1.04 },
  background: "#212121",
  floor: { color: "#2e2e2e", size: 16, visible: true },
  hemi: { sky: "#f6f9ff", ground: "#06070a", intensity: 0.42 },
  key: { color: "#ffffff", intensity: 1.25, position: [4.8, 6.5, 5.2] },
  fill: { color: "#7f9ed7", intensity: 0.1, position: [-5.2, 3.1, -3.8] },
  rim: { color: "#ffffff", intensity: 2.1, position: [-3.8, 4.8, 6.5] },
  rectAreaLights: [],
};

const state = {
  renderer: null,
  scene: null,
  camera: null,
  controls: null,
  floor: null,
  model: null,
  loadId: 0,
  raf: 0,
  sceneConfig: null,
  scenePromise: null,
  textures: [],
  currentProject: null,
  currentSlot: "helmet",
  appearance: {},
};

function cloneJson(value) {
  return JSON.parse(JSON.stringify(value || {}));
}

function vectorFromArray(value, fallback) {
  const source = Array.isArray(value) && value.length >= 3 ? value : fallback;
  return new THREE.Vector3(Number(source[0] || 0), Number(source[1] || 0), Number(source[2] || 0));
}

function colorValue(value, fallback) {
  return value == null ? fallback : value;
}

function numberValue(value, fallback, lo = -Infinity, hi = Infinity) {
  const number = Number(value);
  const next = Number.isFinite(number) ? number : fallback;
  return Math.max(lo, Math.min(hi, next));
}

function mergeDeep(base, override) {
  const result = { ...(base || {}) };
  Object.entries(override || {}).forEach(([key, value]) => {
    if (value && typeof value === "object" && !Array.isArray(value) && result[key] && typeof result[key] === "object" && !Array.isArray(result[key])) {
      result[key] = mergeDeep(result[key], value);
    } else {
      result[key] = Array.isArray(value) ? [...value] : value;
    }
  });
  return result;
}

async function loadDarkSceneConfig() {
  if (state.sceneConfig) return state.sceneConfig;
  if (!state.scenePromise) {
    state.scenePromise = (async () => {
      if (window.pywebview?.api?.load_preview3d_scene_settings) {
        try {
          const result = await window.pywebview.api.load_preview3d_scene_settings();
          const scene = result?.data?.settings?.scenes?.dark || result?.data?.scenes?.dark;
          if (result?.ok && scene) return mergeDeep(DEFAULT_DARK_SCENE, scene);
        } catch (_error) {
          // Fall back to bundled scene settings.
        }
      }
      try {
        const response = await fetch(SCENE_SETTINGS_URL, { cache: "no-store" });
        if (response.ok) {
          const data = await response.json();
          if (data?.scenes?.dark) return mergeDeep(DEFAULT_DARK_SCENE, data.scenes.dark);
        }
      } catch (_error) {
        // Fall back to local defaults.
      }
      return cloneJson(DEFAULT_DARK_SCENE);
    })();
  }
  state.sceneConfig = await state.scenePromise;
  return state.sceneConfig;
}

function disposeObject(object) {
  object?.traverse?.((child) => {
    child.geometry?.dispose?.();
    const materials = Array.isArray(child.material) ? child.material : child.material ? [child.material] : [];
    materials.forEach((material) => {
      Object.values(material || {}).forEach((value) => {
        if (value?.isTexture) value.dispose();
      });
      material?.dispose?.();
    });
  });
}

function clearTextures() {
  state.textures.forEach((texture) => texture?.dispose?.());
  state.textures = [];
}

function disposeDriverPreviewMaterials(root) {
  root?.traverse?.((child) => {
    if (!child.isMesh || !child.material) return;
    const materials = Array.isArray(child.material) ? child.material : [child.material];
    materials.forEach((material) => {
      if (!String(material?.name || "").includes("_DRIVER_PREVIEW")) return;
      material.map?.dispose?.();
      material.dispose?.();
    });
  });
}

function clearSceneObjects() {
  if (state.model) {
    state.scene?.remove?.(state.model);
    disposeObject(state.model);
  }
  state.model = null;
  clearTextures();
}

function applySceneConfig() {
  const config = state.sceneConfig || DEFAULT_DARK_SCENE;
  if (!state.scene || !state.renderer) return;
  state.renderer.toneMapping = THREE.ACESFilmicToneMapping;
  state.renderer.toneMappingExposure = numberValue(config.renderer?.exposure, 1.04, 0.1, 5);
  state.scene.background = new THREE.Color(colorValue(config.background, "#212121"));
  state.scene.environment = null;

  state.scene.children
    .filter((child) => child.userData?.driverPreviewLight)
    .forEach((child) => state.scene.remove(child));

  const hemi = config.hemi || {};
  const hemiLight = new THREE.HemisphereLight(
    colorValue(hemi.sky, "#f6f9ff"),
    colorValue(hemi.ground, "#06070a"),
    numberValue(hemi.intensity, 0.42, 0, 10),
  );
  hemiLight.userData.driverPreviewLight = true;
  state.scene.add(hemiLight);

  ["key", "fill", "rim"].forEach((name) => {
    const lightConfig = config[name] || {};
    const light = new THREE.DirectionalLight(
      colorValue(lightConfig.color, "#ffffff"),
      numberValue(lightConfig.intensity, name === "rim" ? 1.6 : 1, 0, 20),
    );
    light.position.copy(vectorFromArray(lightConfig.position, [4, 5, 4]));
    light.castShadow = true;
    light.userData.driverPreviewLight = true;
    state.scene.add(light);
  });

  RectAreaLightUniformsLib.init();
  (Array.isArray(config.rectAreaLights) ? config.rectAreaLights : []).forEach((item) => {
    const light = new THREE.RectAreaLight(
      colorValue(item.color, "#ffffff"),
      numberValue(item.intensity, 1, 0, 20),
      numberValue(item.width, 4, 0.1, 20),
      numberValue(item.height, 1, 0.1, 20),
    );
    light.position.copy(vectorFromArray(item.position, [0, 4, 2]));
    light.lookAt(vectorFromArray(item.target, [0, 0.5, 0]));
    light.userData.driverPreviewLight = true;
    state.scene.add(light);
  });

  if (state.floor) {
    state.scene.remove(state.floor);
    disposeObject(state.floor);
    state.floor = null;
  }
}

function ensureViewer(canvas) {
  if (state.renderer) {
    resize();
    return;
  }
  state.scene = new THREE.Scene();
  state.camera = new THREE.PerspectiveCamera(42, 1, 0.01, 200);
  state.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false, preserveDrawingBuffer: true });
  state.renderer.outputColorSpace = THREE.SRGBColorSpace;
  state.renderer.shadowMap.enabled = true;
  state.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  state.controls = new OrbitControls(state.camera, canvas);
  state.controls.enableDamping = true;
  state.controls.dampingFactor = 0.08;
  state.controls.screenSpacePanning = false;
  state.controls.minDistance = 0.08;
  state.controls.maxDistance = 12;
  resize();
  animate();
}

function animate() {
  cancelAnimationFrame(state.raf);
  const tick = () => {
    state.controls?.update?.();
    if (state.renderer && state.scene && state.camera) {
      state.renderer.render(state.scene, state.camera);
    }
    state.raf = requestAnimationFrame(tick);
  };
  tick();
}

function resize() {
  if (!state.renderer || !state.camera) return;
  const canvas = state.renderer.domElement;
  const rect = canvas.getBoundingClientRect();
  const width = Math.max(1, Math.floor(rect.width || canvas.clientWidth || 640));
  const height = Math.max(1, Math.floor(rect.height || canvas.clientHeight || 420));
  state.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
  state.renderer.setSize(width, height, false);
  state.camera.aspect = width / height;
  state.camera.updateProjectionMatrix();
}

async function loadModelDataUrl(modelPath) {
  const result = await window.pywebview.api.load_preview_model(modelPath);
  if (!result?.ok) throw new Error(result?.message || "Could not load driver preview model");
  return result.data.data_url;
}

async function loadTextureDataUrl(texturePath, slot) {
  const result = await window.pywebview.api.load_image_preview(texturePath, `driver-preview-${slot || "texture"}`);
  if (!result?.ok) throw new Error(result?.message || "Could not load driver preview texture");
  return result.data.data_url;
}

async function parseGltf(url) {
  const loader = new GLTFLoader();
  return await loader.loadAsync(url);
}

async function loadTexture(url) {
  return await new Promise((resolve, reject) => {
    const loader = new THREE.TextureLoader();
    loader.load(url, resolve, undefined, reject);
  });
}

function configurePreviewTexture(texture, { color = false } = {}) {
  if (!texture) return texture;
  if (color) texture.colorSpace = THREE.SRGBColorSpace;
  texture.flipY = false;
  texture.wrapS = THREE.RepeatWrapping;
  texture.wrapT = THREE.RepeatWrapping;
  texture.anisotropy = Math.min(state.renderer?.capabilities?.getMaxAnisotropy?.() || 1, 8);
  return texture;
}

function materialMatches(materialName, hints = []) {
  const name = String(materialName || "").toLowerCase();
  return hints.some((hint) => {
    const token = String(hint || "").toLowerCase();
    return token && name.includes(token);
  });
}

function createDriverMaterial(original, texture, binding, options = {}) {
  const slot = String(binding?.slot || "").toLowerCase();
  const isVisor = slot === "visor";
  const isSuit = slot === "suit";
  const material = new THREE.MeshStandardMaterial({
    name: `${original?.name || slot || "driver"}_DRIVER_PREVIEW`,
    map: texture,
    color: 0xffffff,
    metalness: isVisor ? 0 : 0.02,
    roughness: isVisor ? 0.18 : isSuit ? 0.62 : 0.46,
    transparent: isVisor || Boolean(texture.image),
    alphaTest: isVisor ? 0.02 : 0,
    opacity: isVisor ? 0.92 : 1,
    side: THREE.DoubleSide,
  });
  return material;
}

async function applyTextureBinding(root, binding) {
  const texturePath = String(binding?.texturePath || "").trim();
  if (!texturePath) return { slot: binding?.slot || "", applied: 0, texturePath };
  const textureUrl = await loadTextureDataUrl(texturePath, binding?.slot);
  const texture = configurePreviewTexture(await loadTexture(textureUrl), { color: true });
  state.textures.push(texture);

  const hints = Array.isArray(binding?.materialHints) ? binding.materialHints : [];
  let applied = 0;
  root.traverse((child) => {
    if (!child.isMesh || !child.material) return;
    const original = child.material;
    const materials = Array.isArray(original) ? original : [original];
    let changed = false;
    const next = materials.map((material) => {
      if (!materialMatches(material?.name, hints)) return material;
      applied += 1;
      changed = true;
      return createDriverMaterial(material, texture, binding);
    });
    if (changed) child.material = Array.isArray(original) ? next : next[0];
  });

  if (!applied && root) {
    root.traverse((child) => {
      if (applied || !child.isMesh || !child.material) return;
      child.material = createDriverMaterial(child.material, texture, binding);
      applied = 1;
    });
  }
  return { slot: binding?.slot || "", applied, texturePath };
}

function fitCamera(root, slot = "") {
  const bounds = new THREE.Box3().setFromObject(root);
  const size = new THREE.Vector3();
  const center = new THREE.Vector3();
  bounds.getSize(size);
  bounds.getCenter(center);
  const maxSize = Math.max(size.x, size.y, size.z, 0.05);
  const distance = maxSize / (2 * Math.tan(THREE.MathUtils.degToRad(state.camera.fov * 0.5)));
  const distanceScale = slot === "helmet" ? 1.55 : slot === "gloves" ? 1.95 : 1.35;
  const direction = slot === "suit"
    ? new THREE.Vector3(0.75, 0.35, 1)
    : new THREE.Vector3(0.72, 0.25, 1);
  direction.normalize();
  state.controls.target.copy(center);
  state.camera.position.copy(center).add(direction.multiplyScalar(distance * distanceScale));
  state.camera.near = Math.max(0.005, maxSize / 200);
  state.camera.far = Math.max(20, maxSize * 80);
  state.camera.updateProjectionMatrix();
  state.controls.update();
}

function modelBySlot(project, slot) {
  const models = Array.isArray(project?.models) ? project.models : [];
  return models.find((model) => model?.slot === slot && model.available) || models.find((model) => model?.available) || null;
}

async function load(project, options = {}) {
  const loadId = state.loadId + 1;
  state.loadId = loadId;
  const canvas = document.getElementById(options.canvasId || "driverPreviewCanvas");
  if (!canvas) throw new Error("Driver preview canvas is missing");
  ensureViewer(canvas);
  state.sceneConfig = await loadDarkSceneConfig();
  if (loadId !== state.loadId) throw new Error("Driver preview load was superseded");
  applySceneConfig();

  const slot = String(options.slot || project?.selectedSlot || "helmet").trim() || "helmet";
  const modelInfo = modelBySlot(project, slot);
  if (!modelInfo) throw new Error("No driver preview model is available");
  const modelUrl = await loadModelDataUrl(modelInfo.modelPath);
  const gltf = await parseGltf(modelUrl);
  if (loadId !== state.loadId) throw new Error("Driver preview load was superseded");

  clearSceneObjects();
  gltf.scene.name = `DriverPreview:${modelInfo.slot}`;
  gltf.scene.traverse((child) => {
    if (!child.isMesh) return;
    child.castShadow = true;
    child.receiveShadow = true;
  });
  state.model = gltf.scene;
  state.scene.add(state.model);
  state.currentProject = project;
  state.currentSlot = modelInfo.slot || slot;

  const applied = [];
  for (const binding of Array.isArray(modelInfo.textureBindings) ? modelInfo.textureBindings : []) {
    if (loadId !== state.loadId) throw new Error("Driver preview load was superseded");
    applied.push(await applyTextureBinding(state.model, binding));
  }
  fitCamera(state.model, modelInfo.slot);
  resize();
  return {
    slot: modelInfo.slot,
    label: modelInfo.label || modelInfo.slot,
    modelPath: modelInfo.modelPath,
    meshName: modelInfo.meshName || "",
    materialCount: Array.isArray(modelInfo.materials) ? modelInfo.materials.length : 0,
    textureBindings: applied,
    meshCount: countMeshes(state.model),
  };
}

async function reloadTextures(project = state.currentProject, options = {}) {
  if (!state.model) return await load(project, { ...options, slot: options.slot || state.currentSlot });
  const slot = String(options.slot || state.currentSlot || "helmet");
  const modelInfo = modelBySlot(project, slot);
  if (!modelInfo) throw new Error("No driver preview model is available");
  disposeDriverPreviewMaterials(state.model);
  clearTextures();
  const applied = [];
  for (const binding of Array.isArray(modelInfo.textureBindings) ? modelInfo.textureBindings : []) {
    applied.push(await applyTextureBinding(state.model, binding));
  }
  state.currentProject = project;
  state.currentSlot = modelInfo.slot || slot;
  return {
    slot: modelInfo.slot,
    label: modelInfo.label || modelInfo.slot,
    modelPath: modelInfo.modelPath,
    textureBindings: applied,
    meshCount: countMeshes(state.model),
  };
}

function setAppearance(appearance = {}) {
  state.appearance = {};
  if (state.currentProject) state.currentProject.appearance = {};
  return { appearance: {} };
}

function countMeshes(root) {
  let count = 0;
  root?.traverse?.((child) => {
    if (child.isMesh) count += 1;
  });
  return count;
}

function clear() {
  state.loadId += 1;
  clearSceneObjects();
  if (state.renderer && state.scene && state.camera) state.renderer.render(state.scene, state.camera);
  return { cleared: true };
}

window.addEventListener("resize", resize);

window.LiveryLabDriverPreview = {
  load,
  reloadTextures,
  setAppearance,
  clear,
  resize,
};
