/**
 * URDF Adapter
 * Converts urdf-loaders result to unified model
 */
import * as THREE from 'three';
import { UnifiedRobotModel, Link, Joint, JointLimits, VisualGeometry, CollisionGeometry, InertialProperties } from '@/models/UnifiedRobotModel.js';

export class URDFAdapter {
    /**
     * Convert urdf-loaders robot object to unified model
     * @param {THREE.Group} robot - Robot object returned by urdf-loaders
     * @param {string} urdfXML - Original URDF XML content (optional, for extracting inertial data)
     * @returns {UnifiedRobotModel}
     */
    static convert(robot, urdfXML = null) {
        const model = new UnifiedRobotModel();
        model.name = robot.name || 'robot';
        model.threeObject = robot;

        // Mark model type as URDF
        if (!robot.userData) robot.userData = {};
        robot.userData.type = 'urdf';

        if (!robot.links || !robot.joints) {
            console.warn('URDF model missing links or joints information');
            return model;
        }

        // If XML provided, parse inertial data
        let inertialData = {};
        if (urdfXML) {
            inertialData = this.parseInertialFromXML(urdfXML);
        }

        // Convert links
        Object.values(robot.links).forEach(urdfLink => {
            const link = this.convertLink(urdfLink);

            // If urdf-loader didn't parse inertial, get from XML
            if (!link.inertial && inertialData[urdfLink.name]) {
                link.inertial = inertialData[urdfLink.name];
            }

            model.addLink(link);
        });

        // Convert joints
        Object.values(robot.joints).forEach(urdfJoint => {
            const joint = this.convertJoint(urdfJoint);
            model.addJoint(joint);
        });

        // If XML provided, supplement effort and velocity from XML (urdf-loaders may not have parsed)
        if (urdfXML) {
            this.supplementJointLimitsFromXML(model, urdfXML);
        }

        // Find root link (link that is not a child of any joint)
        const allChildren = new Set(
            Array.from(model.joints.values()).map(j => j.child).filter(c => c)
        );
        const rootLinks = Array.from(model.links.keys()).filter(
            name => !allChildren.has(name)
        );
        if (rootLinks.length > 0) {
            model.rootLink = rootLinks[0];
        }

        // Enhance materials for better lighting (MuJoCo style)
        this.enhanceMaterials(robot);

        return model;
    }

    /**
     * Enhance materials for better lighting (MuJoCo style)
     * Applies consistent shininess and specular properties to all materials
     * Directly modifies materials in place to ensure changes persist
     */
    static enhanceMaterials(robotObject) {
        robotObject.traverse((child) => {
            if (child.isMesh && child.material) {
                // Handle material arrays
                if (Array.isArray(child.material)) {
                    child.material = child.material.map(mat => {
                        return this.enhanceSingleMaterial(mat);
                    });
                } else {
                    child.material = this.enhanceSingleMaterial(child.material);
                }
            }
        });
    }

