import { TransformNode } from '@babylonjs/core/Meshes/transformNode';
import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight';
import { SpotLight } from '@babylonjs/core/Lights/spotLight';
import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator';
import { CubeTexture } from '@babylonjs/core/Materials/Textures/cubeTexture';
import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
import { Layer } from '@babylonjs/core/Layers/layer';
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Color3, Color4 } from '@babylonjs/core/Maths/math.color';
import type { Engine, Scene } from '@babylonjs/core';
import { ViewCubeBuilder, type ViewCubeUICallbacks } from './ViewCubeBuilder';
import { AdomStatics } from '../AdomStatics';
import { STUDIO_ENV_BASE64 } from '../assets/studioEnv';
import { initAxisHelpers } from './AxisHelpers';

export interface SceneBuilderOptions {
    zUp: boolean;
    showViewCube: boolean;
    environmentUrl?: string;
    viewCubeCallbacks?: ViewCubeUICallbacks;
}

export class SceneBuilder {
    static init(engine: Engine, options: SceneBuilderOptions = { zUp: true, showViewCube: true }): Promise<void> {
        if (!AdomStatics.scene) {
            console.error("SceneBuilder Error: AdomStatics.scene is null.");
            return Promise.reject(new Error("Scene is undefined in AdomStatics."));
        }
        const scene = AdomStatics.scene;
        const { zUp, showViewCube, environmentUrl, viewCubeCallbacks } = options;

        return new Promise<void>((resolve, reject) => {
            try {
                const rootNode = new TransformNode('ViewerRoot', scene);

                const hemDir = zUp ? new Vector3(0, 0, 1) : new Vector3(0, 1, 0);
                const hemisphericLight = new HemisphericLight('light', hemDir, scene);
                hemisphericLight.intensity = 0.2;
                hemisphericLight.parent = rootNode;
                hemisphericLight.diffuse = new Color3(1, 1, 1);
                hemisphericLight.groundColor = new Color3(0.65, 0.65, 0.65);

                // Bottom fill light — illuminates the underside of parts so a
                // user inspecting BGA balls / QFN pads / through-hole leg
                // bottoms can see them clearly. Default OFF (intensity 0)
                // because top-down lighting + ambient is the cleaner default
                // for the typical "look at chip from above" use case;
                // toggled on via ThreeDViewer.setBottomLight() when the user
                // wants to inspect the bottom face. The direction vector
                // points UP (toward +Z in Z-up, +Y in Y-up) so the upper
                // hemisphere of the light wraps around the model from below.
                const bottomDir = zUp ? new Vector3(0, 0, -1) : new Vector3(0, -1, 0);
                const bottomLight = new HemisphericLight('bottomLight', bottomDir, scene);
                bottomLight.intensity = 0;  // toggled to ~0.6 by setBottomLight(true)
                bottomLight.parent = rootNode;
                bottomLight.diffuse = new Color3(1, 1, 1);
                bottomLight.groundColor = new Color3(0.5, 0.5, 0.5);
                AdomStatics.bottomLight = bottomLight;

                // SpotLight — sole direct light, matching Hydrogen.
                // Cone naturally darkens ground edges. Repositioned by
                // frameModel() proportionally to the loaded model's size.
                const spotPos = zUp
                    ? new Vector3(600, -300, 1500)
                    : new Vector3(600, 1500, -300);
                const spotDir = new Vector3(0, 0, 0).subtract(spotPos).normalize();
                const spotLight = new SpotLight('spotLight', spotPos, spotDir, Math.PI * 0.7, 50, scene);
                spotLight.intensity = 2_000_000;
                spotLight.parent = rootNode;
                AdomStatics.spotLight = spotLight;

                const shadowGenerator = new ShadowGenerator(8192, spotLight);
                scene.shadowsEnabled = true;
                shadowGenerator.usePercentageCloserFiltering = true;
                shadowGenerator.filteringQuality = ShadowGenerator.QUALITY_HIGH;
                shadowGenerator.bias = 0.0002 / 1000;
                shadowGenerator.normalBias = 0.08 / 1000;
                shadowGenerator.darkness = -1;
                shadowGenerator.forceBackFacesOnly = true;
                shadowGenerator.frustumEdgeFalloff = 0;

                AdomStatics.shadowGenerator = shadowGenerator;

                // Catch incompatible shadow-filter combos no matter when a
                // consumer sets them (pages flip flags long after load).
                scene.onBeforeRenderObservable.add(() => {
                    AdomStatics.enforceShadowFilterCompat();
                });

                // Studio environment map for PBR reflections + skybox.
                // Bundled as base64 so consumers don't need to copy assets.
                // environmentUrl prop lets them override with their own .env file.
                if (environmentUrl) {
                    scene.environmentTexture = CubeTexture.CreateFromPrefilteredData(environmentUrl, scene);
                } else {
                    const raw = atob(STUDIO_ENV_BASE64.split(',')[1]);
                    const bytes = new Uint8Array(raw.length);
                    for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
                    const blobUrl = URL.createObjectURL(new Blob([bytes], { type: 'application/octet-stream' }));
                    scene.environmentTexture = new CubeTexture(blobUrl, scene, null, false, null, null, null, undefined, true, '.env');
                }

                // 3d-viewer-design skill §3b — neutral steel-blue gradient.
                // Top  ≈ #5a6b7e  (0.353, 0.420, 0.494)  ~37% luma
                // Bot  ≈ #2a3340  (0.165, 0.200, 0.251)  ~20% luma
                // Both ends deliberately brighter than near-black so dark IC
                // bodies stay visible in any vertical position.
                //
                // Rendered as a SCREEN-SPACE background layer, not a skybox
                // mesh. The old 5000-unit skybox was world geometry: on real
                // GPUs its far corners fell into the last sliver of depth
                // precision (near planes here can be ~0.0003 for meter-scale
                // parts) and got far-plane clipped into hard triangular seams
                // that swept across the background while orbiting (adom/wiki
                // #45). A background layer has no depth at all: it cannot
                // clip, seam, or z-fight, and the gradient is identical at
                // every camera angle. PBR reflections on the model still come
                // from scene.environmentTexture, which is untouched.
                const SKILL_BG_TOP = '#5a6b7e';
                const SKILL_BG_BOTTOM = new Color3(0.165, 0.200, 0.251);
                const bgTex = new DynamicTexture('bgGradientTex', { width: 4, height: 512 }, scene, false);
                const bgCtx = bgTex.getContext();
                const grad = bgCtx.createLinearGradient(0, 0, 0, 512);
                grad.addColorStop(0, SKILL_BG_TOP);
                grad.addColorStop(1, SKILL_BG_BOTTOM.toHexString());
                bgCtx.fillStyle = grad;
                bgCtx.fillRect(0, 0, 4, 512);
                bgTex.update();
                const bgLayer = new Layer('bgGradient', null, scene, true);
                bgLayer.texture = bgTex;

                scene.environmentIntensity = 1.0;
                scene.clearColor = new Color4(SKILL_BG_BOTTOM.r, SKILL_BG_BOTTOM.g, SKILL_BG_BOTTOM.b, 1);

                if (showViewCube && AdomStatics.camera) {
                    ViewCubeBuilder.createViewCube(scene, AdomStatics.camera, engine, viewCubeCallbacks);
                }

                // 3d-viewer-design §8 axis helpers — non-negotiable.
                // §8a world-origin axes (default ON, toggleable via API)
                // §8c screen-space corner triad (always on)
                if (AdomStatics.camera) {
                    initAxisHelpers(scene, AdomStatics.camera, { worldDefaultOn: true });
                }

                scene.onPointerObservable.add(() => { /* Ensure pointer events */ });

                const canvas = engine.getRenderingCanvas();
                if (canvas) {
                    canvas.addEventListener('contextmenu', (event) => event.preventDefault());
                }

                resolve();
            } catch (error) {
                console.error("SceneBuilder Error during setup:", error);
                reject(error instanceof Error ? error : new Error(String(error)));
            }
        });
    }
}
