// viewer-utils.js
import THREE from "./init.js";
import { core, setCore } from './core.js';
import TWEEN, { add } from "three/examples/jsm/libs/tween.module.js";
import { normalizeColor, parseCssColor } from './utils.js';
import { t } from "./i18n-utils.js";

export const initClippingPlanes = () => {
  const clippingPlanes = [
    new THREE.Plane(new THREE.Vector3(-1, 0, 0), 0),
    new THREE.Plane(new THREE.Vector3(0, -1, 0), 0),
    new THREE.Plane(new THREE.Vector3(0, 0, -1), 0),
  ];
  setCore('clippingPlanes', clippingPlanes);
  return clippingPlanes;
};

const scaleXYZ = (v, s) =>
  ['x', 'y', 'z'].forEach(k => v[k] *= s);

const DEFAULT_NOTICE_DURATION = 4200;

function normalizeNoticeArgs(toneOrOptions, maybeOptions) {
  let tone = "info";
  let options = {};

  if (typeof toneOrOptions === "string") {
    tone = toneOrOptions;
    options = maybeOptions ?? {};
  } else if (toneOrOptions && typeof toneOrOptions === "object") {
    options = toneOrOptions;
    tone = toneOrOptions.tone ?? "info";
  }

  return { tone, options };
}

export const toastHelper = (key, toneOrOptions, maybeOptions) => {
  return showToast(`toasts.${key}`, toneOrOptions, maybeOptions);
};

export const showToast = (message, toneOrOptions, maybeOptions) => {
  if (!core.showNotifications) return;
  const { tone, options } = normalizeNoticeArgs(toneOrOptions, maybeOptions);

  const duration = Number.isFinite(options.duration)
    ? options.duration
    : DEFAULT_NOTICE_DURATION;

  const key = String(options.key ?? "");
  const replace = options.replace === true;
  const persistent = options.persistent === true;
  const variant = String(options.variant ?? "");
  let i18nKey = "";
  let i18nVars = {};
  const detailI18nKey = String(options.detailI18nKey ?? "");
  const detailI18nVars = options.detailVars && typeof options.detailVars === "object" ? options.detailVars : options;

  // Resolve i18n key if possible, otherwise use the message as-is (for backward compatibility)
  let text;
  let detail = "";

  if (typeof message === "string" && message.includes(".")) {
    // try to resolve as i18n key with optional variables
    i18nKey = message;
    i18nVars = options;
    text = t(message, options);
  } else {
    // fallback (old way)
    text = String(message);
  }

  if (detailI18nKey) {
    detail = t(detailI18nKey, detailI18nVars);
  } else if (options.detail != null) {
    detail = String(options.detail);
  }

  if (window.__E2E__ && window.viewer) {
    window.viewer.toasts ??= [];
    window.viewer.toasts.push([text, detail].filter(Boolean).join(" "));
  }

  const statusNotice = core.statusNotice;
  const enqueueStatusNotice = core.enqueueStatusNotice;

  if (typeof enqueueStatusNotice === "function") {
    enqueueStatusNotice({ message: text, detail, tone, duration, key, replace, persistent, variant, i18nKey, i18nVars, detailI18nKey, detailI18nVars });
    return;
  }

  if (!statusNotice) {
    console.info(`[viewer:${tone}] ${text}`);
    return;
  }

  statusNotice.hidden = false;
  statusNotice.textContent = [text, detail].filter(Boolean).join(" ");
  statusNotice.dataset.tone = tone;
  statusNotice.classList.remove("is-visible", "is-hiding");

  void statusNotice.offsetWidth;
  statusNotice.classList.add("is-visible");

  if (core.statusNoticeTimer) {
    window.clearTimeout(core.statusNoticeTimer);
  }

  core.statusNoticeTimer = window.setTimeout(() => {
    statusNotice.classList.remove("is-visible");
    statusNotice.classList.add("is-hiding");

    window.setTimeout(() => {
      if (!statusNotice.classList.contains("is-visible")) {
        statusNotice.hidden = true;
        statusNotice.classList.remove("is-hiding");
      }
    }, 220);
  }, duration);
};

export function getErrorMessage(error) {
  if (error instanceof Error && error.message) return error.message;
  if (typeof error === "string") return error;
  return String(error);
}

export function reportViewerError(error, options = {}) {
  const {
    context = "",
    consoleLabel = "Viewer error",
    toast = true,
    e2e = true,
  } = options;

  const baseMessage = getErrorMessage(error);
  const message = context ? `${context}: ${baseMessage}` : baseMessage;

  console.error(consoleLabel, error);

  if (e2e && window.__E2E__ && window.viewer) {
    window.viewer.errors ??= [];
    window.viewer.errors.push(message);
  }

  if (toast) {
    showToast(message);
  }

  return message;
}

