import { fixedWidth, fixedHeight, tileBleedShrinkFix, pixelated, fixedFitToWindow, maxHeight, maxWidth } from "./index";
import { ASSERT, debugInit, debugUpdate, debugRender, debug, showWatermark } from "./index";
import { min, lerp, vec2, isOverlapping } from "./index";
import { tileImage, mainCanvas, setMainCanvas,  mainContext, setMainContext, overlayCanvas, setOverlayCanvas, overlayContext, setOverlayContext, setMainCanvasSize } from "./index";
import { medalsRender } from "./index";
import { glInit, glPreRender } from "./index";
import { glCanvas } from "./index";
import { keyIsDown, inputUpdatePost, inputUpdate, glCopyToContext } from "./index";

/*
    LittleJS - The Tiny JavaScript Game Engine That Can!
    MIT License - Copyright 2021 Frank Force

    Engine Features
    - Object oriented system with base class engine object
    - Base class object handles update, physics, collision, rendering, etc
    - Engine helper classes and functions like Vector2, Color, and Timer
    - Super fast rendering system for tile sheets
    - Sound effects audio with zzfx and music with zzfxm
    - Input processing system with gamepad and touchscreen support
    - Tile layer rendering and collision system
    - Particle effect system
    - Medal system tracks and displays achievements
    - Debug tools and debug rendering system
    - Call engineInit() to start it up!
*/


/** Name of engine */
export const engineName = 'LittleJS';

/** Version of engine */
export const engineVersion = '1.1.2';

/** Frames per second to update objects
 *  @default */
export const FPS = 60;

/** How many seconds each frame lasts, engine uses a fixed time step
 *  @default 1/60 */
export const timeDelta = 1 / FPS;

/** Array containing all engine objects */
export let engineObjects: any = [];

/** Array containing only objects that are set to collide with other objects (for optimization) */
export let engineCollideObjects: any = [];

/** Current update frame, used to calculate time */
export let frame = 0;

/** Current engine time since start in seconds, derived from frame */
export let time = 0;

/** Actual clock time since start in seconds (not affected by pause or frame rate clamping) */
export let timeReal = 0;

/** Is the game paused? Causes time and objects to not be updated. */
export let paused = 0;

// Engine internal variables not exposed to documentation
export let frameTimeLastMS = 0, frameTimeBufferMS = 0, debugFPS = 0,
    shrinkTilesX: number, shrinkTilesY: number, drawCount: any, tileImageSize: any, tileImageSizeInverse: any;

export function setDrawCount(count: number): number {
  return drawCount = count;
}

///////////////////////////////////////////////////////////////////////////////