    /**
     * Enhance a single material with better lighting properties
     * Returns enhanced material (may be cloned or modified in place)
     * Saves original properties for lighting toggle
     */
    static enhanceSingleMaterial(material) {
        if (material.isMeshPhongMaterial || material.isMeshStandardMaterial) {
            // Save original properties if not already saved (for lighting toggle)
            if (material.userData.originalShininess === undefined) {
                material.userData.originalShininess = material.shininess !== undefined ? material.shininess : 30;
                // IMPORTANT: Save original color to preserve URDF material colors
                if (!material.userData.originalColor && material.color) {
                    material.userData.originalColor = material.color.clone();
                }
                // Save original specular - if material had no specular, save null
                if (!material.specular) {
                    material.userData.originalSpecular = null;
                } else if (material.specular.isColor) {
                    const spec = material.specular;
                    if (spec.r < 0.1 && spec.g < 0.1 && spec.b < 0.1) {
                        material.userData.originalSpecular = null; // Likely default
                    } else {
                        material.userData.originalSpecular = spec.clone();
                    }
                } else if (typeof material.specular === 'number') {
                    if (material.specular === 0x111111 || material.specular < 0x111111) {
                        material.userData.originalSpecular = null;
                    } else {
                        material.userData.originalSpecular = new THREE.Color(material.specular);
                    }
                } else {
                    material.userData.originalSpecular = null;
                }
            }

            // IMPORTANT: Preserve original color from URDF material definitions
            // Do not modify material.color - urdf-loaders already sets the correct color

            // Apply enhanced lighting (default enabled)
            // Increase shininess for better highlights
            if (material.shininess === undefined || material.shininess < 50) {
                material.shininess = 50;
            }

            // Enhance specular reflection - ensure it's a Color object with proper values
            if (!material.specular) {
                material.specular = new THREE.Color(0.3, 0.3, 0.3);
            } else if (material.specular.isColor) {
                // If it's already a Color object, update values
                if (material.specular.r < 0.2 || material.specular.g < 0.2 || material.specular.b < 0.2) {
                    material.specular.setRGB(0.3, 0.3, 0.3);
                }
            } else if (typeof material.specular === 'number') {
                // Convert number to Color object
                if (material.specular < 0x333333) {
                    material.specular = new THREE.Color(0.3, 0.3, 0.3);
                } else {
                    // Convert hex to Color
                    material.specular = new THREE.Color(material.specular);
                    if (material.specular.r < 0.2) {
                        material.specular.setRGB(0.3, 0.3, 0.3);
                    }
                }
            }

            // Mark material as needing update
            material.needsUpdate = true;
            return material;
        } else if (material.type === 'MeshBasicMaterial') {
            // Convert MeshBasicMaterial to MeshPhongMaterial
            const oldMaterial = material;
            const envMap = typeof window !== 'undefined' && window.app?.sceneManager?.environmentManager?.getEnvironmentMap();
            const newMaterial = new THREE.MeshPhongMaterial({
                color: oldMaterial.color,
                map: oldMaterial.map,
                transparent: oldMaterial.transparent,
                opacity: oldMaterial.opacity,
                side: oldMaterial.side,
                shininess: 50,
                specular: new THREE.Color(0.3, 0.3, 0.3),
                envMap: envMap || null,
                reflectivity: envMap ? 0.3 : 0
            });
            // Save original properties for lighting toggle
            newMaterial.userData.originalShininess = 30;
            newMaterial.userData.originalSpecular = null; // MeshBasicMaterial had no specular
            return newMaterial;
        }

        return material;
    }

    static convertLink(urdfLink) {
        const link = new Link(urdfLink.name);
        link.threeObject = urdfLink;

        // Convert inertial properties
        if (urdfLink.inertial) {
            link.inertial = this.convertInertial(urdfLink.inertial);
        }

        // Note: urdf-loaders has already converted visual and collision to Three.js objects
        // We mainly extract metadata, actual meshes are in threeObject

        return link;
    }

    static convertJoint(urdfJoint) {
        // URDF joint object has jointType property (not type)
        const jointType = urdfJoint.jointType || urdfJoint.type || 'fixed';
        const joint = new Joint(urdfJoint.name, jointType);

        // Extract parent name (urdf-loader returns Three.js object reference)
        joint.parent = urdfJoint.parent?.name || null;

        // child may not be in urdfJoint.child, but in Three.js children array
        // In Three.js scene graph, Joint is parent node, Child Link is Joint's child node
        if (urdfJoint.child && urdfJoint.child.name) {
            joint.child = urdfJoint.child.name;
        } else if (urdfJoint.children && urdfJoint.children.length > 0) {
            // Find Link object from children array
            const childLink = urdfJoint.children.find(child =>
                child.isURDFLink || child.type === 'URDFLink'
            );
            if (childLink) {
                joint.child = childLink.name;
            }
        }

        joint.threeObject = urdfJoint;

        // Convert origin
        if (urdfJoint.origin) {
            joint.origin = {
                xyz: urdfJoint.origin.xyz || [0, 0, 0],
                rpy: urdfJoint.origin.rpy || [0, 0, 0]
            };
        }

        // Convert axis
        if (urdfJoint.axis) {
            joint.axis = { xyz: urdfJoint.axis.xyz || [0, 0, 1] };
        }

        // Convert limits
        if (urdfJoint.limit) {
            joint.limits = this.convertLimits(urdfJoint.limit);
        }

        // Get current value
        if (urdfJoint.angle !== undefined) {
            joint.currentValue = urdfJoint.angle;
        } else if (urdfJoint.jointValue !== undefined) {
            joint.currentValue = urdfJoint.jointValue;
        }

        return joint;
    }

