import { DrawParameters } from "./draw_parameters";
import { Rectangle } from "./rectangle";
import { epsilonCompare, round3Digits } from "./utils";

const floorSpriteCoordinates = false;

const ORIGINAL_SCALE = "1";

export class BaseSprite {
    /**
     * Returns the raw handle
     * @returns {HTMLImageElement|HTMLCanvasElement}
     */
    getRawTexture() {
        abstract;
        return null;
    }

    /**
     * Draws the sprite
     * @param {CanvasRenderingContext2D} context
     * @param {number} x
     * @param {number} y
     * @param {number} w
     * @param {number} h
     */
    draw(context, x, y, w, h) {
        // eslint-disable-line no-unused-vars
        abstract;
    }
}

/**
 * Position of a sprite within an atlas
 */
export class SpriteAtlasLink {
    /**
     *
     * @param {object} param0
     * @param {number} param0.packedX
     * @param {number} param0.packedY
     * @param {number} param0.packOffsetX
     * @param {number} param0.packOffsetY
     * @param {number} param0.packedW
     * @param {number} param0.packedH
     * @param {number} param0.w
     * @param {number} param0.h
     * @param {HTMLImageElement|HTMLCanvasElement} param0.atlas
     */
    constructor({ w, h, packedX, packedY, packOffsetX, packOffsetY, packedW, packedH, atlas }) {
        this.packedX = packedX;
        this.packedY = packedY;
        this.packedW = packedW;
        this.packedH = packedH;
        this.packOffsetX = packOffsetX;
        this.packOffsetY = packOffsetY;
        this.atlas = atlas;
        this.w = w;
        this.h = h;
    }
}

export class AtlasSprite extends BaseSprite {
    /**
     *
     * @param {object} param0
     * @param {string} param0.spriteName
     */
    constructor({ spriteName = "sprite" }) {
        super();
        /** @type {Object.<string, SpriteAtlasLink>} */
        this.linksByResolution = {};
        this.spriteName = spriteName;
    }

    getRawTexture() {
        return this.linksByResolution[ORIGINAL_SCALE].atlas;
    }

    /**
     * Draws the sprite onto a regular context using no contexts
     * @see {BaseSprite.draw}
     */
    draw(context, x, y, w, h) {
        if (G_IS_DEV) {
            assert(context instanceof CanvasRenderingContext2D, "Not a valid context");
        }

        const link = this.linksByResolution[ORIGINAL_SCALE];

        assert(
            link,
            "Link not known: " + ORIGINAL_SCALE + " (having " + Object.keys(this.linksByResolution) + ")"
        );

        const width = w || link.w;
        const height = h || link.h;

        const scaleW = width / link.w;
        const scaleH = height / link.h;

        context.drawImage(
            link.atlas,

            link.packedX,
            link.packedY,
            link.packedW,
            link.packedH,

            x + link.packOffsetX * scaleW,
            y + link.packOffsetY * scaleH,
            link.packedW * scaleW,
            link.packedH * scaleH
        );
    }

    /**
     *
     * @param {DrawParameters} parameters
     * @param {number} x
     * @param {number} y
     * @param {number} size
     * @param {boolean=} clipping
     */
    drawCachedCentered(parameters, x, y, size, clipping = true) {
        this.drawCached(parameters, x - size / 2, y - size / 2, size, size, clipping);
    }

    /**
     *
     * @param {CanvasRenderingContext2D} context
     * @param {number} x
     * @param {number} y
     * @param {number} size
     */
    drawCentered(context, x, y, size) {
        this.draw(context, x - size / 2, y - size / 2, size, size);
    }

    /**
     * Draws the sprite
     * @param {DrawParameters} parameters
     * @param {number} x
     * @param {number} y
     * @param {number} w
     * @param {number} h
     * @param {boolean=} clipping Whether to perform culling
     */
    drawCached(parameters, x, y, w = null, h = null, clipping = true) {
        if (G_IS_DEV) {
            assert(parameters instanceof DrawParameters, "Not a valid context");
            assert(!!w && w > 0, "Not a valid width:" + w);
            assert(!!h && h > 0, "Not a valid height:" + h);
        }

        const visibleRect = parameters.visibleRect;

        const scale = parameters.desiredAtlasScale;
        const link = this.linksByResolution[scale];

        assert(link, "Link not known: " + scale + " (having " + Object.keys(this.linksByResolution) + ")");

        const scaleW = w / link.w;
        const scaleH = h / link.h;

        let destX = x + link.packOffsetX * scaleW;
        let destY = y + link.packOffsetY * scaleH;
        let destW = link.packedW * scaleW;
        let destH = link.packedH * scaleH;

        let srcX = link.packedX;
        let srcY = link.packedY;
        let srcW = link.packedW;
        let srcH = link.packedH;

        let intersection = null;

        if (clipping) {
            const rect = new Rectangle(destX, destY, destW, destH);
            intersection = rect.getIntersection(visibleRect);
            if (!intersection) {
                return;
            }

            srcX += (intersection.x - destX) / scaleW;
            srcY += (intersection.y - destY) / scaleH;

            srcW *= intersection.w / destW;
            srcH *= intersection.h / destH;

            destX = intersection.x;
            destY = intersection.y;

            destW = intersection.w;
            destH = intersection.h;
        }

        // assert(epsilonCompare(scaleW, scaleH), "Sprite should be square for cached rendering");

        if (floorSpriteCoordinates) {
            parameters.context.drawImage(
                link.atlas,

                // atlas src pos
                Math.floor(srcX),
                Math.floor(srcY),

                // atlas src size
                Math.floor(srcW),
                Math.floor(srcH),

                // dest pos
                Math.floor(destX),
                Math.floor(destY),

                // dest size
                Math.floor(destW),
                Math.floor(destH)
            );
        } else {
            parameters.context.drawImage(
                link.atlas,

                // atlas src pos
                srcX,
                srcY,

                // atlas src siize
                srcW,
                srcH,

                // dest pos and size
                destX,
                destY,
                destW,
                destH
            );
        }
    }