function fetchObjectFromConfig(_name) {
  //console.log("Fetching config for", _name, core.objectsConfig);
  return core.objectsConfig?.models?.find(model => model.name === _name);
}

function normalizeVec3(v) {
  if (!v) return null;

  if (Array.isArray(v) && v.length === 3) {
    return { x: v[0], y: v[1], z: v[2] };
  }

  if (
    typeof v === "object" &&
    typeof v.x === "number" &&
    typeof v.y === "number" &&
    typeof v.z === "number"
  ) {
    return v;
  }

  return null;
}


function normalizeGradient(gradient) {
  return {
    type: gradient.type,
    shapeOrDirection: gradient.shapeOrDirection,
    colors: gradient.colors
      .map(normalizeColor)
      .filter(Boolean)
  };
}

function setupObjectHandler(_object, _metadata) {
  if (!_metadata) return;

  const pos = normalizeVec3(_metadata.position ?? _metadata.objPosition);
  if (pos) {
    _object.position.set(pos.x, pos.y, pos.z);
  }

  const scale = normalizeVec3(_metadata.scale ?? _metadata.objScale);
  if (scale) {
    _object.scale.set(scale.x, scale.y, scale.z);
  }

  const rot = normalizeVec3(_metadata.rotation ?? _metadata.objRotation);
  if (rot) {
    _object.rotation.set(
      THREE.MathUtils.degToRad(rot.x),
      THREE.MathUtils.degToRad(rot.y),
      THREE.MathUtils.degToRad(rot.z)
    );
  }
}

function setupGeometryHandler (_object) {
  _object.needsUpdate = true;
  if (typeof _object.geometry !== "undefined") {
    _object.geometry.computeBoundingBox();
    _object.geometry.computeBoundingSphere();
  }
  _object.updateMatrix();
  _object.updateMatrixWorld(true);
}

function centerObjectAtOrigin(_object) {
  const boundingBox = new THREE.Box3();
  boundingBox.setFromObject(_object, true);
  if (boundingBox.isEmpty()) return;

  const center = new THREE.Vector3();
  boundingBox.getCenter(center);
  _object.position.sub(center);
  _object.updateMatrixWorld(true);
}

function setupCameraHandler(_object, meta) {
  if (!meta) return;

  const target = normalizeVec3(meta.controlsTarget);
  const camPos = normalizeVec3(meta.cameraPosition);

  if (!target && !camPos) return;

  const wasDamping = core.controls.enableDamping;
  core.controls.enableDamping = false;

  if (target) {
    core.controls.target?.set(target.x, target.y, target.z);
  }

  if (camPos) {
    core.camera.position?.set(camPos.x, camPos.y, camPos.z);
  }

  core.camera.updateProjectionMatrix();
  core.controls.update();
  core.controls.saveState();
  core.controls.enableDamping = wasDamping;
}

export const setupObject = (_object, _metadata) => {
  let model;
  if (typeof _object.children === "undefined" || _object.children.length == 0) {
    model = fetchObjectFromConfig(_object.name);
  } else if (_object.children.length > 0) {
    model = fetchObjectFromConfig(_object.children[0].name); //TODO: check for multiple objects
  }

  if (_metadata != null) {
    setupObjectHandler(_object, _metadata);
    setupGeometryHandler(_object);
    setupCameraHandler(_object, _metadata);
  }
  else if (typeof core.objectsConfig !== "undefined" && model) { //Setup from config
    if ((!Array.isArray(core.objectsConfig.models) || core.objectsConfig.models.length === 0) && _metadata == null) {
      if (model.position != null) _object.position.set(model.position.x, model.position.y, model.position.z);

      if (model.scale != null) _object.scale.set(model.scale.x, model.scale.y, model.scale.z);
      
      if (model.rotation != null) _object.rotation.set(THREE.MathUtils.degToRad(model.rotation.x), THREE.MathUtils.degToRad(model.rotation.y), THREE.MathUtils.degToRad(model.rotation.z));
    } else {
      let m = core.objectsConfig.models[core.objectsConfig.setupIndex];
      if (m != undefined && _metadata == null) {
        //console.log("Applying config for index", core.objectsConfig.setupIndex, m);
        setupObjectHandler(_object, m);
      } else if (_metadata != null) {
        // Fallback to metadata
        setupObjectHandler(_object, _metadata);
      }        
    }
    setupGeometryHandler(_object);
  }
  else {
    var boundingBox = new THREE.Box3();
    if (Array.isArray(_object)) {
      for (let i = 0; i < _object.length; i++) {
        boundingBox.setFromObject(_object[i]);
        _object[i].position.set(
          -(boundingBox.min.x + boundingBox.max.x) / 2,
          -boundingBox.min.y,
          -(boundingBox.min.z + boundingBox.max.z) / 2
        );
        _object[i].needsUpdate = true;
        if (typeof _object[i].geometry !== "undefined") {
          _object[i].geometry.computeBoundingBox();
          _object[i].geometry.computeBoundingSphere();
        }
        _object[i].updateMatrixWorld();
      }
    } else if (_object.isGroup) {
      // Keep non-presentation behavior, but center fully in presentation mode.
      if (core.PRESENTATION_MODE) {
        centerObjectAtOrigin(_object);
      } else {
        //workaround for specific Group case
        boundingBox.setFromObject(_object);
        _object.position.set(-(boundingBox.min.x+boundingBox.max.x)/2, -boundingBox.min.y, -(boundingBox.min.z+boundingBox.max.z)/2);
        _object.updateMatrixWorld();
      }
    } else if (!core.PRESENTATION_MODE) {
      boundingBox.setFromObject(_object);
      _object.position.set((boundingBox.max.x - boundingBox.min.x ) / 2, (boundingBox.max.y - boundingBox.min.y) / 2, (boundingBox.max.z - boundingBox.min.z ) / 2);
      setupGeometryHandler(_object);
    } else {
      // In presentation mode keep local hierarchy transforms and center only the whole object.
      centerObjectAtOrigin(_object);
      setupGeometryHandler(_object);
    }
  }

  core.cameraLight.position.set(
    core.camera.position.x,
    core.camera.position.y,
    core.camera.position.z
  );
  if (Array.isArray(_object)) {
    core.cameraLightTarget.position.set(
      _object[0].position.x,
      _object[0].position.y,
      _object[0].position.z
    );
  } else {
    core.cameraLightTarget.position.set(
      _object.position.x,
      _object.position.y,
      _object.position.z
    );
  }
  core.cameraLight.target.updateMatrixWorld();
  core.objectsConfig.setupIndex++;
}

