import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
//import URDFLoader from './urdf/ExtendedURDFLoader.js';
import { createRotationSector, createAngleIndicator, createJointLabel } from './JointVisualiser.js';
import ExtendedURDFLoader from './urdf/ExtendedURDFLoader.js';
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { ViewerOverlay } from './ViewerOverlay.js';
import { clamp } from 'three/src/math/MathUtils.js';
const gltfLoader = new GLTFLoader();

const SyncMode = {
  None: 0,
  SimToReal: 1,
  RealToSim: 2,
  Calibrate: 3
};

export class URDFEditor {
  constructor(canvas, sendMotorPosition, serial, curveEditor, tryConnect) {
    this.canvas = canvas;
    this.sendMotorPosition = sendMotorPosition;
    this.serial = serial;
    this.curveEditor = curveEditor;
    this.tryConnect = tryConnect;
    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0xaaaaaa);

    this.camera = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 100);
    this.camera.position.set(-0.3, 0.5, -0.7);
    this.camera.lookAt(0, 0, 0);

    this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
    this.renderer.setSize(canvas.clientWidth, canvas.clientHeight);



    this.controls = new OrbitControls(this.camera, this.renderer.domElement);
    this.controls.target.set(0, 0.1, 0);
    this.controls.update();

    this.raycaster = new THREE.Raycaster();
    this.mouse = new THREE.Vector2();

    this.currentSyncMode = SyncMode.None;

    this.hoveredJoint = null;
    this.draggedJoint = null;
    this.selectedJoint = null;
    this.worldAxis = null;
    this.lastX = null;
    this.isDragging = false;
    this.jointAngles = {};

    this.loader = new ExtendedURDFLoader();
    this.loader.loadMeshCb = (path, manager, onComplete) => {
      if (path.endsWith('.glb') || path.endsWith('.gltf')) {
        const gltfLoader = new GLTFLoader(manager);
        gltfLoader.load(path, (gltf) => {
          onComplete(gltf.scene);
        });
      } else {
        // fall back to default behavior (STL, DAE)
        URDFLoader.prototype.loadMeshCb.call(this.loader, path, manager, onComplete);
      }
    };

    this.loader.packages = {
      'Little_Sophia_Face': '/robots/LittleSophia'
    };
    this.loader.parseVisual = true;
    this.loader.parseCollision = false;


    this.setupScene();
    //this.loadURDF();
    //this.loadURDFFromIndexedDB();
    this.setupEvents();

    const editorCallbacks = {}


    this.overlay = new ViewerOverlay(
      this.renderer,
      this.robot,
      this.jointAngles,
      this.findObjectByName.bind(this),
      this
    );


    // Add some lights (optional if using MeshBasicMaterial)
    const ambient = new THREE.AmbientLight(0x404040);
    this.scene.add(ambient);
    const dirLight = new THREE.DirectionalLight(0xffffff, 1);
    dirLight.position.set(5, 5, 5);
    this.scene.add(dirLight);

    // this.mesh = null;
    // gltfLoader.load('meshes/Little_Sophia.glb', (gltf) => {
    //   const model = gltf.scene;
    //   model.scale.set(0.001, 0.001, 0.001); // adjust if needed
    //   model.rotation.y = Math.PI; // radians (180°)
    //   this.scene.add(model);

    //   // Optional: fit camera to model
    //   const box = new THREE.Box3().setFromObject(model);
    //   const center = box.getCenter(new THREE.Vector3());
    //   const size = box.getSize(new THREE.Vector3()).length();
    // });


    this.animate();
  }

  setupScene() {
    this.scene.add(new THREE.AmbientLight(0xffffff, 0.6));
    const light1 = new THREE.DirectionalLight(0xffffff, 0.8);
    light1.position.set(15, 10, 7);
    this.scene.add(light1);

    const light2 = new THREE.DirectionalLight(0xffffff, 0.8);
    light2.position.set(-15, 10, -7);
    this.scene.add(light2);

    this.scene.add(new THREE.AxesHelper(1));
    this.scene.add(new THREE.GridHelper(2, 10));


  }

  resetEditor() {
    if (!this.robot) return;
    const toRemove = this.scene.children.filter(c => c.type === 'URDFLink' || c.constructor.name === 'URDFRobot');
    toRemove.forEach(robot => {
      // detach from scene
      this.scene.remove(robot);

      // dispose GPU resources
      robot.traverse(obj => {
        if (obj.geometry) obj.geometry.dispose();
        if (obj.material) {
          if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
          else obj.material.dispose();
        }
      });
    });

    // Clear references
    this.robot.urdfRobotNode = null;
    this.robot.joints = {};
    this.robot.links = {};
    this.robot.sceneObject = null;
    this.robot = null;

    this.overlay.resetEditor();
  }

  setAnimationLength(totalFrames) {
    //this.overlay.
  }

  async loadURDF() {
    if (this.robot) {
      console.log("resetting editor");
      this.resetEditor();
    }

    this.urdfPath = '/robots/LittleSophia/urdf/LittleSophia.urdf';

    const urdfText = await fetch(this.urdfPath).then(res => res.text());
    const robot = await this.loader.loadFromString(urdfText);
    //addOriginMarkers(robot);
    robot.rotation.x = -Math.PI / 2;
    this.scene.add(robot);
    this.robot = robot;

    //this.overlay.robot = robot;
    this.overlay.init(robot);

  }

  async loadURDFFromIndexedDB() {
    if (this.robot) {
      console.log("resetting editor");
      this.resetEditor();
    }

    return new Promise((resolve, reject) => {
      const request = indexedDB.open("URDFEditorDB", 1);

      request.onerror = e => reject(e);

      request.onsuccess = e => {
        const db = e.target.result;
        const tx = db.transaction("urdfs", "readonly");
        const getReq = tx.objectStore("urdfs").get("current");

        getReq.onsuccess = async () => {
          const urdfText = getReq.result;
          if (!urdfText) {
            reject("No URDF stored in IndexedDB");
            return;
          }

          try {
            // ✅ feed the text into your loader
            const robot = await this.loader.loadFromString(urdfText);
            robot.rotation.x = -Math.PI / 2;
            this.scene.add(robot);
            this.robot = robot;
            this.overlay.init(robot);
            this.saveURDFToIndexedDB(robot);
            resolve(robot);
          } catch (err) {
            reject(err);
          }
        };
      };
    });
  }

  // WRITE TO URDF POSITION IN ROBOT
  // Find the transmission node for neck_roll
  // const neckTrans = this.robot.urdfRobotNode.querySelector('transmission[name="neck_roll_trans"]');
  // if (neckTrans) {
  //   const actuator = neckTrans.querySelector('actuator[name="neck_roll"]');
  //   if (actuator) {
  //     // Example: change encoderValidMin/Max
  //     actuator.querySelector('encoderValidMin').textContent = "100";
  //     actuator.querySelector('encoderValidMax').textContent = "3900";
  //   }
  // }

  downloadURDF() {

    const serializer = new XMLSerializer();
    const urdfString = serializer.serializeToString(this.robot.urdfRobotNode);

    const a = document.createElement("a");
    a.href = "data:text/xml;charset=utf-8," + encodeURIComponent(urdfString);
    a.download = "robot.urdf";
    a.click();
  }

  async uploadURDF() {
    // 1. Let the user pick a file
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.urdf,.xml'; // restrict to URDF/XML files

    if (this.robot) {
      console.log("resetting editor");
      this.resetEditor();
    }

    input.onchange = async (e) => {
      const file = e.target.files[0];
      if (!file) return;

      // 2. Reset editor if a robot is already loaded
      if (this.robot) {
        console.log("resetting editor");
        this.resetEditor();
      }

      // 3. Read the file contents
      const urdfText = await file.text();

      // 4. Parse and load robot
      const robot = await this.loader.loadFromString(urdfText);
      robot.rotation.x = -Math.PI / 2;
      this.scene.add(robot);
      this.robot = robot;

      // 5. Initialize overlay
      this.overlay.init(robot);
    };

    // trigger the file picker
    input.click();
  }


  saveURDFToIndexedDB(robot) {
    const serializer = new XMLSerializer();
    const urdfString = serializer.serializeToString(robot.urdfRobotNode);

    const request = indexedDB.open("URDFEditorDB", 1);
    request.onupgradeneeded = e => {
      const db = e.target.result;
      db.createObjectStore("urdfs");
    };
    request.onsuccess = e => {
      const db = e.target.result;
      const tx = db.transaction("urdfs", "readwrite");
      tx.objectStore("urdfs").put(urdfString, "current");
      console.log("saved URDF to IndexedDB");
    };

  }

  listUrdfsFromIndexedDB() {
    const request = indexedDB.open("URDFEditorDB", 1);

    request.onsuccess = e => {
      const db = e.target.result;
      const tx = db.transaction("urdfs", "readonly");
      const store = tx.objectStore("urdfs");
      const getAllReq = store.getAll();

      getAllReq.onsuccess = () => {
        console.log("Stored URDFs:", getAllReq.result);
      };
    };
  }


  findObjectByName(root, name) {
    let result = null;
    root.traverse(child => {
      if (child.name === name) {
        result = child;
      }
    });
    return result;
  }


  setupEvents() {
    this.canvas.addEventListener('pointermove', this.onPointerMove.bind(this));
    this.canvas.addEventListener('pointerdown', this.onPointerDown.bind(this));
    this.canvas.addEventListener('pointerup', this.onPointerUp.bind(this));
  }

  onPointerMove(event) {
    const rect = this.canvas.getBoundingClientRect();

    this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

    this.raycaster.setFromCamera(this.mouse, this.camera);

    if (!this.robot) return;
    const intersects = this.raycaster.intersectObjects(this.robot.children, true);

    if (this.isDragging && this.draggedJoint && this.worldAxis) {
      const deltaX = event.clientX - this.lastX;
      this.lastX = event.clientX;
      const angleDelta = deltaX * 0.005;

      let jointName = this.draggedJoint.name;
      const { lower, upper } = getJointLimits(jointName, this.robot);

      const currentAngle = this.jointAngles[jointName] ?? 0;
      const proposedAngle = currentAngle + angleDelta;
      const clampedAngle = Math.max(lower, Math.min(upper, proposedAngle));
      const actualDelta = clampedAngle - currentAngle;

      // ✅ Rotate dragged joint
      this.draggedJoint.rotateOnAxis(this.worldAxis, actualDelta);
      this.jointAngles[jointName] = clampedAngle;
      const jointTransmission = getJointTransmission(jointName, this.robot);
      this.onRotateMotor(jointTransmission.motorID, radiansToTicks(clampedAngle, jointTransmission).toFixed(0));

      // ✅ Apply mimic relationships automatically
      for (const [name, jointObj] of Object.entries(this.robot.joints)) {
        if (jointObj.type === 'URDFMimicJoint') {
          const { mimicJoint, multiplier, offset } = jointObj;
          if (mimicJoint === jointName) {
            const mimicAngle = clampedAngle * multiplier + offset;
            const { lower, upper } = getJointLimits(name, this.robot);
            const mimicClamped = Math.max(lower, Math.min(upper, mimicAngle));
            const mimicDelta = mimicClamped - (this.jointAngles[name] ?? 0);

            const mimicNode = this.robot.getObjectByName(name);
            mimicNode.rotateOnAxis(this.worldAxis, mimicDelta);
            this.jointAngles[name] = mimicClamped;
          }
        }
      }

      // ✅ Update gizmo indicator
      const gizmo = this.draggedJoint.userData.gizmo;
      if (gizmo?.indicator) this.draggedJoint.parent.remove(gizmo.indicator);
      const newIndicator = createAngleIndicator(this.worldAxis, clampedAngle);
      newIndicator.position.copy(this.draggedJoint.position);
      this.draggedJoint.parent.add(newIndicator);
      this.draggedJoint.userData.gizmo.indicator = newIndicator;
    }



    // Hover highlighting
    // Hover highlighting
    if (intersects.length > 0) {
      const target = intersects[0].object;

      // reset previous hover if it's not selected
      if (this.hoveredJoint && this.hoveredJoint !== this.selectedJoint) {
        if (this.hoveredJoint?.material?.emissive) {
          this.hoveredJoint.material.emissive.setHex(0x000000);
        }
      }

      this.hoveredJoint = target;

      if (this.hoveredJoint.material?.emissive) {
        // use a different color if it's also selected
        const color = (this.hoveredJoint === this.selectedJoint) ? 0x555555 : 0x333333;
        this.hoveredJoint.material.emissive.setHex(color);
      }
    } else {
      if (this.hoveredJoint && this.hoveredJoint !== this.selectedJoint) {
        if (this.hoveredJoint?.material?.emissive) {
          this.hoveredJoint.material.emissive.setHex(0x000000);
        }
      }
      this.hoveredJoint = null;
    }

  }

  setSyncMode(newMode) {
    console.log("changing from " + this.currentSyncMode + " to " + newMode);
    this.currentSyncMode = newMode;
    if (this.currentSyncMode == SyncMode.RealToSim) {
      this.serial.requestPositionStreaming(true);
    } else {
      this.serial.requestPositionStreaming(false);

    }
  }

  findMotorByID(motorID) {
    for (const jointName in this.robot.joints) {
      const joint = this.robot.joints[jointName];
      const transmission = joint.transmission;
      if (!transmission) continue;

      if (transmission.motorID === motorID) {
        return transmission;
      }
    }
  }

  findAllMotorIDs() {
    let list = [];
    for (const jointName in this.robot.joints) {
      const joint = this.robot.joints[jointName];
      const transmission = joint.transmission;
      if (!transmission) continue;
      list.push(transmission.motorID);
    }
    return list;
  }

  getMotorTicks(motorID) {
    if (!this.robot?.joints) return null;
    for (const [jointName, jointData] of Object.entries(this.robot.joints)) {
      const t = jointData.transmission;
      if (!t) continue;
      //console.log(t);
      if (t.motorID === motorID) {

        const angle = this.jointAngles?.[jointName] ?? 0;
        return parseInt(radiansToTicks(angle, t));
      }
    }

    return null; // not found
  }

  syncWithCurveEditor() {
    let allIDs = this.findAllMotorIDs();

    let ids = [];
    let positions = [];
    allIDs.forEach(id => {
      let pos = this.curveEditor.getMotorPositionAtTime(id, this.curveEditor.currentTime);
      if (this.currentSyncMode !== SyncMode.RealToSim) {
        this.setMotorPosition(id, pos);
      }

      ids.push(id);
      positions.push(pos);
    });

    if (this.currentSyncMode === SyncMode.SimToReal) {
      this.sendMotorPosition(ids, positions);
    }
  }

  setMotorPosition(motorID, positionTicks) {
    for (const jointName in this.robot.joints) {
      const joint = this.robot.joints[jointName];
      const transmission = joint.transmission;
      if (!transmission) continue;

      if (transmission.motorID === motorID) {
        // 1) Convert ticks → target joint angle (radians)
        const targetAngle = ticksToRadians(positionTicks, transmission);

        // 2) Compute delta vs tracked state
        const currentAngle = this.jointAngles[jointName] ?? 0;
        const delta = targetAngle - currentAngle;

        // 3) Rotate driven joint
        joint.rotateOnAxis(joint.axis, delta);
        this.jointAngles[jointName] = targetAngle;

        // 4) ✅ Apply mimic relationships
        for (const [name, jointObj] of Object.entries(this.robot.joints)) {
          if (jointObj.type === 'URDFMimicJoint') {
            const { mimicJoint, multiplier = 1.0, offset = 0.0 } = jointObj;
            if (mimicJoint === jointName) {
              const mimicAngle = targetAngle * multiplier + offset;
              const mimicCurrent = this.jointAngles[name] ?? 0;
              const mimicDelta = mimicAngle - mimicCurrent;

              const mimicNode = this.robot.getObjectByName(name);
              mimicNode.rotateOnAxis(mimicNode.axis, mimicDelta);
              this.jointAngles[name] = mimicAngle;
            }
          }
        }

        break; // found and updated the matching joint
      }
    }
  }

  onRotateMotor(motorID, positionTicks) {
    if (this.currentSyncMode === SyncMode.SimToReal) {
      this.sendMotorPosition(motorID, positionTicks);
    }
  }

  onPointerDown(event) {
    if (!this.hoveredJoint) return;

    if (this.hoveredJoint) {
      // reset previous selection
      if (this.selectedJoint) {
        this.selectedJoint.material.emissive.setHex(0x000000);
      }
      this.selectedJoint = this.hoveredJoint;
      this.selectedJoint.material.emissive.setHex(0x555555); // selection color
      let motorID = this.findJointAncestor(this.selectedJoint, 0).transmission.motorID;
      this.curveEditor.selectMotor(motorID);
    }

    this.isDragging = true;
    this.lastX = event.clientX;
    this.controls.enabled = false;

    // Decide how far up the chain to go
    let levelsUp = 0; // default: climb one level from link → joint
    if (event.ctrlKey) levelsUp = 1;
    if (event.altKey) levelsUp = 2;

    this.draggedJoint = this.findJointAncestor(this.hoveredJoint, levelsUp);
    if (!this.draggedJoint) return;

    // ✅ Skip mimic joints
    if (this.draggedJoint.type === 'URDFMimicJoint') {
      this.draggedJoint = findJointAncestor(this.draggedJoint, 1);
    }


    const jointName = this.draggedJoint.name;
    const jointData = this.robot.joints?.[jointName];
    if (!(jointData?.axis instanceof THREE.Vector3)) return;

    const axis = jointData.axis.clone();
    if (axis.lengthSq() === 0) return;
    this.worldAxis = axis.normalize();

    const { lower, upper } = getJointLimits(jointName, this.robot);
    const transmission = jointData.transmission;
    const encoderRange = transmission
      ? transmission.encoderRange / transmission.mechanicalReduction
      : Math.PI;

    const sector = createRotationSector(this.worldAxis, lower, upper, encoderRange);
    const indicator = createAngleIndicator(this.worldAxis, this.jointAngles[jointName] ?? 0);

    sector.position.copy(this.draggedJoint.position);
    indicator.position.copy(this.draggedJoint.position);

    this.draggedJoint.parent.add(sector);
    this.draggedJoint.parent.add(indicator);
    this.draggedJoint.userData.gizmo = { sector, indicator };
  }



  onPointerUp() {
    this.isDragging = false;
    this.controls.enabled = true;

    const gizmo = this.draggedJoint?.userData.gizmo;
    if (gizmo) {
      this.draggedJoint.parent.remove(gizmo.sector);
      this.draggedJoint.parent.remove(gizmo.indicator);
    }

    this.draggedJoint = null;
    this.worldAxis = null;
  }

  findJointAncestor(joint, levelsUp = 0) {
    let current = joint;
    let steps = 0;

    while (current && steps <= levelsUp) {
      current = current.parent;
      // climb until we find a URDFJoint (not a mimic)
      while (current && !(current.type === 'URDFJoint')) {
        current = current.parent;
      }
      steps++;
    }

    return current;
  }

  applyCalibrationOffsets() {
    console.log(this.jointAngles);
    if (!this.robot?.urdfRobotNode || !this.jointAngles) return;

    for (const jointName in this.jointAngles) {
      const offset = this.jointAngles[jointName]; // radians
      const jointNode = this.robot.urdfRobotNode.querySelector(`joint[name="${jointName}"]`);
      if (!jointNode) continue;

      // get or create <origin>
      let originNode = jointNode.querySelector("origin");
      let [rx, ry, rz] = originNode?.getAttribute("rpy")?.split(" ").map(parseFloat) || [0, 0, 0];

      // determine axis of rotation
      const axisNode = jointNode.querySelector("axis");
      const [ax, ay, az] = axisNode ? axisNode.getAttribute("xyz").split(" ").map(parseFloat) : [0, 0, 1];

      // apply offset along the correct axis
      if (Math.abs(ax) === 1) rx += offset * ax;
      else if (Math.abs(ay) === 1) ry += offset * ay;
      else if (Math.abs(az) === 1) rz += offset * az;

      // update the URDF DOM
      console.log(jointName, [rx, ry, rz]);
      this.updateJointOriginRpy(jointName, [rx, ry, rz]);
    }
  }


  updateJointOriginRpy(jointName, rpyArray) {
    //console.log("Updating URDF joint rpy:", jointName, rpyArray);
    const urdfNode = this.robot?.urdfRobotNode;
    if (!urdfNode) return;

    // find joint anywhere in the URDF
    const jointNode = urdfNode.querySelector(`joint[name="${jointName}"]`);
    if (!jointNode) return;

    // find or create the <origin> child
    let originNode = jointNode.querySelector("origin");
    if (!originNode) {
      originNode = urdfNode.ownerDocument.createElement("origin");
      jointNode.appendChild(originNode);
    }

    // ensure we have 3 numbers [rx, ry, rz]
    const [rx, ry, rz] = rpyArray;
    originNode.setAttribute("rpy", `${rx} ${ry} ${rz}`);
  }



  animate() {
    requestAnimationFrame(() => this.animate());
    //console.log(this.robot.joints)
    this.renderer.render(this.scene, this.camera);
    this.overlay.draw();
    //console.log(this.mesh);
  }
}

