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';

const gltfLoader = new GLTFLoader();


export class URDFEditor {
  constructor(canvas) {
    this.canvas = canvas;

    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.overlayCanvas = document.getElementById('overlay-canvas');
    this.overlayCtx = this.overlayCanvas.getContext('2d');

    // Match size to renderer
    const resizeOverlay = () => {
      const { clientWidth, clientHeight } = this.renderer.domElement;
      this.overlayCanvas.width = clientWidth;
      this.overlayCanvas.height = clientHeight;
    };
    window.addEventListener('resize', resizeOverlay);
    resizeOverlay();


    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.hoveredJoint = null;
    this.draggedJoint = 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.setupEvents();



    // 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));
  }

  async loadURDF() {
    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;

    // for (const jointName in this.robot.joints) {
    //   console.log(jointName, this.robot.joints[jointName].transmission);
    // }
  }



  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) {
    this.mouse.x = (event.clientX / this.canvas.clientWidth) * 2 - 1;
    this.mouse.y = -(event.clientY / this.canvas.clientHeight) * 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;
      //console.log(jointName);
      // ✅ Apply mimic relationships automatically
      // ✅ Enforce mimics
      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
    if (intersects.length > 0) {
      const target = intersects[0].object;
      if (this.hoveredJoint?.material?.emissive) {
        this.hoveredJoint.material.emissive.setHex(0x000000);
      }
      this.hoveredJoint = target;
      if (this.hoveredJoint.material?.emissive) {
        this.hoveredJoint.material.emissive.setHex(0x333333);
      }
    } else {
      if (this.hoveredJoint?.material?.emissive) {
        this.hoveredJoint.material.emissive.setHex(0x000000);
      }
      this.hoveredJoint = null;
    }
  }


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

    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;
  }


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



  drawJointOverlay() {
    const ctx = this.overlayCtx;
    if (!ctx || !this.robot?.joints) return;

    ctx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height);
    ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
    ctx.fillRect(0, 0, 300, this.overlayCanvas.height);

    ctx.font = '14px monospace';
    ctx.fillStyle = '#0f0';
    ctx.textBaseline = 'top';

    let y = 10;
    const nameX = 10;
    const angleX = 220; // adjust for spacing

    for (const jointName in this.robot.joints) {
      const jointData = this.robot.joints[jointName];
      const jointObject = this.findObjectByName(this.robot, jointName);
      if (!jointData || !jointObject) continue;

      const angle = this.jointAngles?.[jointName] ?? 0;
      const degrees = (angle * 180 / Math.PI).toFixed(1);

      const rotatedChild = jointObject.children?.[0];
      const childName = rotatedChild?.name || '(unnamed)';
      const motorName = jointData.transmission?.actuatorName || '(no motor)';
      if (motorName == "(no motor)") {
        continue;
      }
      ctx.fillText(motorName, nameX, y);
      const angleStr = `${degrees}°`;
      const angleWidth = ctx.measureText(angleStr).width;
      ctx.fillText(angleStr, angleX - angleWidth, y);

      const t = jointData.transmission;
      if (t) {
        //const actuatorAngleDeg = (degrees / t.mechanicalReduction) + 4096 / 2;
        const ticks = radiansToTicks(angle, t).toFixed(0);
        const isValid = ticks >= t.encoderValidMin && ticks <= t.encoderValidMax;

        //ctx.fillStyle = isValid ? '#0f0' : '#f00';
        ctx.fillText(`${ticks}`, nameX + 250, y);
      }

      y += 18;
    }
  }
}

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);
  }

}
