var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { BufferAttribute, BufferGeometry, Color, DoubleSide, Material, Mesh, MeshBasicMaterial, NearestFilter, SRGBColorSpace, Texture } from "three";
import { serializable, serializeable } from "../engine/engine_serialization_decorator.js";
import { getParam } from "../engine/engine_utils.js";
import { NEEDLE_progressive } from "../engine/extensions/NEEDLE_progressive.js";
import { RGBAColor } from "../engine/js-extensions/index.js";
import { Behaviour } from "./Component.js";
const debug = getParam("debugspriterenderer");
const showWireframe = getParam("wireframe");
/**
 * @internal
 */
class SpriteUtils {
    static cache = {};
    static getOrCreateGeometry(sprite) {
        if (sprite.__cached_geometry)
            return sprite.__cached_geometry;
        if (sprite.guid) {
            if (SpriteUtils.cache[sprite.guid]) {
                if (debug)
                    console.log("Take cached geometry for sprite", sprite.guid);
                return SpriteUtils.cache[sprite.guid];
            }
        }
        const geo = new BufferGeometry();
        sprite.__cached_geometry = geo;
        const vertices = new Float32Array(sprite.triangles.length * 3);
        const uvs = new Float32Array(sprite.triangles.length * 2);
        for (let i = 0; i < sprite.triangles.length; i += 1) {
            const index = sprite.triangles[i];
            vertices[i * 3] = -sprite.vertices[index].x;
            vertices[i * 3 + 1] = sprite.vertices[index].y;
            vertices[i * 3 + 2] = 0;
            const uv = sprite.uv[index];
            uvs[i * 2] = uv.x;
            uvs[i * 2 + 1] = 1 - uv.y;
        }
        geo.setAttribute("position", new BufferAttribute(vertices, 3));
        geo.setAttribute("uv", new BufferAttribute(uvs, 2));
        if (sprite.guid)
            this.cache[sprite.guid] = geo;
        if (debug)
            console.log("Built sprite geometry", sprite, geo);
        return geo;
    }
}
/// <summary>
///   <para>SpriteRenderer draw mode.</para>
/// </summary>
export var SpriteDrawMode;
(function (SpriteDrawMode) {
    /// <summary>
    ///   <para>Displays the full sprite.</para>
    /// </summary>
    SpriteDrawMode[SpriteDrawMode["Simple"] = 0] = "Simple";
    /// <summary>
    ///   <para>The SpriteRenderer will render the sprite as a 9-slice image where the corners will remain constant and the other sections will scale.</para>
    /// </summary>
    SpriteDrawMode[SpriteDrawMode["Sliced"] = 1] = "Sliced";
    /// <summary>
    ///   <para>The SpriteRenderer will render the sprite as a 9-slice image where the corners will remain constant and the other sections will tile.</para>
    /// </summary>
    SpriteDrawMode[SpriteDrawMode["Tiled"] = 2] = "Tiled";
})(SpriteDrawMode || (SpriteDrawMode = {}));
class Vec2 {
    x;
    y;
}
function updateTextureIfNecessary(tex) {
    if (!tex)
        return;
    if (tex.colorSpace != SRGBColorSpace) {
        tex.colorSpace = SRGBColorSpace;
        tex.needsUpdate = true;
    }
    if (tex.minFilter == NearestFilter && tex.magFilter == NearestFilter) {
        tex.anisotropy = 1;
        tex.needsUpdate = true;
    }
}
/**
 * A sprite is a mesh that represents a 2D image. Used by the {@link SpriteRenderer} to render 2D images in the scene.
 * @summary 2D image renderer
 * @category Rendering
 * @group Components
 */