async function setupEmptyCamera(_object) {
  console.log("Setting up empty camera");
  var boundingBox = new THREE.Box3();
  if (Array.isArray(_object)) {
    for (let i = 0; i < _object.length; i++) {
      const box = new THREE.Box3().setFromObject(_object[i], true);
      boundingBox.union(box);
    }
  } else {
    boundingBox.setFromObject(_object, true);
  }
  var size = new THREE.Vector3();
  boundingBox.getSize(size);
  var center = new THREE.Vector3();
  boundingBox.getCenter(center);
  // Set camera position at the center level, behind the model
  const distance = size.length();
  core.camera.position.set(center.x, center.y, center.z + distance);
  await fitCameraToCenteredObject(_object, true);
}

function parseColor(v) {
  if (Array.isArray(v)) {
    const [r, g, b, a = 1] = v;
    return { r, g, b, a };
  }

  if (typeof v === "string") {
    return parseCssColor(v); // #hex / rgb / rgba
  }

  return null;
}

function parseGradientArray(arr) {
  if (!Array.isArray(arr) || arr.length === 0) return null;

  // [r, g, b] → single color
  if (
    arr.length === 3 &&
    arr.every(v => typeof v === "number")
  ) {
    return {
      type: "linear",
      colors: [ { r: arr[0], g: arr[1], b: arr[2], a: 1 } ]
    };
  }

  // list of colors
  const colors = arr
    .map(parseColor)
    .filter(Boolean);

  if (colors.length < 2) return null;

  return {
    type: "linear",
    colors
  };
}

function resolveBackground(meta, sceneId) {
  const raw =
    meta.scenes?.[sceneId]?.background ??
    meta.scene?.background ??
    meta.globals?.background ??
    null;

  if (!raw) return { kind: "default" };

  // object + array of colors
  if (typeof raw === "object" && Array.isArray(raw.value)) {
    const gradient = parseGradientArray(raw.value);
    if (gradient) {
      const normalizedGradient = normalizeGradient(gradient);
      return { kind: "gradient", normalizedGradient };
    }
  }

  // css string
  if (typeof raw === "string") {
    const gradient = parseGradient(raw);
    if (gradient) {
      const normalizedGradient = normalizeGradient(gradient);
      return { kind: "gradient", normalizedGradient };
    }
    return { kind: "color", color: raw };
  }

  return { kind: "default" };
}