/** Start up LittleJS engine with your callback functions
 *  @param {Function} gameInit       - Called once after the engine starts up, setup the game
 *  @param {Function} gameUpdate     - Called every frame at 60 frames per second, handle input and update the game state
 *  @param {Function} gameUpdatePost - Called after physics and objects are updated, setup camera and prepare for render
 *  @param {Function} gameRender     - Called before objects are rendered, draw any background effects that appear behind objects
 *  @param {Function} gameRenderPost - Called after objects are rendered, draw effects or hud that appear above all objects
 *  @param {String} tileImageSource  - Tile image to use, everything starts when the image is finished loading
 */

 export function engineInit(gameInit: any, gameUpdate: any, gameUpdatePost: any, gameRender: any, gameRenderPost: any, tileImageSource: any) {
    // init engine when tiles load
    tileImage.onload = () => {
        // save tile image info

        tileImageSizeInverse = vec2(1).divide(tileImageSize = vec2(tileImage.width, tileImage.height));

        debug && (tileImage.onload = () => ASSERT(1)); // tile sheet can not reloaded
        shrinkTilesX = tileBleedShrinkFix / tileImageSize.x;
        shrinkTilesY = tileBleedShrinkFix / tileImageSize.y;

        // setup html
        document.body.appendChild(setMainCanvas(document.createElement('canvas')));

        document.body.setAttribute('style', 'margin:0;overflow:hidden;background:#000');
        mainCanvas.setAttribute('style', 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)' +
            (pixelated ? ';image-rendering:crisp-edges;image-rendering:pixelated' : '')); // pixelated rendering
        setMainContext(mainCanvas.getContext('2d'));

        // init stuff and start engine
        debugInit();
        glInit();

        // create overlay canvas for hud to appear above gl canvas
        document.body.appendChild(setOverlayCanvas(document.createElement('canvas')));
        overlayCanvas.setAttribute('style', 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)');
        setOverlayContext(overlayCanvas.getContext('2d'));

        gameInit();
        engineUpdate();
    };

    // main update loop
    const engineUpdate = (frameTimeMS = 0) => {
        requestAnimationFrame(engineUpdate);

        // update time keeping
        let frameTimeDeltaMS = frameTimeMS - frameTimeLastMS;
        frameTimeLastMS = frameTimeMS;
        if (debug || showWatermark)
            debugFPS = lerp(.05, 1e3 / (frameTimeDeltaMS || 1), debugFPS);
        if (debug)
            frameTimeDeltaMS *= keyIsDown(107) ? 5 : keyIsDown(109) ? .2 : 1; // +/- to speed/slow time
        timeReal += frameTimeDeltaMS / 1e3;

        // @ts-expect-error ts-migrate(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
        frameTimeBufferMS = min(frameTimeBufferMS + !paused * frameTimeDeltaMS, 50); // clamp incase of slow framerate

        if (paused) {
            // do post update even when paused
            inputUpdate();
            debugUpdate();
            gameUpdatePost();
            inputUpdatePost();
        }
        else {
            // apply time delta smoothing, improves smoothness of framerate in some browsers
            let deltaSmooth = 0;
            if (frameTimeBufferMS < 0 && frameTimeBufferMS > -9) {
                // force an update each frame if time is close enough (not just a fast refresh rate)
                deltaSmooth = frameTimeBufferMS;
                frameTimeBufferMS = 0;
            }

            // update multiple frames if necessary in case of slow framerate
            for (; frameTimeBufferMS >= 0; frameTimeBufferMS -= 1e3 / FPS) {
                // update game and objects
                inputUpdate();
                gameUpdate();
                engineObjectsUpdate();

                // do post update
                debugUpdate();
                gameUpdatePost();
                inputUpdatePost();
            }

            // add the time smoothing back in
            frameTimeBufferMS += deltaSmooth;
        }

        if (fixedWidth) {
            // clear set fixed size
            mainCanvas.width = fixedWidth;
            mainCanvas.height = fixedHeight;

            if (fixedFitToWindow) {
                // fit to window by adding space on top or bottom if necessary
                const aspect = innerWidth / innerHeight;
                const fixedAspect = fixedWidth / fixedHeight;
                mainCanvas.style.width = overlayCanvas.style.width = aspect < fixedAspect ? '100%' : '';
                mainCanvas.style.height = overlayCanvas.style.height = aspect < fixedAspect ? '' : '100%';
                if (glCanvas) {
                    glCanvas.style.width = mainCanvas.style.width;
                    glCanvas.style.height = mainCanvas.style.height;
                }
            }
        }
        else {
            // clear and set size to same as window
            mainCanvas.width = min(innerWidth, maxWidth);
            mainCanvas.height = min(innerHeight, maxHeight);
        }

        // save canvas size and clear overlay canvas
        setMainCanvasSize(vec2(overlayCanvas.width = mainCanvas.width, overlayCanvas.height = mainCanvas.height));
        mainContext.imageSmoothingEnabled = !pixelated; // disable smoothing for pixel art

        // render sort then render while removing destroyed objects
        glPreRender(mainCanvas.width, mainCanvas.height);
        gameRender();

        engineObjects.sort((a: any, b: any) => a.renderOrder - b.renderOrder);
        for (const o of engineObjects)
            o.destroyed || o.render();
        gameRenderPost();
        medalsRender();
        debugRender();

        glCopyToContext(mainContext);

        if (showWatermark) {
            // update fps
            overlayContext.textAlign = 'right';
            overlayContext.textBaseline = 'top';
            overlayContext.font = '1em monospace';
            overlayContext.fillStyle = '#000';
            const text = engineName + ' ' + 'v' + engineVersion + ' / '
                + drawCount + ' / ' + engineObjects.length + ' / ' + debugFPS.toFixed(1);
            overlayContext.fillText(text, mainCanvas.width - 3, 3);
            overlayContext.fillStyle = '#fff';
            overlayContext.fillText(text, mainCanvas.width - 2, 2);
            drawCount = 0;
        }
    }

    // set tile image source to load the image and start the engine

    tileImageSource ? tileImage.src = tileImageSource : tileImage.onload;
}


///////////////////////////////////////////////////////////////////////////////

/** Calls update on each engine object (recursively if child), removes destroyed objects, and updated time */

export function engineObjectsUpdate() {
    // recursive object update
    const updateObject = (o: any) => {
        if (!o.destroyed) {
            o.update();
            for (const child of o.children)
                updateObject(child);
        }
    }
    for (const o of engineObjects)
        o.parent || updateObject(o);

    // remove destroyed objects

    engineObjects = engineObjects.filter((o: any) => !o.destroyed);

    engineCollideObjects = engineCollideObjects.filter((o: any) => !o.destroyed);

    // increment frame and update time
    time = ++frame / FPS;
}

/** Detroy and remove all objects that are not persistent or descendants of a persistent object */

export function engineObjectsDestroy() {
    for (const o of engineObjects)
        o.persistent || o.parent || o.destroy();

    engineObjects = engineObjects.filter((o: any) => !o.destroyed);
}

/** Triggers a callback for each object within a given area
 *  @param {Vector2} [pos] - Center of test area
 *  @param {Number} [size] - Radius of circle if float, rectangle size if Vector2
 *  @param {Function} [callbackFunction] - Calls this function on every object that passes the test
 *  @param {Array} [objects=engineObjects] - List of objects to check */


 export function engineObjectsCallback(pos: any, size: any, callbackFunction: any, objects = engineObjects) {
    if (!pos) {
        // all objects
        for (const o of objects)
            callbackFunction(o);
    }
    else if (size.x != undefined) {
        // aabb test
        for (const o of objects)
            isOverlapping(pos, size, o.pos, o.size) && callbackFunction(o);
    }
    else {
        // circle test
        const sizeSquared = size * size;
        for (const o of objects)
            pos.distanceSquared(o.pos) < sizeSquared && callbackFunction(o);
    }
}