export class Sprite {
    constructor(texture) {
        if (texture) {
            this.texture = texture;
            this.triangles = [0, 1, 2, 0, 2, 3];
            this.uv = [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }, { x: 0, y: 1 }];
            this.vertices = [{ x: -.5, y: -.5 }, { x: .5, y: -.5 }, { x: .5, y: .5 }, { x: -.5, y: .5 }];
        }
    }
    guid;
    texture;
    triangles;
    uv;
    vertices;
    /** @internal */
    __cached_geometry;
    /**
     * The mesh that represents the sprite
     */
    get mesh() {
        if (!this._mesh) {
            this._mesh = new Mesh(SpriteUtils.getOrCreateGeometry(this), this.material);
        }
        return this._mesh;
    }
    _mesh;
    /**
     * The material used to render the sprite
     */
    get material() {
        if (!this._material) {
            if (this.texture) {
                updateTextureIfNecessary(this.texture);
            }
            this._material = new MeshBasicMaterial({
                map: this.texture,
                color: 0xffffff,
                side: DoubleSide,
                transparent: true
            });
        }
        return this._material;
    }
    _material;
    /**
     * The geometry of the sprite that can be used to create a mesh
     */
    getGeometry() {
        return SpriteUtils.getOrCreateGeometry(this);
    }
}
__decorate([
    serializable()
], Sprite.prototype, "guid", void 0);
__decorate([
    serializable(Texture)
], Sprite.prototype, "texture", void 0);
__decorate([
    serializeable()
], Sprite.prototype, "triangles", void 0);
__decorate([
    serializeable()
], Sprite.prototype, "uv", void 0);
__decorate([
    serializeable()
], Sprite.prototype, "vertices", void 0);
const $spriteTexOwner = Symbol("spriteOwner");
/**
 * @category Sprites
 */
export class SpriteSheet {
    sprites;
    constructor() {
        this.sprites = [];
    }
}
__decorate([
    serializable(Sprite)
], SpriteSheet.prototype, "sprites", void 0);
/**
 * Used by the {@link SpriteRenderer} to hold the sprite sheet and the currently active sprite index.
 *
 * @category Sprites
 */
export class SpriteData {
    static create() {
        const i = new SpriteData();
        i.spriteSheet = new SpriteSheet();
        return i;
    }
    // we don't assign anything here because it's used by the serialization system.
    // there's currently a limitation in the serializer when e.g. spriteSheet is already assigned it will not be overriden by the serializer
    // hence the spriteSheet field is undefined by default
    constructor() { }
    clone() {
        const i = new SpriteData();
        i.index = this.index;
        i.spriteSheet = this.spriteSheet;
        return i;
    }
    /**
     * Set the sprite to be rendered in the currently assigned sprite sheet at the currently active index {@link index}
     */
    set sprite(sprite) {
        if (!sprite) {
            return;
        }
        if (!this.spriteSheet) {
            this.spriteSheet = new SpriteSheet();
            this.spriteSheet.sprites = [sprite];
            this.index = 0;
        }
        else {
            if (this.index === null || this.index === undefined)
                this.index = 0;
            this.spriteSheet.sprites[this.index] = sprite;
        }
    }
    /** The currently active sprite */
    get sprite() {
        if (!this.spriteSheet)
            return undefined;
        return this.spriteSheet.sprites[this.index];
    }
    /**
     * The spritesheet holds all sprites that can be rendered by the sprite renderer
     */
    spriteSheet;
    /**
     * The index of the sprite to be rendered in the currently assigned sprite sheet
     */
    index = 0;
    update(material) {
        if (!this.spriteSheet)
            return;
        const index = this.index;
        if (index < 0 || index >= this.spriteSheet.sprites.length)
            return;
        const sprite = this.spriteSheet.sprites[index];
        const tex = sprite?.texture;
        if (!tex)
            return;
        updateTextureIfNecessary(tex);
        if (!sprite["__hasLoadedProgressive"]) {
            sprite["__hasLoadedProgressive"] = true;
            const previousTexture = tex;
            NEEDLE_progressive.assignTextureLOD(tex, 0).then(res => {
                if (res instanceof Texture) {
                    sprite.texture = res;
                    const shouldUpdateInMaterial = material?.["map"] === previousTexture;
                    if (shouldUpdateInMaterial) {
                        material["map"] = res;
                        material.needsUpdate = true;
                    }
                }
            });
        }
    }
}
__decorate([
    serializable(SpriteSheet)
], SpriteData.prototype, "spriteSheet", void 0);
__decorate([
    serializable()
], SpriteData.prototype, "index", void 0);
/**
 * The sprite renderer renders a sprite on a GameObject using an assigned spritesheet ({@link SpriteData}).
 *
 * - Example: https://engine.needle.tools/samples/spritesheet-animation
 *
 * @category Sprites
 * @group Components
 */