export async function setupCamera(_object, _data) {
  const _light = core.lightObjects[0];
  const cfg = _data ?? core.CONFIG ?? null;
  const fallback = _data ?? core.objectsConfig ?? null;

  // --- CAMERA POSITION ---
  const camPos = cfg?.cameraPosition ?? fallback?.camera?.position;

  if (Array.isArray(camPos)) {
    core.camera.position.set(camPos[0], camPos[1], camPos[2]);
  } else if (camPos && typeof camPos === "object") {
    core.camera.position.set(camPos.x, camPos.y, camPos.z);
  } else {
    await setupEmptyCamera(_object);
  }

  // --- CONTROLS TARGET + ZOOM ---
  const target = cfg?.controlsTarget ?? fallback?.camera?.target;

  if (Array.isArray(target)) {
    core.controls.target.set(target[0], target[1], target[2]);
  } else if (target) {
    core.controls?.target.set(target.x, target.y, target.z);
  }

  const customZoom = cfg?.controlsZoom?.[0];

  if (typeof customZoom === "number" && customZoom !== 0) {
    const dir = new THREE.Vector3()
    .subVectors(core.camera.position, core.controls?.target || new THREE.Vector3())
    .normalize();


    core.camera.position
    .copy(core.controls?.target || new THREE.Vector3())
    .add(dir.multiplyScalar(customZoom));
  }

  // --- LIGHTS ---
  if (!cfg && fallback?.scene?.lights) {
    fallback.scene.lights.forEach(light => {
      switch (light.type) {
        case "directional":
        _light.position.set(light.position.x, light.position.y, light.position.z);
        _light.color = new THREE.Color(normalizeColor(light.color));
        _light.intensity = light.intensity;
        break;
        case "ambient":
        core.ambientLight.color = new THREE.Color(normalizeColor(light.color));
        core.ambientLight.intensity = light.intensity;
        break;
        case "point":
        core.cameraLight.color = new THREE.Color(normalizeColor(light.color));
        core.cameraLight.intensity = light.intensity;
        break;
      }
    });
  } 
  else if (cfg) {
    if (cfg.lightAmbientColor) {
      core.ambientLight.color = new THREE.Color(normalizeColor(cfg.lightAmbientColor[0]));
      core.ambientLight.intensity = cfg.lightAmbientIntensity?.[0] ?? core.ambientLight.intensity;
    }

    if (cfg.lightColor) {
      _light.color = new THREE.Color(normalizeColor(cfg.lightColor[0]));
      _light.intensity = cfg.lightIntensity?.[0] ?? _light.intensity;
    }

    if (cfg.lightCameraColor) {
      core.cameraLight.color = new THREE.Color(normalizeColor(cfg.lightCameraColor[0]));
      core.cameraLight.intensity = cfg.lightCameraIntensity?.[0] ?? core.cameraLight.intensity;
    }
  }

  // --- BACKGROUND ---
  //const sceneBg = fallback?.scene?.background;

  if (!core.PRESENTATION_MODE) {
    const bg = resolveBackground(fallback, core.activeScene);

    switch (bg.kind) {
      case "gradient":
      case "radial":
        applyGradientCss(bg.normalizedGradient);
        break;

      case "color":
      case "linear":
        changeBackground("linear", bg.color);
        break;

      case "default":
      case "unknown":
        changeBackground(
          "radial",
          core.colors.BackgroundColor,
          core.colors.BackgroundColorOuter
        );
        break;
    }
  } else {
    const grandient = {type: "radial", colors: [{r: 255, g: 255, b: 255, a: 0}, {r: 255, g: 255, b: 255, a: 0}]};
    applyGradientCss(grandient);
  }

  core.camera.updateProjectionMatrix();
  core.controls?.update();

  await fitCameraToCenteredObject(_object, false, cfg);
}

  // Show interaction hint on first load
  function showInteractionHint(boxCenter) {
  if (window.__E2E__) return;
  //if (localStorage.getItem("viewerHintSeen")) return;

  if (core.GESTURE == null) return;
  core.GESTURE.rotate = true;

  core.GESTURE.target = boxCenter.clone();
  core.controls.target.copy(core.GESTURE.target);

  if (core.handHint == null) return;
  core.handHint.hidden = false;
  core.handHint.classList.add("hand-drag-animate");
}

