import { spine } from "./spine-webgl.js";

export class SDV {
    constructor(sdName) {
        this.canvas;
        this.gl;
        this.shader;
        this.batcher;
        this.skeletonRenderer;
        this.assetManager;

        this.mvp = new spine.webgl.Matrix4();

        this.lastFrameTime;
        this.skeleton;

        this.selectedSD = sdName;

        this.load = this.load.bind(this);
        this.render = this.render.bind(this);
        this.resize = this.resize.bind(this);
        this.setupAnimationUI = this.setupAnimationUI.bind(this);
    }

    init() {
        // Setup canvas and WebGL context. We pass alpha: false to canvas.getContext() so we don't use premultiplied alpha when
        // loading textures. That is handled separately by PolygonBatcher.
        this.canvas = document.getElementById("sdv-canvas");
        this.canvas.width = window.innerWidth;
        this.canvas.height = window.innerHeight;
        const config = { alpha: false };
        this.gl = this.canvas.getContext("webgl", config) || this.canvas.getContext("experimental-webgl", config);
        if (!this.gl) {
            alert("WebGL is unavailable.");
            return;
        }

        // Create a simple shader, mesh, model-view-projection matrix, SkeletonRenderer, and AssetManager.
        this.shader = spine.webgl.Shader.newTwoColoredTextured(this.gl);
        this.batcher = new spine.webgl.PolygonBatcher(this.gl);
        this.mvp.ortho2d(0, 0, this.canvas.width - 1, this.canvas.height - 1);
        this.skeletonRenderer = new spine.webgl.SkeletonRenderer(this.gl);
        this.assetManager = new spine.webgl.AssetManager(this.gl);

        this.assetManager.loadBinary("../../../assets/char/" + this.selectedSD + ".skel");
        this.assetManager.loadTextureAtlas("../../../assets/char/" + this.selectedSD + ".atlas");
        requestAnimationFrame(this.load);
    }

    load() {
        // Wait until the AssetManager has loaded all resources, then load the skeletons.
        if (this.assetManager.isLoadingComplete()) {
            this.skeleton = this.loadSkeleton(this.selectedSD, "normal", false);
            this.setupUI();
            this.lastFrameTime = Date.now() / 1000;
            requestAnimationFrame(this.render); // Loading is done, call render every frame.
        } else {
            requestAnimationFrame(this.load);
        }
    }

    loadSkeleton(name, initialAnimation, premultipliedAlpha) {
        // Load the texture atlas using name.atlas from the AssetManager.
        const atlas = this.assetManager.get("../../../assets/char/" + name + ".atlas");

        // Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
        const atlasLoader = new spine.AtlasAttachmentLoader(atlas);

        // Create a SkeletonBinary instance for parsing the .skel file.
        const skeletonBinary = new spine.SkeletonBinary(atlasLoader);

        // Set the scale to apply during parsing, parse the file, and create a new skeleton.
        skeletonBinary.scale = 1;
        const skeletonData = skeletonBinary.readSkeletonData(
            this.assetManager.get("../../../assets/char/" + name + ".skel")
        );
        const skeleton = new spine.Skeleton(skeletonData);
        const bounds = this.calculateSetupPoseBounds(skeleton);

        // Create an AnimationState, and set the initial animation in looping mode.
        const animationStateData = new spine.AnimationStateData(skeleton.data);
        const animationState = new spine.AnimationState(animationStateData);
        animationState.setAnimation(0, initialAnimation, true);

        // Pack everything up and return to caller.
        return { skeleton: skeleton, state: animationState, bounds: bounds, premultipliedAlpha: premultipliedAlpha };
    }

    calculateSetupPoseBounds(skeleton) {
        skeleton.setToSetupPose();
        skeleton.updateWorldTransform();
        const offset = new spine.Vector2();
        const size = new spine.Vector2();
        skeleton.getBounds(offset, size, []);

        return { offset: offset, size: size };
    }

    setupUI() {
        const animationList = document.getElementById("sdv-animation-list");
        const skeleton = this.skeleton.skeleton;
        const state = this.skeleton.state;

        this.setupAnimationUI(animationList, skeleton, state);
    }

    setupAnimationUI(animationList, skeleton, state) {
        animationList.textContent = "";
        const activeAnimation = state.tracks[0].animation.name;
        for (let i = 0; i < skeleton.data.animations.length; i++) {
            const name = skeleton.data.animations[i].name;
            const option = document.createElement("option");
            option.setAttribute("value", name);
            option.textContent = name;
            if (name === activeAnimation) {
                option.setAttribute("selected", "selected");
            }
            animationList.appendChild(option);
        }

        animationList.addEventListener("change", () => {
            const state = this.skeleton.state;
            const skeleton = this.skeleton.skeleton;
            const animationName = animationList.value;
            skeleton.setToSetupPose();
            state.setAnimation(0, animationName, true);
        });
    }

    render() {
        const now = Date.now() / 1000;
        const delta = now - this.lastFrameTime;
        this.lastFrameTime = now;

        // Update the MVP matrix to adjust for canvas size changes
        this.resize();

        this.gl.clearColor(0.215, 0.215, 0.262, 1);
        this.gl.clear(this.gl.COLOR_BUFFER_BIT);

        // Apply the animation state based on the delta time.
        const skeleton = this.skeleton.skeleton;
        const state = this.skeleton.state;
        const premultipliedAlpha = this.skeleton.premultipliedAlpha;
        state.update(delta);
        state.apply(skeleton);
        skeleton.updateWorldTransform();

        // Bind the shader and set the texture and model-view-projection matrix.
        this.shader.bind();
        this.shader.setUniformi(spine.webgl.Shader.SAMPLER, 0);
        this.shader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, this.mvp.values);

        // Start the batch and tell the SkeletonRenderer to render the active skeleton.
        this.batcher.begin(this.shader);

        this.skeletonRenderer.premultipliedAlpha = premultipliedAlpha;
        this.skeletonRenderer.draw(this.batcher, skeleton);
        this.batcher.end();

        this.shader.unbind();

        requestAnimationFrame(this.render);
    }

    resize() {
        const w = this.canvas.clientWidth;
        const h = this.canvas.clientHeight;
        if (this.canvas.width != w || this.canvas.height != h) {
            this.canvas.width = w;
            this.canvas.height = h;
        }

        // Calculations to center the skeleton in the canvas.
        const bounds = this.skeleton.bounds;
        const centerX = bounds.offset.x + bounds.size.x / 2;
        const centerY = bounds.offset.y + bounds.size.y / 2;
        const scaleX = bounds.size.x / this.canvas.width;
        const scaleY = bounds.size.y / this.canvas.height;
        const scale = ((sX, sY) => {
            const s = Math.max(sX, sY) * 2;
            return s < 1 ? 1 : s;
        })(scaleX, scaleY);
        const width = this.canvas.width * scale;
        const height = this.canvas.height * scale;

        this.mvp.ortho2d(centerX - width / 2, centerY - height / 2, width, height);
        this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
    }
}