    /**
     * Renders into an html element
     * @param {HTMLElement} element
     * @param {number} w
     * @param {number} h
     */
    renderToHTMLElement(element, w = 1, h = 1) {
        element.style.position = "relative";
        element.innerHTML = this.getAsHTML(w, h);
    }

    /**
     * Returns the html to render as icon
     * @param {number} w
     * @param {number} h
     */
    getAsHTML(w, h) {
        const link = this.linksByResolution["0.5"];

        // Find out how much we have to scale it so that it fits
        const scaleX = w / link.w;
        const scaleY = h / link.h;

        // Find out how big the scaled atlas is
        const atlasW = link.atlas.width * scaleX;
        const atlasH = link.atlas.height * scaleY;

        // @ts-ignore
        const srcSafe = link.atlas.src.replaceAll("\\", "/");

        // Find out how big we render the sprite
        const widthAbsolute = scaleX * link.packedW;
        const heightAbsolute = scaleY * link.packedH;

        // Compute the position in the relative container
        const leftRelative = (link.packOffsetX * scaleX) / w;
        const topRelative = (link.packOffsetY * scaleY) / h;
        const widthRelative = widthAbsolute / w;
        const heightRelative = heightAbsolute / h;

        // Scale the atlas relative to the width and height of the element
        const bgW = atlasW / widthAbsolute;
        const bgH = atlasH / heightAbsolute;

        // Figure out what the position of the atlas is
        const bgX = link.packedX * scaleX;
        const bgY = link.packedY * scaleY;

        // Fuck you, whoever thought its a good idea to make background-position work like it does now
        const bgXRelative = -bgX / (widthAbsolute - atlasW);
        const bgYRelative = -bgY / (heightAbsolute - atlasH);

        return `
            <span class="spritesheetImage" style="
                background-image: url('${srcSafe}');
                left: ${round3Digits(leftRelative * 100.0)}%;
                top: ${round3Digits(topRelative * 100.0)}%;
                width: ${round3Digits(widthRelative * 100.0)}%;
                height: ${round3Digits(heightRelative * 100.0)}%;
                background-repeat: repeat;
                background-position: ${round3Digits(bgXRelative * 100.0)}% ${round3Digits(
            bgYRelative * 100.0
        )}%;
                background-size: ${round3Digits(bgW * 100.0)}% ${round3Digits(bgH * 100.0)}%;
            "></span>
        `;
    }
}

export class RegularSprite extends BaseSprite {
    constructor(sprite, w, h) {
        super();
        this.w = w;
        this.h = h;
        this.sprite = sprite;
    }

    getRawTexture() {
        return this.sprite;
    }

    /**
     * Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
     * images into buffers
     * @param {CanvasRenderingContext2D} context
     * @param {number} x
     * @param {number} y
     * @param {number} w
     * @param {number} h
     */
    draw(context, x, y, w, h) {
        assert(context, "No context given");
        assert(x !== undefined, "No x given");
        assert(y !== undefined, "No y given");
        assert(w !== undefined, "No width given");
        assert(h !== undefined, "No height given");
        context.drawImage(this.sprite, x, y, w, h);
    }

    /**
     * Draws the sprite, do *not* use this for sprites which are rendered! Only for drawing
     * images into buffers
     * @param {CanvasRenderingContext2D} context
     * @param {number} x
     * @param {number} y
     * @param {number} w
     * @param {number} h
     */
    drawCentered(context, x, y, w, h) {
        assert(context, "No context given");
        assert(x !== undefined, "No x given");
        assert(y !== undefined, "No y given");
        assert(w !== undefined, "No width given");
        assert(h !== undefined, "No height given");
        context.drawImage(this.sprite, x - w / 2, y - h / 2, w, h);
    }
}