function animateCameraToPose ({
  finalCameraPos,     // THREE.Vector3 (target camera position)
  finalTarget,        // THREE.Vector3 (target)
  boundingBox,        // THREE.Box3 (optional, near/far)
  duration = 3500,
  easing = TWEEN.Easing.Cubic.Out,
  startOffsetFactor = 0.5, // % of moving back (0.2–0.4 should be good)
  animate = true,
  distanceOffsetFactor = 0,   // additional factor to move closer (0.1 = 10% closer) (optional)
  distanceOffsetUnits  = 0,   // additional world units to move closer (optional)
}) {

  const endCamPos = finalCameraPos.clone();
  const endTarget = finalTarget.clone();

  const dir = endCamPos.clone().sub(endTarget).normalize();

  const baseDistance = endCamPos.distanceTo(endTarget);

  const distanceOffset =
    baseDistance * distanceOffsetFactor + distanceOffsetUnits;

  const startCamPos = endCamPos.clone().add(
    dir.multiplyScalar(
      baseDistance * startOffsetFactor + distanceOffset
    )
  );
  const startTarget = endTarget.clone(); // target

  if (!animate) {
    core.camera.position.copy(endCamPos);
    core.controls?.target.copy(endTarget);
    core.controls?.update();
    return;
  }

  core.camera.position.copy(startCamPos);
  core.controls?.target.copy(startTarget);
  core.controls?.update();

  const camTweenPos = startCamPos.clone();
  const targetTweenPos = startTarget.clone();

  core.cameraTween = new TWEEN.Tween(camTweenPos)
    .to(endCamPos, duration)
    .easing(easing)
    .onUpdate(() => {
      core.camera.position.copy(camTweenPos);
    });

  core.targetTween = new TWEEN.Tween(targetTweenPos)
    .to(endTarget, duration)
    .easing(easing)
    .onUpdate(() => {
      core.controls?.target.copy(targetTweenPos);
      core.controls?.update();
    });

  core.cameraTween.start();
  core.targetTween.start();

  // === (near / far / limits) ===
  core.cameraTween.onComplete(() => {
    core.camera.position.copy(endCamPos);
    core.controls?.target.copy(endTarget);
    core.controls?.update();
    const boxCenter = boundingBox ? boundingBox.getCenter(new THREE.Vector3()) : new THREE.Vector3();
    if (boundingBox) {
      const boxSize = boundingBox.getSize(new THREE.Vector3()).length();

      const maxDistance =
        endCamPos.distanceTo(boxCenter) + boxSize;

      core.camera.near = Math.max(maxDistance / 1000, 0.001);
      core.camera.far  = maxDistance * 10;
      core.camera.updateProjectionMatrix();

      if (core.controls) {
        core.controls.maxDistance = maxDistance * 2;
      }
    }
    showInteractionHint(boxCenter);
  });
}

async function fitCameraToCenteredObject(object, _fit, cfg) {
  const boundingBox = new THREE.Box3();
  if (Array.isArray(object)) {
    for (let i = 0; i < object.length; i++) {
      const box = new THREE.Box3().setFromObject(object[i], true);
      boundingBox.union(box);
    }
  } else {
    boundingBox.setFromObject(object, true);
  }

  var size = new THREE.Vector3(), center = new THREE.Vector3();
  boundingBox.getSize(size);
  boundingBox.getCenter(center); // center point
  // ground
  var distance1 = new THREE.Vector3(
    Math.abs(boundingBox.max.x - boundingBox.min.x),
    Math.abs(boundingBox.max.y - boundingBox.min.y),
    Math.abs(boundingBox.max.z - boundingBox.min.z)
  );
  core.gridSize = Math.max(distance1.x, distance1.y, distance1.z);

  core.dirLightTarget = new THREE.Object3D();
  core.dirLightTarget.position.set(0, 0, 0);

  core.lightHelper = new THREE.DirectionalLightHelper(core.dirLight, core.gridSize);
  core.scene.add(core.lightHelper);
  core.lightHelper.visible = false;

  core.scene.add(core.dirLightTarget);
  core.dirLight.target = core.dirLightTarget;
  core.dirLight.target.updateMatrixWorld();

  var gridSizeScale = core.gridSize * 2.5;
  if (core.basicGrid !== undefined) core.scene.remove(core.basicGrid);
  core.basicGrid = new THREE.Group();
  var planeMaterial = new THREE.ShadowMaterial({ opacity: 0.35 })

  var planeMesh = new THREE.Mesh(new THREE.PlaneGeometry(gridSizeScale, gridSizeScale), planeMaterial);

  planeMesh.rotation.x = -Math.PI / 2;
  planeMesh.position.set(0, 0, 0);
  planeMesh.receiveShadow = true;
  core.basicGrid.add(planeMesh);

  core.axesHelper = new THREE.AxesHelper(core.gridSize);
  core.axesHelper.position.set(0, 0, 0);
  core.axesHelper.visible = false;
  core.basicGrid.add(core.axesHelper);

  const grid = new THREE.GridHelper(gridSizeScale, 25, 0xaeaeae, 0x000000);
  grid.material.opacity = 0.05;
  grid.material.transparent = true;
  grid.position.set(0, 0, 0);
  core.basicGrid.add(grid);

  core.scene.add(core.basicGrid);
 
  // === fit camera distance ===
  const halfHeight = size.y / 2;
  const halfWidth  = size.x / 2;

  const fitHeightDistance =
    halfHeight / Math.tan(THREE.MathUtils.degToRad(core.camera.fov / 2));

  const fitWidthDistance =
    halfWidth /
    Math.tan(THREE.MathUtils.degToRad(core.camera.fov / 2)) /
    core.camera.aspect;

  const distance = Math.max(fitHeightDistance, fitWidthDistance) * 1.95;

  // === target position ===
  const dir = new THREE.Vector3(0.5, -0.25, -1).normalize(); // 45-degree angle perspective
  dir.multiplyScalar(-distance);

  const finalCameraPos = center.clone().add(dir);
  const finalTarget = center.clone();

  // === override from config if available ===
  if (cfg?.cameraPosition?.length === 3) {
    finalCameraPos.set(
      cfg.cameraPosition[0],
      cfg.cameraPosition[1],
      cfg.cameraPosition[2]
    );
  }

  if (cfg?.controlsTarget?.length === 3) {
    finalTarget.set(
      cfg.controlsTarget[0],
      cfg.controlsTarget[1],
      cfg.controlsTarget[2]
    );
  }

  // Store reset position for "Reset camera" action
  core.cameraCoords = finalCameraPos.clone();
  core.controlsTarget = finalTarget.clone();

  // === animate ===
  animateCameraToPose({
    finalCameraPos,
    finalTarget,
    boundingBox,
    duration: 3500,
    startOffsetFactor: 0.15,
    distanceOffsetFactor: -0.5, // 0.1 = 10% closer
    distanceOffsetUnits: 0, // +0.5 world units
  });

  if (_fit) {
    var rotateMetadata = new THREE.Vector3();
    rotateMetadata = new THREE.Vector3(
      THREE.MathUtils.radToDeg(core.helperObjects[0]?.rotation.x || 1),
      THREE.MathUtils.radToDeg(core.helperObjects[0]?.rotation.y || 5),
      THREE.MathUtils.radToDeg(core.helperObjects[0]?.rotation.z || 1)
    );
    const rootObject = Array.isArray(object) ? object[0] : object;
    core.objectsConfig.originalMetadata = {
      objPosition: [rootObject?.position?.x || 0, rootObject?.position?.y || 0, rootObject?.position?.z || 0],
      objRotation: [rotateMetadata.x, rotateMetadata.y, rotateMetadata.z],
      objScale: [
        core.helperObjects[0]?.scale.x || 1,
        core.helperObjects[0]?.scale.y || 5,
        core.helperObjects[0]?.scale.z || 1,
      ],
      cameraPosition: [core.camera.position.x, core.camera.position.y, core.camera.position.z],
      controlsTarget: [core.controls.target.x, core.controls.target.y, core.controls.target.z],
    };
  }
  if (!core.PRESENTATION_MODE) {
    setupClippingPlanes(object, {x: boundingBox.max.x*1.1, y: boundingBox.max.y*1.1, z: boundingBox.max.z*1.1});
  }
}

