import { Matrix } from '../../Math/matrix';
import { TransformStack } from './transform-stack';
import { vec } from '../../Math/vector';
import { Color } from '../../Color';
import { StateStack } from './state-stack';
import { Logger } from '../../Util/Log';
import { DebugText } from './debug-text';
import { RenderTarget } from './render-target';
import { TextureLoader } from './texture-loader';
// renderers
import { LineRenderer } from './line-renderer/line-renderer';
import { PointRenderer } from './point-renderer/point-renderer';
import { ScreenPassPainter } from './screen-pass-painter/screen-pass-painter';
import { ImageRenderer } from './image-renderer/image-renderer';
import { RectangleRenderer } from './rectangle-renderer/rectangle-renderer';
import { CircleRenderer } from './circle-renderer/circle-renderer';
import { Pool } from '../../Util/Pool';
import { DrawCall } from './draw-call';
import { AffineMatrix } from '../../Math/affine-matrix';
import { Material } from './material';
import { MaterialRenderer } from './material-renderer/material-renderer';
import { Shader } from './shader';
export const pixelSnapEpsilon = 0.0001;
class ExcaliburGraphicsContextWebGLDebug {
    constructor(_webglCtx) {
        this._webglCtx = _webglCtx;
        this._debugText = new DebugText();
    }
    /**
     * Draw a debugging rectangle to the context
     * @param x
     * @param y
     * @param width
     * @param height
     */
    drawRect(x, y, width, height, rectOptions = { color: Color.Black }) {
        this.drawLine(vec(x, y), vec(x + width, y), { ...rectOptions });
        this.drawLine(vec(x + width, y), vec(x + width, y + height), { ...rectOptions });
        this.drawLine(vec(x + width, y + height), vec(x, y + height), { ...rectOptions });
        this.drawLine(vec(x, y + height), vec(x, y), { ...rectOptions });
    }
    /**
     * Draw a debugging line to the context
     * @param start
     * @param end
     * @param lineOptions
     */
    drawLine(start, end, lineOptions = { color: Color.Black }) {
        this._webglCtx.draw('ex.line', start, end, lineOptions.color);
    }
    /**
     * Draw a debugging point to the context
     * @param point
     * @param pointOptions
     */
    drawPoint(point, pointOptions = { color: Color.Black, size: 5 }) {
        this._webglCtx.draw('ex.point', point, pointOptions.color, pointOptions.size);
    }
    drawText(text, pos) {
        this._debugText.write(this._webglCtx, text, pos);
    }
}
export class ExcaliburGraphicsContextWebGL {
    get z() {
        return this._state.current.z;
    }
    set z(value) {
        this._state.current.z = value;
    }
    get opacity() {
        return this._state.current.opacity;
    }
    set opacity(value) {
        this._state.current.opacity = value;
    }
    get tint() {
        return this._state.current.tint;
    }
    set tint(color) {
        this._state.current.tint = color;
    }
    get width() {
        return this.__gl.canvas.width;
    }
    get height() {
        return this.__gl.canvas.height;
    }
    get ortho() {
        return this._ortho;
    }
    /**
     * Checks the underlying webgl implementation if the requested internal resolution is supported
     * @param dim
     */
    checkIfResolutionSupported(dim) {
        // Slight hack based on this thread https://groups.google.com/g/webgl-dev-list/c/AHONvz3oQTo
        let supported = true;
        if (dim.width > 4096 || dim.height > 4096) {
            supported = false;
        }
        return supported;
    }
    constructor(options) {
        this._logger = Logger.getInstance();
        this._renderers = new Map();
        this._isDrawLifecycle = false;
        this.useDrawSorting = true;
        this._drawCallPool = new Pool(() => new DrawCall(), (instance) => {
            instance.priority = 0;
            instance.z = 0;
            instance.renderer = undefined;
            instance.args = undefined;
            return instance;
        }, 4000);
        this._drawCallIndex = 0;
        this._drawCalls = new Array(4000).fill(null);
        // Postprocessing is a tuple with 2 render targets, these are flip-flopped during the postprocessing process
        this._postProcessTargets = [];
        this._postprocessors = [];
        this._transform = new TransformStack();
        this._state = new StateStack();
        /**
         * Snaps the drawing x/y coordinate to the nearest whole pixel
         */
        this.snapToPixel = false;
        /**
         * Native context smoothing
         */
        this.smoothing = false;
        /**
         * Whether the pixel art sampler is enabled for smooth sub pixel anti-aliasing
         */
        this.pixelArtSampler = false;
        /**
         * UV padding in pixels to use in internal image rendering to prevent texture bleed
         *
         */
        this.uvPadding = 0.01;
        this.backgroundColor = Color.ExcaliburBlue;
        this.multiSampleAntialiasing = true;
        this.transparency = true;
        this._isContextLost = false;
        this._disposed = false;
        this._imageToWidth = new Map();
        this._imageToHeight = new Map();
        this.debug = new ExcaliburGraphicsContextWebGLDebug(this);
        this._totalPostProcessorTime = 0;
        const { canvasElement, context, enableTransparency, antialiasing, uvPadding, multiSampleAntialiasing, pixelArtSampler, powerPreference, snapToPixel, backgroundColor, useDrawSorting, handleContextLost, handleContextRestored } = options;
        this.__gl =
            context !== null && context !== void 0 ? context : canvasElement.getContext('webgl2', {
                antialias: antialiasing !== null && antialiasing !== void 0 ? antialiasing : this.smoothing,
                premultipliedAlpha: false,
                alpha: enableTransparency !== null && enableTransparency !== void 0 ? enableTransparency : this.transparency,
                depth: false,
                powerPreference: powerPreference !== null && powerPreference !== void 0 ? powerPreference : 'high-performance'
            });
        if (!this.__gl) {
            throw Error('Failed to retrieve webgl context from browser');
        }
        if (handleContextLost) {
            this.__gl.canvas.addEventListener('webglcontextlost', handleContextLost, false);
        }
        if (handleContextRestored) {
            this.__gl.canvas.addEventListener('webglcontextrestored', handleContextRestored, false);
        }
        this.__gl.canvas.addEventListener('webglcontextlost', () => {
            this._isContextLost = true;
        });
        this.__gl.canvas.addEventListener('webglcontextrestored', () => {
            this._isContextLost = false;
        });
        this.textureLoader = new TextureLoader(this.__gl);
        this.snapToPixel = snapToPixel !== null && snapToPixel !== void 0 ? snapToPixel : this.snapToPixel;
        this.smoothing = antialiasing !== null && antialiasing !== void 0 ? antialiasing : this.smoothing;
        this.transparency = enableTransparency !== null && enableTransparency !== void 0 ? enableTransparency : this.transparency;
        this.pixelArtSampler = pixelArtSampler !== null && pixelArtSampler !== void 0 ? pixelArtSampler : this.pixelArtSampler;
        this.uvPadding = uvPadding !== null && uvPadding !== void 0 ? uvPadding : this.uvPadding;
        this.multiSampleAntialiasing = typeof multiSampleAntialiasing === 'boolean' ? multiSampleAntialiasing : this.multiSampleAntialiasing;
        this.samples = typeof multiSampleAntialiasing === 'object' ? multiSampleAntialiasing.samples : undefined;
        this.backgroundColor = backgroundColor !== null && backgroundColor !== void 0 ? backgroundColor : this.backgroundColor;
        this.useDrawSorting = useDrawSorting !== null && useDrawSorting !== void 0 ? useDrawSorting : this.useDrawSorting;
        this._drawCallPool.disableWarnings = true;
        this._drawCallPool.preallocate();
        this._init();
    }
    dispose() {
        if (!this._disposed) {
            this._disposed = true;
            this.textureLoader.dispose();
            for (const renderer of this._renderers.values()) {
                renderer.dispose();
            }
            this._renderers.clear();
            this._drawCallPool.dispose();
            this._drawCalls.length = 0;
            this.__gl = null;
        }
    }
    _init() {
        const gl = this.__gl;
        // Setup viewport and view matrix
        this._ortho = Matrix.ortho(0, gl.canvas.width, gl.canvas.height, 0, 400, -400);
        gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
        // Clear background
        gl.clearColor(this.backgroundColor.r / 255, this.backgroundColor.g / 255, this.backgroundColor.b / 255, this.backgroundColor.a);
        gl.clear(gl.COLOR_BUFFER_BIT);
        // Enable alpha blending
        // https://www.realtimerendering.com/blog/gpus-prefer-premultiplication/
        gl.enable(gl.BLEND);
        gl.blendEquation(gl.FUNC_ADD);
        gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
        gl.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
        gl.blendFuncSeparate(gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
        // Setup builtin renderers
        this.register(new ImageRenderer({
            uvPadding: this.uvPadding,
            pixelArtSampler: this.pixelArtSampler
        }));
        this.register(new MaterialRenderer());
        this.register(new RectangleRenderer());
        this.register(new CircleRenderer());
        this.register(new PointRenderer());
        this.register(new LineRenderer());
        this.materialScreenTexture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, this.materialScreenTexture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, this.width, this.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
        gl.bindTexture(gl.TEXTURE_2D, null);
        this._screenRenderer = new ScreenPassPainter(gl);
        this._renderTarget = new RenderTarget({
            gl,
            transparency: this.transparency,
            width: gl.canvas.width,
            height: gl.canvas.height
        });
        this._postProcessTargets = [
            new RenderTarget({
                gl,
                transparency: this.transparency,
                width: gl.canvas.width,
                height: gl.canvas.height
            }),
            new RenderTarget({
                gl,
                transparency: this.transparency,
                width: gl.canvas.width,
                height: gl.canvas.height
            })
        ];
        this._msaaTarget = new RenderTarget({
            gl,
            transparency: this.transparency,
            width: gl.canvas.width,
            height: gl.canvas.height,
            antialias: this.multiSampleAntialiasing,
            samples: this.samples
        });
    }
    register(renderer) {
        this._renderers.set(renderer.type, renderer);
        renderer.initialize(this.__gl, this);
    }
    get(rendererName) {
        return this._renderers.get(rendererName);
    }
    _isCurrentRenderer(renderer) {
        if (!this._currentRenderer || this._currentRenderer === renderer) {
            return true;
        }
        return false;
    }
    beginDrawLifecycle() {
        this._isDrawLifecycle = true;
    }
    endDrawLifecycle() {
        this._isDrawLifecycle = false;
    }
    draw(rendererName, ...args) {
        if (!this._isDrawLifecycle) {
            this._logger.warnOnce(`Attempting to draw outside the the drawing lifecycle (preDraw/postDraw) is not supported and is a source of bugs/errors.\n` +
                `If you want to do custom drawing, use Actor.graphics, or any onPreDraw or onPostDraw handler.`);
        }
        if (this._isContextLost) {
            this._logger.errorOnce(`Unable to draw ${rendererName}, the webgl context is lost`);
            return;
        }
        const renderer = this._renderers.get(rendererName);
        if (renderer) {
            if (this.useDrawSorting) {
                const drawCall = this._drawCallPool.get();
                drawCall.z = this._state.current.z;
                drawCall.priority = renderer.priority;
                drawCall.renderer = rendererName;
                this.getTransform().clone(drawCall.transform);
                drawCall.state.z = this._state.current.z;
                drawCall.state.opacity = this._state.current.opacity;
                drawCall.state.tint = this._state.current.tint;
                drawCall.state.material = this._state.current.material;
                drawCall.args = args;
                this._drawCalls[this._drawCallIndex++] = drawCall;
            }
            else {
                // Set the current renderer if not defined
                if (!this._currentRenderer) {
                    this._currentRenderer = renderer;
                }
                if (!this._isCurrentRenderer(renderer)) {
                    // switching graphics means we must flush the previous
                    this._currentRenderer.flush();
                }
                // If we are still using the same renderer we can add to the current batch
                renderer.draw(...args);
                this._currentRenderer = renderer;
            }
        }
        else {
            throw Error(`No renderer with name ${rendererName} has been registered`);
        }
    }
    resetTransform() {
        this._transform.current = AffineMatrix.identity();
    }
    updateViewport(resolution) {
        const gl = this.__gl;
        this._ortho = this._ortho = Matrix.ortho(0, resolution.width, resolution.height, 0, 400, -400);
        this._renderTarget.setResolution(gl.canvas.width, gl.canvas.height);
        this._msaaTarget.setResolution(gl.canvas.width, gl.canvas.height);
        this._postProcessTargets[0].setResolution(gl.canvas.width, gl.canvas.height);
        this._postProcessTargets[1].setResolution(gl.canvas.width, gl.canvas.height);
    }
    _getImageWidth(image) {
        let maybeWidth = this._imageToWidth.get(image);
        if (maybeWidth === undefined) {
            maybeWidth = image.width;
            this._imageToWidth.set(image, maybeWidth);
        }
        return maybeWidth;
    }
    _getImageHeight(image) {
        let maybeHeight = this._imageToHeight.get(image);
        if (maybeHeight === undefined) {
            maybeHeight = image.height;
            this._imageToHeight.set(image, maybeHeight);
        }
        return maybeHeight;
    }
    drawImage(image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight) {
        if (swidth === 0 || sheight === 0) {
            return; // zero dimension dest exit early
        }
        else if (dwidth === 0 || dheight === 0) {
            return; // zero dimension dest exit early
        }
        else if (this._getImageWidth(image) === 0 || this._getImageHeight(image) === 0) {
            return; // zero dimension source exit early
        }
        if (!image) {
            Logger.getInstance().warn('Cannot draw a null or undefined image');
            // tslint:disable-next-line: no-console
            if (console.trace) {
                // tslint:disable-next-line: no-console
                console.trace();
            }
            return;
        }
        if (this._state.current.material) {
            this.draw('ex.material', image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
        }
        else {
            this.draw('ex.image', image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
        }
    }
    drawLine(start, end, color, thickness = 1) {
        this.draw('ex.rectangle', start, end, color, thickness);
    }
    drawRectangle(pos, width, height, color, stroke, strokeThickness) {
        this.draw('ex.rectangle', pos, width, height, color, stroke, strokeThickness);
    }
    drawCircle(pos, radius, color, stroke, thickness) {
        this.draw('ex.circle', pos, radius, color, stroke, thickness);
    }
    save() {
        this._transform.save();
        this._state.save();
    }
    restore() {
        this._transform.restore();
        this._state.restore();
    }
    translate(x, y) {
        this._transform.translate(this.snapToPixel ? ~~(x + pixelSnapEpsilon) : x, this.snapToPixel ? ~~(y + pixelSnapEpsilon) : y);
    }
    rotate(angle) {
        this._transform.rotate(angle);
    }
    scale(x, y) {
        this._transform.scale(x, y);
    }
    transform(matrix) {
        this._transform.current = matrix;
    }
    getTransform() {
        return this._transform.current;
    }
    multiply(m) {
        this._transform.current.multiply(m, this._transform.current);
    }
    addPostProcessor(postprocessor) {
        this._postprocessors.push(postprocessor);
        postprocessor.initialize(this.__gl);
    }
    removePostProcessor(postprocessor) {
        const index = this._postprocessors.indexOf(postprocessor);
        if (index !== -1) {
            this._postprocessors.splice(index, 1);
        }
    }
    clearPostProcessors() {
        this._postprocessors.length = 0;
    }
    updatePostProcessors(delta) {
        for (const postprocessor of this._postprocessors) {
            const shader = postprocessor.getShader();
            shader.use();
            const uniforms = shader.getUniforms();
            this._totalPostProcessorTime += delta;
            if (uniforms.find((u) => u.name === 'u_time_ms')) {
                shader.setUniformFloat('u_time_ms', this._totalPostProcessorTime);
            }
            if (uniforms.find((u) => u.name === 'u_elapsed_ms')) {
                shader.setUniformFloat('u_elapsed_ms', delta);
            }
            if (uniforms.find((u) => u.name === 'u_resolution')) {
                shader.setUniformFloatVector('u_resolution', vec(this.width, this.height));
            }
            if (postprocessor.onUpdate) {
                postprocessor.onUpdate(delta);
            }
        }
    }
    set material(material) {
        this._state.current.material = material;
    }
    get material() {
        return this._state.current.material;
    }
    /**
     * Creates and initializes the material which compiles the internal shader
     * @param options
     * @returns Material
     */
    createMaterial(options) {
        const material = new Material({ ...options, graphicsContext: this });
        return material;
    }
    createShader(options) {
        const gl = this.__gl;
        const { vertexSource, fragmentSource } = options;
        const shader = new Shader({
            gl,
            vertexSource,
            fragmentSource
        });
        shader.compile();
        return shader;
    }
    clear() {
        const gl = this.__gl;
        const currentTarget = this.multiSampleAntialiasing ? this._msaaTarget : this._renderTarget;
        currentTarget.use();
        gl.clearColor(this.backgroundColor.r / 255, this.backgroundColor.g / 255, this.backgroundColor.b / 255, this.backgroundColor.a);
        // Clear the context with the newly set color. This is
        // the function call that actually does the drawing.
        gl.clear(gl.COLOR_BUFFER_BIT);
    }
    /**
     * Flushes all batched rendering to the screen
     */
    flush() {
        if (this._isContextLost) {
            this._logger.errorOnce(`Unable to flush the webgl context is lost`);
            return;
        }
        // render target captures all draws and redirects to the render target
        let currentTarget = this.multiSampleAntialiasing ? this._msaaTarget : this._renderTarget;
        currentTarget.use();
        if (this.useDrawSorting) {
            // null out unused draw calls
            for (let i = this._drawCallIndex; i < this._drawCalls.length; i++) {
                this._drawCalls[i] = null;
            }
            // sort draw calls
            // Find the original order of the first instance of the draw call
            const originalSort = new Map();
            for (const [name] of this._renderers) {
                let firstIndex = 0;
                for (firstIndex = 0; firstIndex < this._drawCallIndex; firstIndex++) {
                    if (this._drawCalls[firstIndex].renderer === name) {
                        break;
                    }
                }
                originalSort.set(name, firstIndex);
            }
            this._drawCalls.sort((a, b) => {
                if (a === null || b === null) {
                    return 0;
                }
                const zIndex = a.z - b.z;
                const originalSortOrder = originalSort.get(a.renderer) - originalSort.get(b.renderer);
                const priority = a.priority - b.priority;
                if (zIndex === 0) {
                    // sort by z first
                    if (priority === 0) {
                        // sort by priority
                        return originalSortOrder; // use the original order to inform draw call packing to maximally preserve painter order
                    }
                    return priority;
                }
                return zIndex;
            });
            const oldTransform = this._transform.current;
            const oldState = this._state.current;
            if (this._drawCalls.length && this._drawCallIndex) {
                let currentRendererName = this._drawCalls[0].renderer;
                let currentRenderer = this._renderers.get(currentRendererName);
                for (let i = 0; i < this._drawCallIndex; i++) {
                    // hydrate the state for renderers
                    this._transform.current = this._drawCalls[i].transform;
                    this._state.current = this._drawCalls[i].state;
                    if (this._drawCalls[i].renderer !== currentRendererName) {
                        // switching graphics renderer means we must flush the previous
                        currentRenderer.flush();
                        currentRendererName = this._drawCalls[i].renderer;
                        currentRenderer = this._renderers.get(currentRendererName);
                    }
                    // ! hack to grab screen texture before materials run because they might want it
                    if (currentRenderer instanceof MaterialRenderer && this.material.isUsingScreenTexture) {
                        currentTarget.copyToTexture(this.materialScreenTexture);
                        currentTarget.use();
                    }
                    // If we are still using the same renderer we can add to the current batch
                    currentRenderer.draw(...this._drawCalls[i].args);
                }
                if (currentRenderer.hasPendingDraws()) {
                    currentRenderer.flush();
                }
            }
            // reset state
            this._transform.current = oldTransform;
            this._state.current = oldState;
            // reclaim draw calls
            this._drawCallPool.done();
            this._drawCallIndex = 0;
            this._imageToHeight.clear();
            this._imageToWidth.clear();
        }
        else {
            // This is the final flush at the moment to draw any leftover pending draw
            for (const renderer of this._renderers.values()) {
                if (renderer.hasPendingDraws()) {
                    renderer.flush();
                }
            }
        }
        currentTarget.disable();
        // post process step
        if (this._postprocessors.length > 0) {
            const source = currentTarget.toRenderSource();
            source.use();
        }
        // flip flop render targets for post processing
        for (let i = 0; i < this._postprocessors.length; i++) {
            currentTarget = this._postProcessTargets[i % 2];
            this._postProcessTargets[i % 2].use();
            this._screenRenderer.renderWithPostProcessor(this._postprocessors[i]);
            this._postProcessTargets[i % 2].toRenderSource().use();
        }
        // Final blit to the screen
        currentTarget.blitToScreen();
    }
}
//# sourceMappingURL=ExcaliburGraphicsContextWebGL.js.map