import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

/**
 * Base renderer providing shared Three.js setup, lighting, texture creation,
 * geometry building, and animation loop for LEGO model viewers.
 */
export class BaseRenderer {
    constructor(canvas) {
        this.canvas = canvas;
        this.animating = false;
        this.modelGroup = null;
        this.textures = new Map();

        this.scene = new THREE.Scene();

        this.camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);

        this.renderer = new THREE.WebGLRenderer({
            canvas,
            antialias: true,
            alpha: true
        });
        this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        this.renderer.setClearColor(0x000000, 0);

        this.setupLighting();

        this.controls = null;
        this._didDrag = false;
    }

    setupLighting() {
        const ambient = new THREE.AmbientLight(0xffffff, 0.8);
        this.scene.add(ambient);

        const sunLight = new THREE.DirectionalLight(0xffffff, 0.6);
        sunLight.position.set(1, 2, 3);
        this.scene.add(sunLight);
    }

    setupControls(target) {
        this.controls = new OrbitControls(this.camera, this.canvas);
        this.controls.enableZoom = true;
        this.controls.enablePan = true;
        this.controls.enableDamping = true;
        this.controls.dampingFactor = 0.1;
        this.controls.autoRotate = true;
        this.controls.autoRotateSpeed = 4.0;
        this.controls.target.copy(target);

        this.controls.addEventListener('start', () => {
            this.controls.autoRotate = false;
        });

        this._onPointerDown = (e) => {
            this._didDrag = false;
            this._pointerStart = { x: e.clientX, y: e.clientY };
        };
        this._onPointerMove = (e) => {
            if (!this._pointerStart) return;
            const dx = e.clientX - this._pointerStart.x;
            const dy = e.clientY - this._pointerStart.y;
            if (dx * dx + dy * dy > 9) this._didDrag = true;
        };

        this.canvas.addEventListener('pointerdown', this._onPointerDown);
        this.canvas.addEventListener('pointermove', this._onPointerMove);

        this._initialAutoRotate = this.controls.autoRotate;
        this.controls.saveState();
    }

    resetView() {
        if (!this.controls) return;
        this.controls.reset();
        this.controls.autoRotate = this._initialAutoRotate;
    }

    wasDragged() {
        return this._didDrag;
    }

    /**
     * Create a Three.js texture from parsed texture data
     */
    createTexture(textureData) {
        const canvas = document.createElement('canvas');
        canvas.width = textureData.width;
        canvas.height = textureData.height;
        const ctx = canvas.getContext('2d');

        const imageData = ctx.createImageData(textureData.width, textureData.height);
        for (let i = 0; i < textureData.pixels.length; i++) {
            const colorIdx = textureData.pixels[i];
            const color = textureData.palette[colorIdx] || { r: 0, g: 0, b: 0 };
            imageData.data[i * 4 + 0] = color.r;
            imageData.data[i * 4 + 1] = color.g;
            imageData.data[i * 4 + 2] = color.b;
            imageData.data[i * 4 + 3] = 255;
        }
        ctx.putImageData(imageData, 0, 0);

        const texture = new THREE.CanvasTexture(canvas);
        texture.minFilter = THREE.NearestFilter;
        texture.magFilter = THREE.NearestFilter;
        texture.wrapS = THREE.RepeatWrapping;
        texture.wrapT = THREE.RepeatWrapping;
        return texture;
    }

    /**
     * Create a single geometry from mesh data
     */
    createGeometry(mesh, lod) {
        if (!mesh.polygonIndices || mesh.polygonIndices.length === 0) {
            return null;
        }

        const hasTexture = mesh.textureIndices && mesh.textureIndices.length > 0;

        const vertexIndicesPacked = [];
        for (const poly of mesh.polygonIndices) {
            vertexIndicesPacked.push(poly.a, poly.b, poly.c);
        }

        const textureIndicesFlat = [];
        if (hasTexture) {
            for (const texPoly of mesh.textureIndices) {
                textureIndicesFlat.push(texPoly.a, texPoly.b, texPoly.c);
            }
        }

        const meshVertices = [];
        const meshNormals = [];
        const meshUvs = [];
        const indices = [];

        for (let i = 0; i < vertexIndicesPacked.length; i++) {
            const packed = vertexIndicesPacked[i];

            if ((packed & 0x80000000) !== 0) {
                indices.push(meshVertices.length);

                const gv = packed & 0xFFFF;
                const v = lod.vertices[gv] || { x: 0, y: 0, z: 0 };
                meshVertices.push([-v.x, v.y, v.z]);

                const gn = (packed >>> 16) & 0x7fff;
                const n = lod.normals[gn] || { x: 0, y: 1, z: 0 };
                meshNormals.push([-n.x, n.y, n.z]);

                if (hasTexture && lod.textureVertices && lod.textureVertices.length > 0) {
                    const tex = textureIndicesFlat[i];
                    const uv = lod.textureVertices[tex] || { u: 0, v: 0 };
                    meshUvs.push([uv.u, 1 - uv.v]);
                }
            } else {
                indices.push(packed & 0xFFFF);
            }
        }

        // Reverse face winding
        for (let i = 0; i < indices.length; i += 3) {
            const temp = indices[i];
            indices[i] = indices[i + 2];
            indices[i + 2] = temp;
        }

        const geometry = new THREE.BufferGeometry();
        geometry.setAttribute('position', new THREE.Float32BufferAttribute(meshVertices.flat(), 3));
        geometry.setAttribute('normal', new THREE.Float32BufferAttribute(meshNormals.flat(), 3));
        geometry.setIndex(indices);

        if (hasTexture && meshUvs.length > 0) {
            geometry.setAttribute('uv', new THREE.Float32BufferAttribute(meshUvs.flat(), 2));
        }

        return geometry;
    }

    centerAndScaleModel(scaleFactor) {
        if (!this.modelGroup) return;

        const box = new THREE.Box3().setFromObject(this.modelGroup);
        const center = box.getCenter(new THREE.Vector3());
        const size = box.getSize(new THREE.Vector3());

        this.modelGroup.position.sub(center);

        const maxDim = Math.max(size.x, size.y, size.z);
        if (maxDim > 0) {
            const scale = scaleFactor / maxDim;
            this.modelGroup.scale.setScalar(scale);
        }
    }

    clearModel() {
        if (this.modelGroup) {
            this.modelGroup.traverse((child) => {
                if (child instanceof THREE.Mesh) {
                    child.geometry?.dispose();
                    child.material?.dispose();
                }
            });
            this.scene.remove(this.modelGroup);
            this.modelGroup = null;
        }

        for (const texture of this.textures.values()) {
            texture.dispose();
        }
        this.textures.clear();
    }

    start() {
        this.animating = true;
        this.animate();
    }

    stop() {
        this.animating = false;
    }

    animate = () => {
        if (!this.animating) return;
        requestAnimationFrame(this.animate);

        this.updateAnimation();

        this.renderer.render(this.scene, this.camera);
    }

    /**
     * Override in subclasses for custom animation logic.
     * Called each frame before rendering.
     */
    updateAnimation() {
        this.controls?.update();
    }

    resize(width, height) {
        this.camera.aspect = width / height;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(width, height, false);
    }

    dispose() {
        this.animating = false;
        if (this.controls) {
            this.controls.dispose();
            this.canvas.removeEventListener('pointerdown', this._onPointerDown);
            this.canvas.removeEventListener('pointermove', this._onPointerMove);
        }
        this.clearModel();
        this.renderer?.dispose();
    }
}