    static convertLimits(limit) {
        const limits = new JointLimits();
        if (limit.lower !== undefined) limits.lower = limit.lower;
        if (limit.upper !== undefined) limits.upper = limit.upper;
        if (limit.effort !== undefined) limits.effort = limit.effort;
        if (limit.velocity !== undefined) limits.velocity = limit.velocity;
        return limits;
    }

    /**
     * Supplement joint effort and velocity information from URDF XML
     * (because urdf-loaders may not have parsed these attributes)
     */
    static supplementJointLimitsFromXML(model, urdfXML) {
        try {
            const parser = new DOMParser();
            const doc = parser.parseFromString(urdfXML, 'text/xml');

            const jointElements = doc.querySelectorAll('joint');
            jointElements.forEach(jointEl => {
                const jointName = jointEl.getAttribute('name');
                const joint = model.joints.get(jointName);

                if (joint) {
                    const limitEl = jointEl.querySelector('limit');
                    if (limitEl) {
                        if (!joint.limits) {
                            joint.limits = new JointLimits();
                        }

                        // Read effort and velocity
                        const effort = limitEl.getAttribute('effort');
                        const velocity = limitEl.getAttribute('velocity');

                        if (effort !== null) {
                            joint.limits.effort = parseFloat(effort);
                        }
                        if (velocity !== null) {
                            joint.limits.velocity = parseFloat(velocity);
                        }

                        // Also ensure lower and upper are correctly set
                        const lower = limitEl.getAttribute('lower');
                        const upper = limitEl.getAttribute('upper');
                        if (lower !== null) {
                            joint.limits.lower = parseFloat(lower);
                        }
                        if (upper !== null) {
                            joint.limits.upper = parseFloat(upper);
                        }
                    }
                }
            });
        } catch (error) {
            console.error('Failed to supplement joint limit information from XML:', error);
        }
    }

    static convertInertial(inertial) {
        const props = new InertialProperties();
        if (inertial.mass !== undefined) props.mass = inertial.mass;
        if (inertial.origin) {
            props.origin = {
                xyz: inertial.origin.xyz || [0, 0, 0],
                rpy: inertial.origin.rpy || [0, 0, 0]
            };
        }
        if (inertial.ixx !== undefined) props.ixx = inertial.ixx;
        if (inertial.iyy !== undefined) props.iyy = inertial.iyy;
        if (inertial.izz !== undefined) props.izz = inertial.izz;
        if (inertial.ixy !== undefined) props.ixy = inertial.ixy;
        if (inertial.ixz !== undefined) props.ixz = inertial.ixz;
        if (inertial.iyz !== undefined) props.iyz = inertial.iyz;
        return props;
    }