function parseGradient(str) {
  if (!str || typeof str !== "string") return null;

  // Match "radial-gradient" or "linear-gradient"
  const typeMatch = str.match(/(radial|linear)-gradient\s*\(([^,]+)/i);
  const gradientType = typeMatch ? typeMatch[1].toLowerCase() : null;
  const shapeOrDirection = typeMatch ? typeMatch[2].trim() : null;

  const colors = [];

  /* ==========================
     RGB / RGBA
  ========================== */
  const rgbMatches = str.matchAll(
    /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/gi
  );

  for (const [, r, g, b, a] of rgbMatches) {
    colors.push({
      r: +r,
      g: +g,
      b: +b,
      a: a !== undefined ? +a : 1,
    });
  }

  /* ==========================
     HEX (#RGB / #RRGGBB)
  ========================== */
  const hexMatches = str.matchAll(/#([0-9a-f]{3}|[0-9a-f]{6})/gi);

  for (const [, hex] of hexMatches) {
    const fullHex =
      hex.length === 3
        ? hex.split("").map(c => c + c).join("")
        : hex;

    colors.push({
      r: parseInt(fullHex.slice(0, 2), 16),
      g: parseInt(fullHex.slice(2, 4), 16),
      b: parseInt(fullHex.slice(4, 6), 16),
      a: 1,
    });
  }

  return {
    type: gradientType,        // "radial" | "linear"
    shapeOrDirection,          // "circle", "to right", etc.
    colors,                    // [{ r, g, b, a }]
  };
}

function rgbaToCss(c) {
  return `rgba(${c.r}, ${c.g}, ${c.b}, ${c.a})`;
}

function changeBackgroundHelper(_color1, _color2, _alpha = 100) {
  core.mainCanvas.style.setProperty(
    "background",
    `radial-gradient(circle, ${_color1} 0%, ${_color2} ${_alpha}%)`
  );
}

export function applyGradientCss(gradient) {
  if (!gradient || !Array.isArray(gradient.colors) || gradient.colors.length === 0) {
    return;
  }

  const colors = gradient.colors;

  // 1 color → solid background
  if (colors.length === 1) {
    const c = rgbaToCss(colors[0]);
    changeBackground("linear", c);
    return;
  }

  // 2 colors → legacy helper
  if (colors.length === 2) {
    const c1 = rgbaToCss(colors[0]);
    const c2 = rgbaToCss(colors[1]);
    changeBackground(gradient.type, c1, c2);
    return;
  }

  // >= 3 stops → full CSS gradient
  const stops = colors.map((c, i) => {
    const t = Math.round((i / (colors.length - 1)) * 100);
    return `${rgbaToCss(c)} ${t}%`;
  });

  const css =
    gradient.type === "radial"
      ? `radial-gradient(circle, ${stops.join(", ")})`
      : `linear-gradient(to bottom, ${stops.join(", ")})`;

  core.mainCanvas.style.setProperty("background", css);
}

export function changeBackground(_type, _color1, _color2 = _color1, _alpha = 100) {
  switch (_type) {
    case "linear":
      changeBackgroundHelper(_color1, _color1, _alpha);
      break;
    case "gradient":
    case "radial":
      changeBackgroundHelper(_color1, _color2, _alpha);
      break;
  }
}

function setupClippingPlanes(_geom, _distance) {
  /*var _geometry;
  if (_geom.isGroup)
    _geometry = _geom.children;
  else
    _geometry = _geom.geometry.clone();*/
  core.clippingPlanes[0].constant = _distance.x;
  core.clippingPlanes[1].constant = _distance.y;
  core.clippingPlanes[2].constant = _distance.z;
  let clippingCenterY = 0;
  if (_geom) {
    const bounds = new THREE.Box3();
    if (Array.isArray(_geom)) {
      for (let i = 0; i < _geom.length; i++) {
        bounds.union(new THREE.Box3().setFromObject(_geom[i], true));
      }
    } else {
      bounds.setFromObject(_geom, true);
    }
    if (!bounds.isEmpty()) {
      clippingCenterY = bounds.getCenter(new THREE.Vector3()).y;
    }
  }
  const updatePlaneHelperPosition = (index, constantValue) => {
    const helper = core.planeHelpers?.[index];
    const plane = core.clippingPlanes?.[index];
    if (!helper || !plane) return;
    helper.position.copy(plane.normal).multiplyScalar(-constantValue);
  };
  if (core.EDITOR) {

  if (core.transformControlClippingPlaneX && core.transformControlClippingPlaneY && core.transformControlClippingPlaneZ) {
    core.scene.add(core.transformControlClippingPlaneX?.getHelper());
    core.scene.add(core.transformControlClippingPlaneY?.getHelper());
    core.scene.add(core.transformControlClippingPlaneZ?.getHelper());
  }

  let planeColor = new THREE.Color(0xffffff).getHexString();
  if (core.scene.background != null) planeColor = core.scene.background.getHexString();

  core.planeHelpers = core.clippingPlanes.map(
    (p) => new THREE.PlaneHelper(p, core.gridSize * 2, invertHexColor(planeColor))
  );
  core.planeHelpers.forEach((ph, index) => {
    ph.visible = false;
    ph.name = "PlaneHelper";
    updatePlaneHelperPosition(index, core.clippingPlanes[index].constant);
    if (index === 0 || index === 2) {
      ph.userData.clippingCenterY = clippingCenterY;
      const baseUpdateMatrixWorld = ph.updateMatrixWorld.bind(ph);
      ph.updateMatrixWorld = function updatePlaneHelperMatrix(force) {
        baseUpdateMatrixWorld(force);
        this.position.y = this.userData.clippingCenterY || 0;
        this.updateMatrix();
        if (this.parent) {
          this.matrixWorld.multiplyMatrices(this.parent.matrixWorld, this.matrix);
        } else {
          this.matrixWorld.copy(this.matrix);
        }
        for (let i = 0; i < this.children.length; i++) {
          this.children[i].updateMatrixWorld(true);
        }
      };
      core.scene.add(ph);
    } else {
      core.scene.add(ph);
    }
  });

  core.distanceGeometry = _distance;
  scaleXYZ(core.distanceGeometry, 2);
  const showClippingPlaneToast = (axisLabel, enabled) => {
    toastHelper("clippingHelperToggle", "info", {
      axis: axisLabel,
      state: enabled
    });
  };
  const refreshClippingHint = () => {
    const update = core.updateClippingHintVisibility;
    if (typeof update === "function") {
      update();
      return;
    }
    if (!core.clippingHint || !core.planeParams?.clippingMode) return;
    const mode = core.planeParams.clippingMode;
    core.clippingHint.hidden = !(mode.x || mode.y || mode.z);
  };
  const tr = (key, fallback) => window?.Viewer?.t?.(key, fallback) ?? fallback;
  let displayHelper = {x: getOrAddGuiController(core.planeParams.planeX, "displayHelperX"), constantX: getOrAddGuiController(core.planeParams.planeX, "constantX"), y: getOrAddGuiController(core.planeParams.planeY, "displayHelperY"), constantY: getOrAddGuiController(core.planeParams.planeY, "constantY"), z: getOrAddGuiController(core.planeParams.planeZ, "displayHelperZ"), constantZ: getOrAddGuiController(core.planeParams.planeZ, "constantZ"), outline: getOrAddGuiController(core.planeParams.outline, "visible")};
  displayHelper.x?.name?.(tr("gui.displayHelperX", "Show X helper"));
  displayHelper.constantX?.name?.(tr("gui.constantX", "Constant X"));
  displayHelper.y?.name?.(tr("gui.displayHelperY", "Show Y helper"));
  displayHelper.constantY?.name?.(tr("gui.constantY", "Constant Y"));
  displayHelper.z?.name?.(tr("gui.displayHelperZ", "Show Z helper"));
  displayHelper.constantZ?.name?.(tr("gui.constantZ", "Constant Z"));
  displayHelper.outline?.name?.(tr("gui.visible", "Visible"));
  displayHelper.x?.onChange((v) => {
      core.planeParams.clippingMode.x = core.planeHelpers[0].visible = v;
      if (v) {
        core.transformControlClippingPlaneX.attach(core.planeHelpers[0]);
        if (core.planeParams.outline.visible) core.outlineClipping.visible = true;
      } else {
        core.transformControlClippingPlaneX.detach();
        if (
          !core.planeParams.clippingMode.y &&
          !core.planeParams.clippingMode.z &&
          !core.planeParams.outline.visible
        )
          core.outlineClipping.visible = false;
      }
      showClippingPlaneToast("X", v);
      refreshClippingHint();
    });

    displayHelper?.constantX.min(-core.distanceGeometry.x)
      .max(core.distanceGeometry.x)
      .setValue(core.distanceGeometry.x)
      .step(core.gridSize / 100)
      .listen()
      .onChange((d) => {
        core.clippingPlanes[0].constant = d;
        updatePlaneHelperPosition(0, d);
      });

    displayHelper.y?.onChange((v) => {
      core.planeParams.clippingMode.y = core.planeHelpers[1].visible = v;
      if (v) {
        core.transformControlClippingPlaneY.attach(core.planeHelpers[1]);
        if (core.planeParams.outline.visible) core.outlineClipping.visible = true;
      } else {
        core.transformControlClippingPlaneY.detach();
        if (
          !core.planeParams.clippingMode.x &&
          !core.planeParams.clippingMode.z &&
          !core.planeParams.outline.visible
        )
          core.outlineClipping.visible = false;
      }
      showClippingPlaneToast("Y", v);
      refreshClippingHint();
    });
    displayHelper?.constantY
      .min(-core.distanceGeometry.y)
      .max(core.distanceGeometry.y)
      .setValue(core.distanceGeometry.y)
      .step(core.gridSize / 100)
      .listen()
      .onChange((d) => {
        core.clippingPlanes[1].constant = d;
        updatePlaneHelperPosition(1, d);
      });
  
    displayHelper.z?.onChange((v) => {
      core.planeParams.clippingMode.z = core.planeHelpers[2].visible = v;
      if (v) {
        core.transformControlClippingPlaneZ.attach(core.planeHelpers[2]);
        if (core.planeParams.outline.visible) core.outlineClipping.visible = true;
      } else {
        core.transformControlClippingPlaneZ.detach();
        if (
          !core.planeParams.clippingMode.x &&
          !core.planeParams.clippingMode.y &&
          !core.planeParams.outline.visible
        )
          core.outlineClipping.visible = false;
      }
      showClippingPlaneToast("Z", v);
      refreshClippingHint();
    });
    displayHelper?.constantZ
      .min(-core.distanceGeometry.z)
      .max(core.distanceGeometry.z)
      .setValue(core.distanceGeometry.z)
      .step(core.gridSize / 100)
      .listen()
      .onChange((d) => {
        core.clippingPlanes[2].constant = d;
        updatePlaneHelperPosition(2, d);
      });

    displayHelper.outline.onChange((v) => {
      core.outlineClipping.visible = v;
    });
    refreshClippingHint();
  }
}


// Color helpers
export function invertHexColor(hexTripletColor) {
  let color = hexTripletColor.substring(1);
  color = parseInt(color, 16);
  color = 0xffffff ^ color;
  color = color.toString(16);
  color = ("000000" + color).slice(-6);
  return "#" + color;
}

export function getOrAddGuiController(object, prop) {
  const findController = (folder) => {
    if (!folder) return null;
    const controller = folder.controllers?.find(c => c._name === prop || c.property === prop);
    if (controller) return controller;

    for (const subfolder of folder.folders || []) {
      const found = findController(subfolder);
      if (found) return found;
    }
    return null;
  };

  let controller = findController(core.clippingFolder);
  if (controller) return controller;

  return core.clippingFolder?.add(object, prop);
}