export class SpriteRenderer extends Behaviour {
    /** @internal The draw mode of the sprite renderer */
    drawMode = SpriteDrawMode.Simple;
    /** @internal Used when drawMode is set to Tiled */
    size = { x: 1, y: 1 };
    color;
    /**
     * The material that is used to render the sprite
     */
    sharedMaterial;
    // additional data
    transparent = true;
    cutoutThreshold = 0;
    castShadows = false;
    renderOrder = 0;
    toneMapped = true;
    /**
     * Assign a new texture to the currently active sprite
     */
    set texture(value) {
        if (!this._spriteSheet)
            return;
        const currentSprite = this._spriteSheet.spriteSheet?.sprites[this.spriteIndex];
        if (!currentSprite)
            return;
        currentSprite.texture = value;
        this.updateSprite();
    }
    /**
     * Add a new sprite to the currently assigned sprite sheet. The sprite will be added to the end of the sprite sheet.
     * Note that the sprite will not be rendered by default - set the `spriteIndex` to the index of the sprite to be rendered.
     * @param sprite The sprite to be added
     * @returns The index of the sprite in the sprite sheet
     * @example
     * ```typescript
     * const spriteRenderer = gameObject.addComponent(SpriteRenderer);
     * const index = spriteRenderer.addSprite(mySprite);
     * if(index >= 0)
     *   spriteRenderer.spriteIndex = index;
     * ```
     */
    addSprite(sprite, setActive = false) {
        if (!this._spriteSheet) {
            this._spriteSheet = SpriteData.create();
        }
        if (!this._spriteSheet.spriteSheet)
            return -1;
        this._spriteSheet.spriteSheet?.sprites.push(sprite);
        const index = this._spriteSheet.spriteSheet?.sprites.length - 1;
        if (setActive) {
            this.spriteIndex = index;
        }
        return index;
    }
    /**
     * Get the currently active sprite
     */
    get sprite() {
        return this._spriteSheet;
    }
    /**
     * Set the sprite to be rendered in the currently assigned sprite sheet at the currently active index {@link spriteIndex}
     */
    set sprite(value) {
        if (value === this._spriteSheet)
            return;
        if (typeof value === "number") {
            // the value if interpolated is sometimes *slightly* off (e.g. 0.999999 or 1.000001) so we round it
            const index = Math.round(value);
            if (debug)
                console.log("[SpriteSheet] Set index to " + index + " (was " + this.spriteIndex + ")", value);
            this.spriteIndex = index;
        }
        else if (value instanceof Sprite) {
            if (!this._spriteSheet) {
                this._spriteSheet = SpriteData.create();
            }
            if (this._spriteSheet.sprite != value) {
                this._spriteSheet.sprite = value;
            }
            this.updateSprite();
        }
        else if (value != this._spriteSheet) {
            this._spriteSheet = value;
            this.updateSprite();
        }
    }
    /**
     * Set the index of the sprite to be rendered in the currently assigned sprite sheet
     */
    set spriteIndex(value) {
        if (!this._spriteSheet)
            return;
        this._spriteSheet.index = value;
        this.updateSprite();
    }
    get spriteIndex() {
        return this._spriteSheet?.index ?? 0;
    }
    /**
     * Get the number of sprites in the currently assigned sprite sheet
     */
    get spriteFrames() {
        return this._spriteSheet?.spriteSheet?.sprites.length ?? 0;
    }
    _spriteSheet;
    _currentSprite;
    /** @internal */
    awake() {
        this._currentSprite = undefined;
        if (!this._spriteSheet) {
            this._spriteSheet = SpriteData.create();
        }
        else {
            // Ensure each SpriteRenderer has a unique spritesheet instance for cases where sprite renderer are cloned at runtime and then different sprites are assigned to each instance
            this._spriteSheet = this._spriteSheet.clone();
        }
        if (debug) {
            console.log("Awake", this.name, this, this.sprite);
        }
    }
    /** @internal */
    start() {
        if (!this._currentSprite)
            this.updateSprite();
        else if (this.gameObject)
            this.gameObject.add(this._currentSprite);
    }
    /**
     * Update the sprite. Modified properties will be applied to the sprite mesh. This method is called automatically when the sprite is changed.
     * @param force If true, the sprite will be forced to update.
     * @returns True if the sprite was updated successfully
     */
    updateSprite(force = false) {
        if (!this.__didAwake && !force)
            return false;
        const data = this._spriteSheet;
        if (!data?.spriteSheet?.sprites) {
            console.warn("SpriteRenderer has no data or spritesheet assigned...");
            return false;
        }
        const sprite = data.spriteSheet.sprites[this.spriteIndex];
        if (!sprite) {
            if (debug)
                console.warn("Sprite not found", this.spriteIndex, data.spriteSheet.sprites);
            return false;
        }
        if (!this._currentSprite) {
            const mat = new MeshBasicMaterial({ color: 0xffffff, side: DoubleSide });
            if (showWireframe)
                mat.wireframe = true;
            if (this.color) {
                if (!mat["color"])
                    mat["color"] = new Color();
                mat["color"].copy(this.color);
                mat["opacity"] = this.color.alpha;
            }
            mat.transparent = true;
            mat.toneMapped = this.toneMapped;
            mat.depthWrite = false;
            if (sprite.texture && !mat.wireframe) {
                let tex = sprite.texture;
                // the sprite renderer modifies the texture offset and scale
                // so we need to clone the texture
                // if the same texture is used multiple times
                if (tex[$spriteTexOwner] !== undefined && tex[$spriteTexOwner] !== this && this.spriteFrames > 1) {
                    tex = sprite.texture = tex.clone();
                }
                tex[$spriteTexOwner] = this;
                mat["map"] = tex;
            }
            this.sharedMaterial = mat;
            this._currentSprite = new Mesh(SpriteUtils.getOrCreateGeometry(sprite), mat);
            this._currentSprite.renderOrder = Math.round(this.renderOrder);
            NEEDLE_progressive.assignTextureLOD(mat, 0);
        }
        else {
            this._currentSprite.geometry = SpriteUtils.getOrCreateGeometry(sprite);
            this._currentSprite.material["map"] = sprite.texture;
        }
        if (this._currentSprite.parent !== this.gameObject) {
            if (this.drawMode === SpriteDrawMode.Tiled)
                this._currentSprite.scale.set(this.size.x, this.size.y, 1);
            if (this.gameObject)
                this.gameObject.add(this._currentSprite);
        }
        if (this._currentSprite) {
            this._currentSprite.layers.set(this.layer);
        }
        if (this.sharedMaterial) {
            this.sharedMaterial.alphaTest = this.cutoutThreshold;
            this.sharedMaterial.transparent = this.transparent;
        }
        this._currentSprite.castShadow = this.castShadows;
        data?.update(this.sharedMaterial);
        return true;
    }
}
__decorate([
    serializable()
], SpriteRenderer.prototype, "drawMode", void 0);
__decorate([
    serializable(Vec2)
], SpriteRenderer.prototype, "size", void 0);
__decorate([
    serializable(RGBAColor)
], SpriteRenderer.prototype, "color", void 0);
__decorate([
    serializable(Material)
], SpriteRenderer.prototype, "sharedMaterial", void 0);
__decorate([
    serializable()
], SpriteRenderer.prototype, "transparent", void 0);
__decorate([
    serializable()
], SpriteRenderer.prototype, "cutoutThreshold", void 0);
__decorate([
    serializable()
], SpriteRenderer.prototype, "castShadows", void 0);
__decorate([
    serializable()
], SpriteRenderer.prototype, "renderOrder", void 0);
__decorate([
    serializable()
], SpriteRenderer.prototype, "toneMapped", void 0);
__decorate([
    serializable(SpriteData)
], SpriteRenderer.prototype, "sprite", null);
//# sourceMappingURL=SpriteRenderer.js.map