function ticksToRadians(ticks, actuator) {
  const ticksPerDeg = actuator.encoderTicks / actuator.encoderRange; // ≈ 22.75
  const centered = ticks - actuator.encoderTicks / 2; // shift so mid = 0
  const actuatorDeg = centered / ticksPerDeg;
  const jointDeg = actuatorDeg / actuator.mechanicalReduction;
  return jointDeg * (Math.PI / 180); // convert degrees → radians
}

function radiansToTicks(radians, actuator) {
  const ticksPerDeg = actuator.encoderTicks / actuator.encoderRange; // ≈ 22.75
  const jointDeg = radians * (180 / Math.PI); // radians → degrees
  const actuatorDeg = jointDeg * actuator.mechanicalReduction; // apply reduction
  const centered = actuatorDeg * ticksPerDeg; // convert to ticks offset from mid
  const ticks = centered + actuator.encoderTicks / 2; // shift back to full range
  return ticks;
}


function getJointLimits(jointName, robot) {
  const transmission = robot.joints[jointName].transmission;

  if (!transmission) {
    return { lower: -Math.PI, upper: Math.PI };
  }
  //console.log(transmission.encoderValidMin, transmission.encoderValidMax);
  const lowerRad = ticksToRadians(transmission.encoderValidMin, transmission);
  const upperRad = ticksToRadians(transmission.encoderValidMax, transmission);
  //console.log(lowerRad, upperRad);
  return { lower: lowerRad, upper: upperRad };
}

function getJointTransmission(jointName, robot) {
  return robot.joints[jointName].transmission;
}

function loadObjWithMtl(objPath, mtlPath, onLoad) {
  const mtlLoader = new MTLLoader();
  mtlLoader.load(mtlPath, (materials) => {
    materials.preload();
    const objLoader = new OBJLoader();
    objLoader.setMaterials(materials);
    objLoader.load(objPath, (object) => {
      onLoad(object);
    });
  });
}

function addOriginMarkers(robot) {
  console.log(robot.joints);
  for (const [name, joint] of Object.entries(robot.joints)) {
    const marker = new THREE.Mesh(
      new THREE.SphereGeometry(0.01),
      new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    );
    joint.add(marker);

    const axes = new THREE.AxesHelper(0.05);
    joint.add(axes);

    console.log("Added marker for joint:", name);
  }

}