    /**
     * Parse inertial data from URDF XML
     * @param {string} xmlContent - URDF XML content
     * @returns {Object} Mapping from link names to inertial data
     */
    static parseInertialFromXML(xmlContent) {
        const inertialData = {};

        try {
            const parser = new DOMParser();
            const xmlDoc = parser.parseFromString(xmlContent, 'text/xml');

            // Get all link elements
            const links = xmlDoc.getElementsByTagName('link');

            for (let i = 0; i < links.length; i++) {
                const linkElement = links[i];
                const linkName = linkElement.getAttribute('name');

                // Find inertial element
                const inertialElement = linkElement.getElementsByTagName('inertial')[0];
                if (!inertialElement) continue;

                const inertial = new InertialProperties();

                // Parse mass
                const massElement = inertialElement.getElementsByTagName('mass')[0];
                if (massElement) {
                    inertial.mass = parseFloat(massElement.getAttribute('value'));
                }

                // Parse origin
                const originElement = inertialElement.getElementsByTagName('origin')[0];
                if (originElement) {
                    const xyz = originElement.getAttribute('xyz');
                    const rpy = originElement.getAttribute('rpy');
                    inertial.origin = {
                        xyz: xyz ? xyz.split(' ').map(parseFloat) : [0, 0, 0],
                        rpy: rpy ? rpy.split(' ').map(parseFloat) : [0, 0, 0]
                    };
                } else {
                    inertial.origin = { xyz: [0, 0, 0], rpy: [0, 0, 0] };
                }

                // Parse inertia
                const inertiaElement = inertialElement.getElementsByTagName('inertia')[0];
                if (inertiaElement) {
                    inertial.ixx = parseFloat(inertiaElement.getAttribute('ixx')) || 0;
                    inertial.iyy = parseFloat(inertiaElement.getAttribute('iyy')) || 0;
                    inertial.izz = parseFloat(inertiaElement.getAttribute('izz')) || 0;
                    inertial.ixy = parseFloat(inertiaElement.getAttribute('ixy')) || 0;
                    inertial.ixz = parseFloat(inertiaElement.getAttribute('ixz')) || 0;
                    inertial.iyz = parseFloat(inertiaElement.getAttribute('iyz')) || 0;
                }

                inertialData[linkName] = inertial;
            }
        } catch (error) {
            console.error('Failed to parse URDF XML inertial data:', error);
        }

        return inertialData;
    }

    /**
     * Set joint angle (using urdf-loaders' setJointValue method)
     * Reference URDFClasses.js setJointValue implementation
     */
    static setJointAngle(joint, angle, ignoreLimits = false) {
        joint.currentValue = angle;

        // URDF format: use urdf-loader's setJointValue method
        if (joint.threeObject) {
            // If ignoring limits, temporarily modify URDF joint object's limit values
            let originalLimits = null;
            if (ignoreLimits && joint.threeObject.limit) {
                originalLimits = {
                    lower: joint.threeObject.limit.lower,
                    upper: joint.threeObject.limit.upper
                };
                joint.threeObject.limit.lower = -Math.PI * 2;
                joint.threeObject.limit.upper = Math.PI * 2;
            }

            // Prefer setJointValue method (urdf-loader's standard method)
            if (typeof joint.threeObject.setJointValue === 'function') {
                joint.threeObject.setJointValue(angle);

                // Restore original limits
                if (originalLimits && joint.threeObject.limit) {
                    joint.threeObject.limit.lower = originalLimits.lower;
                    joint.threeObject.limit.upper = originalLimits.upper;
                }
                return;
            } else if (typeof joint.threeObject.setAngle === 'function') {
                joint.threeObject.setAngle(angle);

                // Restore original limits
                if (originalLimits && joint.threeObject.limit) {
                    joint.threeObject.limit.lower = originalLimits.lower;
                    joint.threeObject.limit.upper = originalLimits.upper;
                }
                return;
            }

            // Restore original limits (if none of the above methods executed)
            if (originalLimits && joint.threeObject.limit) {
                joint.threeObject.limit.lower = originalLimits.lower;
                joint.threeObject.limit.upper = originalLimits.upper;
            }

            // If none available, manually set rotation
            // Note: For URDF, we usually shouldn't reach here, as urdf-loader should provide setJointValue
            console.warn(`Joint ${joint.name} has no setJointValue or setAngle method, attempting manual setup`);
            if (joint.type === 'revolute' || joint.type === 'continuous') {
                const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(0, 0, 1);
                // Note: joint.threeObject may not be a Three.js object, cannot directly set rotation
                if (joint.threeObject.rotation) {
                    joint.threeObject.rotation.setFromAxisAngle(axis, angle);
                }
            } else if (joint.type === 'prismatic') {
                const axis = joint.axis ? new THREE.Vector3(...joint.axis.xyz).normalize() : new THREE.Vector3(1, 0, 0);
                if (joint.threeObject.position) {
                    joint.threeObject.position.copy(axis.multiplyScalar(angle));
                }
            }
        } else {
            console.warn(`Joint ${joint.name} has no threeObject`);
        }
    }
}

