// @flow

import browser from '../util/browser.js';
import window from '../util/window.js';

import {mat4} from 'gl-matrix';
import SourceCache from '../source/source_cache.js';
import EXTENT from '../data/extent.js';
import pixelsToTileUnits from '../source/pixels_to_tile_units.js';
import SegmentVector from '../data/segment.js';
import {PosArray, TileBoundsArray, TriangleIndexArray, LineStripIndexArray} from '../data/array_types.js';
import {values} from '../util/util.js';
import {Evented, Event} from '../util/evented.js';
import {isMapAuthenticated} from '../util/mapbox.js';
import posAttributes from '../data/pos_attributes.js';
import boundsAttributes from '../data/bounds_attributes.js';
import ProgramConfiguration from '../data/program_configuration.js';
import CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index.js';
import shaders from '../shaders/shaders.js';
import Program from './program.js';
import {programUniforms} from './program/program_uniforms.js';
import Context from '../gl/context.js';
import {fogUniformValues} from '../render/fog.js';
import DepthMode from '../gl/depth_mode.js';
import StencilMode from '../gl/stencil_mode.js';
import ColorMode from '../gl/color_mode.js';
import CullFaceMode from '../gl/cull_face_mode.js';
import Texture from './texture.js';
import {clippingMaskUniformValues} from './program/clipping_mask_program.js';
import Color from '../style-spec/util/color.js';
import symbol from './draw_symbol.js';
import circle from './draw_circle.js';
import assert from 'assert';
import heatmap from './draw_heatmap.js';
import line from './draw_line.js';
import fill from './draw_fill.js';
import fillExtrusion from './draw_fill_extrusion.js';
import hillshade from './draw_hillshade.js';
import raster from './draw_raster.js';
import background from './draw_background.js';
import debug, {drawDebugPadding, drawDebugQueryGeometry} from './draw_debug.js';
import custom from './draw_custom.js';
import sky from './draw_sky.js';
import drawGlobeAtmosphere from './draw_globe_atmosphere.js';
import {GlobeSharedBuffers, globeToMercatorTransition} from '../geo/projection/globe_util.js';
import {Terrain} from '../terrain/terrain.js';
import {Debug} from '../util/debug.js';
import Tile from '../source/tile.js';
import {RGBAImage} from '../util/image.js';

const draw = {
    symbol,
    circle,
    heatmap,
    line,
    fill,
    'fill-extrusion': fillExtrusion,
    hillshade,
    raster,
    background,
    sky,
    debug,
    custom
};

import type Transform from '../geo/transform.js';
import type {OverscaledTileID, UnwrappedTileID} from '../source/tile_id.js';
import type Style from '../style/style.js';
import type StyleLayer from '../style/style_layer.js';
import type {CrossFaded} from '../style/properties.js';
import type LineAtlas from './line_atlas.js';
import type ImageManager from './image_manager.js';
import type GlyphManager from './glyph_manager.js';
import type VertexBuffer from '../gl/vertex_buffer.js';
import type IndexBuffer from '../gl/index_buffer.js';
import type {DepthRangeType, DepthMaskType, DepthFuncType} from '../gl/types.js';
import type ResolvedImage from '../style-spec/expression/types/resolved_image.js';
import type {DynamicDefinesType} from './program/program_uniforms.js';

export type RenderPass = 'offscreen' | 'opaque' | 'translucent' | 'sky';
export type CanvasCopyInstances = {
    canvasCopies: WebGLTexture[],
    timeStamps: number[]
}

type PainterOptions = {
    showOverdrawInspector: boolean,
    showTileBoundaries: boolean,
    showTerrainWireframe: boolean,
    showQueryGeometry: boolean,
    showPadding: boolean,
    rotating: boolean,
    zooming: boolean,
    moving: boolean,
    gpuTiming: boolean,
    gpuTimingDeferredRender: boolean,
    fadeDuration: number,
    isInitialLoad: boolean,
    speedIndexTiming: boolean
}

type TileBoundsBuffers = {|
  tileBoundsBuffer: VertexBuffer,
  tileBoundsIndexBuffer: IndexBuffer,
  tileBoundsSegments: SegmentVector,
|};

type GPUTimers = {[layerId: string]: any};

/**
 * Initialize a new painter object.
 *
 * @param {Canvas} gl an experimental-webgl drawing context
 * @private
 */
class Painter extends Evented {
    context: Context;
    transform: Transform;
    _tileTextures: {[_: number]: Array<Texture> };
    numSublayers: number;
    depthEpsilon: number;
    emptyProgramConfiguration: ProgramConfiguration;
    width: number;
    height: number;
    tileExtentBuffer: VertexBuffer;
    tileExtentSegments: SegmentVector;
    debugBuffer: VertexBuffer;
    debugIndexBuffer: IndexBuffer;
    debugSegments: SegmentVector;
    viewportBuffer: VertexBuffer;
    viewportSegments: SegmentVector;
    quadTriangleIndexBuffer: IndexBuffer;
    mercatorBoundsBuffer: VertexBuffer;
    mercatorBoundsSegments: SegmentVector;
    _tileClippingMaskIDs: Map<number, number>;
    _skippedStencilTileIDs: Set<number>;
    stencilClearMode: StencilMode;
    style: Style;
    options: PainterOptions;
    lineAtlas: LineAtlas;
    imageManager: ImageManager;
    glyphManager: GlyphManager;
    depthRangeFor3D: DepthRangeType;
    opaquePassCutoff: number;
    frameCounter: number;
    renderPass: RenderPass;
    currentLayer: number;
    currentStencilSource: ?string;
    nextStencilID: number;
    id: string;
    _showOverdrawInspector: boolean;
    cache: {[_: string]: Program<*> };
    crossTileSymbolIndex: CrossTileSymbolIndex;
    symbolFadeChange: number;
    gpuTimers: GPUTimers;
    deferredRenderGpuTimeQueries: Array<any>;
    emptyTexture: Texture;
    identityMat: Float32Array;
    debugOverlayTexture: Texture;
    debugOverlayCanvas: HTMLCanvasElement;
    _terrain: ?Terrain;
    globeSharedBuffers: ?GlobeSharedBuffers;
    tileLoaded: boolean;
    frameCopies: Array<WebGLTexture>;
    loadTimeStamps: Array<number>;
    _backgroundTiles: {[key: number]: Tile};

    constructor(gl: WebGLRenderingContext, transform: Transform) {
        super();

        this.context = new Context(gl);
        this.transform = transform;
        this._tileTextures = {};
        this.frameCopies = [];
        this.loadTimeStamps = [];

        this.setup();

        // Within each layer there are multiple distinct z-planes that can be drawn to.
        // This is implemented using the WebGL depth buffer.
        this.numSublayers = SourceCache.maxUnderzooming + SourceCache.maxOverzooming + 1;
        this.depthEpsilon = 1 / Math.pow(2, 16);

        this.crossTileSymbolIndex = new CrossTileSymbolIndex();

        this.deferredRenderGpuTimeQueries = [];
        this.gpuTimers = {};
        this.frameCounter = 0;
        this._backgroundTiles = {};
        this._tileClippingMaskIDs = new Map();
        this._skippedStencilTileIDs = new Set();
    }

    updateTerrain(style: Style, cameraChanging: boolean) {
        const enabled = !!style && !!style.terrain && this.transform.projection.supportsTerrain;
        if (!enabled && (!this._terrain || !this._terrain.enabled)) return;
        if (!this._terrain) {
            this._terrain = new Terrain(this, style);
        }
        const terrain: Terrain = this._terrain;
        this.transform.elevation = enabled ? terrain : null;
        terrain.update(style, this.transform, cameraChanging);
    }

    _updateFog(style: Style) {
        const fog = style.fog;
        if (!fog || fog.getOpacity(this.transform.pitch) < 1 || fog.properties.get('horizon-blend') < 0.03) {
            this.transform.fogCullDistSq = null;
            return;
        }

        // We start culling where the fog opacity function hits
        // 98% which leaves a non-noticeable change threshold.
        const [start, end] = fog.getFovAdjustedRange(this.transform._fov);

        if (start > end) {
            this.transform.fogCullDistSq = null;
            return;
        }

        const fogBoundFraction = 0.78;
        const fogCullDist = start + (end - start) * fogBoundFraction;

        this.transform.fogCullDistSq = fogCullDist * fogCullDist;
    }

    get terrain(): ?Terrain {
        return this.transform._terrainEnabled() && this._terrain && this._terrain.enabled ? this._terrain : null;
    }

    /*
     * Update the GL viewport, projection matrix, and transforms to compensate
     * for a new width and height value.
     */
    resize(width: number, height: number) {
        this.width = width * browser.devicePixelRatio;
        this.height = height * browser.devicePixelRatio;
        this.context.viewport.set([0, 0, this.width, this.height]);

        if (this.style) {
            for (const layerId of this.style.order) {
                this.style._layers[layerId].resize();
            }
        }
    }

    setup() {
        const context = this.context;

        const tileExtentArray = new PosArray();
        tileExtentArray.emplaceBack(0, 0);
        tileExtentArray.emplaceBack(EXTENT, 0);
        tileExtentArray.emplaceBack(0, EXTENT);
        tileExtentArray.emplaceBack(EXTENT, EXTENT);
        this.tileExtentBuffer = context.createVertexBuffer(tileExtentArray, posAttributes.members);
        this.tileExtentSegments = SegmentVector.simpleSegment(0, 0, 4, 2);

        const debugArray = new PosArray();
        debugArray.emplaceBack(0, 0);
        debugArray.emplaceBack(EXTENT, 0);
        debugArray.emplaceBack(0, EXTENT);
        debugArray.emplaceBack(EXTENT, EXTENT);
        this.debugBuffer = context.createVertexBuffer(debugArray, posAttributes.members);
        this.debugSegments = SegmentVector.simpleSegment(0, 0, 4, 5);

        const viewportArray = new PosArray();
        viewportArray.emplaceBack(-1, -1);
        viewportArray.emplaceBack(1, -1);
        viewportArray.emplaceBack(-1, 1);
        viewportArray.emplaceBack(1, 1);
        this.viewportBuffer = context.createVertexBuffer(viewportArray, posAttributes.members);
        this.viewportSegments = SegmentVector.simpleSegment(0, 0, 4, 2);

        const tileBoundsArray = new TileBoundsArray();
        tileBoundsArray.emplaceBack(0, 0, 0, 0);
        tileBoundsArray.emplaceBack(EXTENT, 0, EXTENT, 0);
        tileBoundsArray.emplaceBack(0, EXTENT, 0, EXTENT);
        tileBoundsArray.emplaceBack(EXTENT, EXTENT, EXTENT, EXTENT);
        this.mercatorBoundsBuffer = context.createVertexBuffer(tileBoundsArray, boundsAttributes.members);
        this.mercatorBoundsSegments = SegmentVector.simpleSegment(0, 0, 4, 2);

        const quadTriangleIndices = new TriangleIndexArray();
        quadTriangleIndices.emplaceBack(0, 1, 2);
        quadTriangleIndices.emplaceBack(2, 1, 3);
        this.quadTriangleIndexBuffer = context.createIndexBuffer(quadTriangleIndices);

        const tileLineStripIndices = new LineStripIndexArray();
        for (const i of [0, 1, 3, 2, 0]) tileLineStripIndices.emplaceBack(i);
        this.debugIndexBuffer = context.createIndexBuffer(tileLineStripIndices);

        this.emptyTexture = new Texture(context,
            new RGBAImage({width: 1, height: 1}, Uint8Array.of(0, 0, 0, 0)), context.gl.RGBA);

        this.identityMat = mat4.create();

        const gl = this.context.gl;
        this.stencilClearMode = new StencilMode({func: gl.ALWAYS, mask: 0}, 0x0, 0xFF, gl.ZERO, gl.ZERO, gl.ZERO);
        this.loadTimeStamps.push(window.performance.now());
    }

    getMercatorTileBoundsBuffers(): TileBoundsBuffers {
        return {
            tileBoundsBuffer: this.mercatorBoundsBuffer,
            tileBoundsIndexBuffer: this.quadTriangleIndexBuffer,
            tileBoundsSegments: this.mercatorBoundsSegments
        };
    }

    getTileBoundsBuffers(tile: Tile): TileBoundsBuffers {
        tile._makeTileBoundsBuffers(this.context, this.transform.projection);
        if (tile._tileBoundsBuffer) {
            const tileBoundsBuffer = tile._tileBoundsBuffer;
            const tileBoundsIndexBuffer = tile._tileBoundsIndexBuffer;
            const tileBoundsSegments = tile._tileBoundsSegments;
            return {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments};
        } else {
            return this.getMercatorTileBoundsBuffers();
        }
    }

    /*
     * Reset the drawing canvas by clearing the stencil buffer so that we can draw
     * new tiles at the same location, while retaining previously drawn pixels.
     */
    clearStencil() {
        const context = this.context;
        const gl = context.gl;

        this.nextStencilID = 1;
        this.currentStencilSource = undefined;
        this._tileClippingMaskIDs.clear();
        this._skippedStencilTileIDs.clear();

        // As a temporary workaround for https://github.com/mapbox/mapbox-gl-js/issues/5490,
        // pending an upstream fix, we draw a fullscreen stencil=0 clipping mask here,
        // effectively clearing the stencil buffer: once an upstream patch lands, remove
        // this function in favor of context.clear({ stencil: 0x0 })
        this.useProgram('clippingMask').draw(context, gl.TRIANGLES,
            DepthMode.disabled, this.stencilClearMode, ColorMode.disabled, CullFaceMode.disabled,
            clippingMaskUniformValues(this.identityMat),
            '$clipping', this.viewportBuffer, null,
            this.quadTriangleIndexBuffer, this.viewportSegments);
    }

    resetStencilClippingMasks() {
        if (!this.terrain) {
            this.currentStencilSource = undefined;
            this._tileClippingMaskIDs.clear();
            this._skippedStencilTileIDs.clear();
        }
    }

    _renderTileClippingMasks(layer: StyleLayer, sourceCache?: SourceCache, tileIDs?: Array<OverscaledTileID>) {
        if (!sourceCache || this.currentStencilSource === sourceCache.id || !layer.isTileClipped() || !tileIDs || tileIDs.length === 0) {
            return;
        }

        const renderableSkippedTileIDs = [];
        let dirtyStencilClippingMasks = false;
        if (this._tileClippingMaskIDs && !this.terrain) {
            // Equivalent tile set is already rendered in stencil
            for (const coord of tileIDs) {
                if (!this._tileClippingMaskIDs.has(coord.key)) {
                    dirtyStencilClippingMasks = true;
                }
                if (this._skippedStencilTileIDs.has(coord.key)) {
                    if (!sourceCache.getTile(coord).getBucket(layer)) {
                        continue;
                    }
                    this._skippedStencilTileIDs.delete(coord.key);
                    renderableSkippedTileIDs.push(coord);
                }
            }
            if (!dirtyStencilClippingMasks && renderableSkippedTileIDs.length === 0) {
                return;
            }
        }

        const context = this.context;
        const gl = context.gl;
        context.setColorMode(ColorMode.disabled);
        context.setDepthMode(DepthMode.disabled);
        const program = this.useProgram('clippingMask');

        const renderStencil = (tileID) => {
            const tile = sourceCache.getTile(tileID);
            const {tileBoundsBuffer, tileBoundsIndexBuffer, tileBoundsSegments} = this.getTileBoundsBuffers(tile);
            program.draw(context, gl.TRIANGLES, DepthMode.disabled,
                // Tests will pass if the new ref is greater than the previous value, and ref value will be written to stencil buffer.
                new StencilMode({func: gl.GREATER, mask: 0xFF}, this._tileClippingMaskIDs.get(tileID.key) || 0, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE),
                ColorMode.disabled, CullFaceMode.disabled, clippingMaskUniformValues(tileID.projMatrix),
                '$clipping', tileBoundsBuffer, null,
                tileBoundsIndexBuffer, tileBoundsSegments);
        };

        if (!dirtyStencilClippingMasks && renderableSkippedTileIDs.length > 0) {
            for (const tileID of renderableSkippedTileIDs) {
                renderStencil(tileID);
            }
        } else {
            if (this._tileClippingMaskIDs.size === 0 || this.nextStencilID + tileIDs.length > 256) {
                // we'll run out of fresh IDs so we need to clear and start from scratch
                this.clearStencil();
            }

            this._tileClippingMaskIDs.clear();
            this._skippedStencilTileIDs.clear();

            for (const tileID of tileIDs) {
                this._tileClippingMaskIDs.set(tileID.key, this.nextStencilID++);
                if (!sourceCache.getTile(tileID).getBucket(layer)) {
                    this._skippedStencilTileIDs.add(tileID.key);
                    continue;
                }
                renderStencil(tileID);
            }
        }

        if (this._skippedStencilTileIDs.size === 0) {
            this.currentStencilSource = sourceCache.id;
        }
    }

    stencilModeFor3D(): StencilMode {
        this.currentStencilSource = undefined;

        if (this.nextStencilID + 1 > 256) {
            this.clearStencil();
        }

        const id = this.nextStencilID++;
        const gl = this.context.gl;
        return new StencilMode({func: gl.NOTEQUAL, mask: 0xFF}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE);
    }

    stencilModeForClipping(tileID: OverscaledTileID): $ReadOnly<StencilMode>  {
        if (this.terrain) return this.terrain.stencilModeForRTTOverlap(tileID);
        const gl = this.context.gl;
        return new StencilMode({func: gl.EQUAL, mask: 0xFF}, this._tileClippingMaskIDs.get(tileID.key) || 0, 0x00, gl.KEEP, gl.KEEP, gl.REPLACE);
    }

    /*
     * Sort coordinates by Z as drawing tiles is done in Z-descending order.
     * All children with the same Z write the same stencil value.  Children
     * stencil values are greater than parent's.  This is used only for raster
     * and raster-dem tiles, which are already clipped to tile boundaries, to
     * mask area of tile overlapped by children tiles.
     * Stencil ref values continue range used in _tileClippingMaskIDs.
     *
     * Returns [StencilMode for tile overscaleZ map, sortedCoords].
     */
    stencilConfigForOverlap(tileIDs: Array<OverscaledTileID>): [{[_: number]: $ReadOnly<StencilMode>}, Array<OverscaledTileID>] {
        const gl = this.context.gl;
        const coords = tileIDs.sort((a, b) => b.overscaledZ - a.overscaledZ);
        const minTileZ = coords[coords.length - 1].overscaledZ;
        const stencilValues = coords[0].overscaledZ - minTileZ + 1;
        if (stencilValues > 1) {
            this.currentStencilSource = undefined;
            if (this.nextStencilID + stencilValues > 256) {
                this.clearStencil();
            }
            const zToStencilMode = {};
            for (let i = 0; i < stencilValues; i++) {
                zToStencilMode[i + minTileZ] = new StencilMode({func: gl.GEQUAL, mask: 0xFF}, i + this.nextStencilID, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE);
            }
            this.nextStencilID += stencilValues;
            return [zToStencilMode, coords];
        }
        return [{[minTileZ]: StencilMode.disabled}, coords];
    }

    colorModeForRenderPass(): $ReadOnly<ColorMode> {
        const gl = this.context.gl;
        if (this._showOverdrawInspector) {
            const numOverdrawSteps = 8;
            const a = 1 / numOverdrawSteps;

            return new ColorMode([gl.CONSTANT_COLOR, gl.ONE], new Color(a, a, a, 0), [true, true, true, true]);
        } else if (this.renderPass === 'opaque') {
            return ColorMode.unblended;
        } else {
            return ColorMode.alphaBlended;
        }
    }

    depthModeForSublayer(n: number, mask: DepthMaskType, func: ?DepthFuncType): $ReadOnly<DepthMode> {
        if (!this.opaquePassEnabledForLayer()) return DepthMode.disabled;
        const depth = 1 - ((1 + this.currentLayer) * this.numSublayers + n) * this.depthEpsilon;
        return new DepthMode(func || this.context.gl.LEQUAL, mask, [depth, depth]);
    }

    /*
     * The opaque pass and 3D layers both use the depth buffer.
     * Layers drawn above 3D layers need to be drawn using the
     * painter's algorithm so that they appear above 3D features.
     * This returns true for layers that can be drawn using the
     * opaque pass.
     */
    opaquePassEnabledForLayer(): boolean {
        return this.currentLayer < this.opaquePassCutoff;
    }

    render(style: Style, options: PainterOptions) {
        this.style = style;
        this.options = options;

        this.lineAtlas = style.lineAtlas;
        this.imageManager = style.imageManager;
        this.glyphManager = style.glyphManager;

        this.symbolFadeChange = style.placement.symbolFadeChange(browser.now());

        this.imageManager.beginFrame();

        const layerIds = this.style.order;
        const sourceCaches = this.style._sourceCaches;

        for (const id in sourceCaches) {
            const sourceCache = sourceCaches[id];
            if (sourceCache.used) {
                sourceCache.prepare(this.context);
            }
        }

        const coordsAscending: {[_: string]: Array<OverscaledTileID>} = {};
        const coordsDescending: {[_: string]: Array<OverscaledTileID>} = {};
        const coordsDescendingSymbol: {[_: string]: Array<OverscaledTileID>} = {};

        for (const id in sourceCaches) {
            const sourceCache = sourceCaches[id];
            coordsAscending[id] = sourceCache.getVisibleCoordinates();
            coordsDescending[id] = coordsAscending[id].slice().reverse();
            coordsDescendingSymbol[id] = sourceCache.getVisibleCoordinates(true).reverse();
        }

        this.opaquePassCutoff = Infinity;
        for (let i = 0; i < layerIds.length; i++) {
            const layerId = layerIds[i];
            if (this.style._layers[layerId].is3D()) {
                this.opaquePassCutoff = i;
                break;
            }
        }

        if (this.terrain) {
            this.terrain.updateTileBinding(coordsDescendingSymbol);
            // All render to texture is done in translucent pass to remove need
            // for depth buffer allocation per tile.
            this.opaquePassCutoff = 0;
        }

        if (this.transform.projection.name === 'globe' && !this.globeSharedBuffers) {
            this.globeSharedBuffers = new GlobeSharedBuffers(this.context);
        }

        // Following line is billing related code. Do not change. See LICENSE.txt
        if (!isMapAuthenticated(this.context.gl)) return;

        // Offscreen pass ===============================================
        // We first do all rendering that requires rendering to a separate
        // framebuffer, and then save those for rendering back to the map
        // later: in doing this we avoid doing expensive framebuffer restores.
        this.renderPass = 'offscreen';

        for (const layerId of layerIds) {
            const layer = this.style._layers[layerId];
            const sourceCache = style._getLayerSourceCache(layer);
            if (!layer.hasOffscreenPass() || layer.isHidden(this.transform.zoom)) continue;

            const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined;
            if (!(layer.type === 'custom' || layer.isSky()) && !(coords && coords.length)) continue;

            this.renderLayer(this, sourceCache, layer, coords);
        }

        this.depthRangeFor3D = [0, 1 - ((style.order.length + 2) * this.numSublayers * this.depthEpsilon)];

        // Terrain depth offscreen render pass ==========================
        // With terrain on, renders the depth buffer into a texture.
        // This texture is used for occlusion testing (labels)
        if (this.terrain && (this.style.hasSymbolLayers() || this.style.hasCircleLayers())) {
            this.terrain.drawDepth();
        }

        // Rebind the main framebuffer now that all offscreen layers have been rendered:
        this.context.bindFramebuffer.set(null);
        this.context.viewport.set([0, 0, this.width, this.height]);

        // Clear buffers in preparation for drawing to the main framebuffer
        // If fog is enabled, use the fog color as default clear color.
        let clearColor = Color.transparent;
        if (this.style.fog && this.style.fog.getOpacity(this.transform.pitch)) {
            clearColor = this.style.fog.properties.get('color');
        }
        this.context.clear({color: options.showOverdrawInspector ? Color.black : clearColor, depth: 1});
        this.clearStencil();

        this._showOverdrawInspector = options.showOverdrawInspector;

        // Opaque pass ===============================================
        // Draw opaque layers top-to-bottom first.
        this.renderPass = 'opaque';

        if (!this.terrain) {
            for (this.currentLayer = layerIds.length - 1; this.currentLayer >= 0; this.currentLayer--) {
                const layer = this.style._layers[layerIds[this.currentLayer]];
                const sourceCache = style._getLayerSourceCache(layer);
                if (layer.isSky()) continue;
                const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined;
                this._renderTileClippingMasks(layer, sourceCache, coords);
                this.renderLayer(this, sourceCache, layer, coords);
            }
        }

        // Sky pass ======================================================
        // Draw all sky layers bottom to top.
        // They are drawn at max depth, they are drawn after opaque and before
        // translucent to fail depth testing and mix with translucent objects.
        this.renderPass = 'sky';
        const isTransitioning = globeToMercatorTransition(this.transform.zoom) > 0.0;
        if ((isTransitioning || this.transform.projection.name !== 'globe') && this.transform.isHorizonVisible()) {
            for (this.currentLayer = 0; this.currentLayer < layerIds.length; this.currentLayer++) {
                const layer = this.style._layers[layerIds[this.currentLayer]];
                const sourceCache = style._getLayerSourceCache(layer);
                if (!layer.isSky()) continue;
                const coords = sourceCache ? coordsDescending[sourceCache.id] : undefined;

                this.renderLayer(this, sourceCache, layer, coords);
            }
        }
        if (this.transform.projection.name === 'globe') {
            drawGlobeAtmosphere(this);
        }

        // Translucent pass ===============================================
        // Draw all other layers bottom-to-top.
        this.renderPass = 'translucent';

        this.currentLayer = 0;
        while (this.currentLayer < layerIds.length) {
            const layer = this.style._layers[layerIds[this.currentLayer]];
            const sourceCache = style._getLayerSourceCache(layer);

            // Nothing to draw in translucent pass for sky layers, advance
            if (layer.isSky()) {
                ++this.currentLayer;
                continue;
            }

            // With terrain on and for draped layers only, issue rendering and progress
            // this.currentLayer until the next non-draped layer.
            // Otherwise we interleave terrain draped render with non-draped layers on top
            if (this.terrain && this.style.isLayerDraped(layer)) {
                if (layer.isHidden(this.transform.zoom)) {
                    ++this.currentLayer;
                    continue;
                }
                const terrain = (((this.terrain): any): Terrain);
                const prevLayer = this.currentLayer;
                this.currentLayer = terrain.renderBatch(this.currentLayer);
                assert(this.context.bindFramebuffer.current === null);
                assert(this.currentLayer > prevLayer);
                continue;
            }
            // For symbol layers in the translucent pass, we add extra tiles to the renderable set
            // for cross-tile symbol fading. Symbol layers don't use tile clipping, so no need to render
            // separate clipping masks
            const coords = sourceCache ?
                (layer.type === 'symbol' ? coordsDescendingSymbol : coordsDescending)[sourceCache.id] :
                undefined;

            this._renderTileClippingMasks(layer, sourceCache, sourceCache ? coordsAscending[sourceCache.id] : undefined);
            this.renderLayer(this, sourceCache, layer, coords);
            this.fire(new Event('layerRendered', layer));

            ++this.currentLayer;
        }

        if (this.terrain) {
            this.terrain.postRender();
        }

        if (this.options.showTileBoundaries || this.options.showQueryGeometry) {
            //Use source with highest maxzoom
            let selectedSource = null;
            const layers = values(this.style._layers);
            layers.forEach((layer) => {
                const sourceCache = style._getLayerSourceCache(layer);
                if (sourceCache && !layer.isHidden(this.transform.zoom)) {
                    if (!selectedSource || (selectedSource.getSource().maxzoom < sourceCache.getSource().maxzoom)) {
                        selectedSource = sourceCache;
                    }
                }
            });
            if (selectedSource) {
                if (this.options.showTileBoundaries) {
                    draw.debug(this, selectedSource, selectedSource.getVisibleCoordinates());
                }

                Debug.run(() => {
                    if (this.options.showQueryGeometry && selectedSource) {
                        drawDebugQueryGeometry(this, selectedSource, selectedSource.getVisibleCoordinates());
                    }
                });
            }
        }

        if (this.options.showPadding) {
            drawDebugPadding(this);
        }

        // Set defaults for most GL values so that anyone using the state after the render
        // encounters more expected values.
        this.context.setDefault();
        this.frameCounter = (this.frameCounter + 1) % Number.MAX_SAFE_INTEGER;

        if (this.tileLoaded && this.options.speedIndexTiming) {
            this.loadTimeStamps.push(window.performance.now());
            this.saveCanvasCopy();
        }
    }

    renderLayer(painter: Painter, sourceCache?: SourceCache, layer: StyleLayer, coords?: Array<OverscaledTileID>) {
        if (layer.isHidden(this.transform.zoom)) return;
        if (layer.type !== 'background' && layer.type !== 'sky' && layer.type !== 'custom' && !(coords && coords.length)) return;
        this.id = layer.id;
        this.gpuTimingStart(layer);
        if (!painter.transform.projection.unsupportedLayers || !painter.transform.projection.unsupportedLayers.includes(layer.type)) {
            draw[layer.type](painter, sourceCache, layer, coords, this.style.placement.variableOffsets, this.options.isInitialLoad);
        }
        this.gpuTimingEnd();
    }

    gpuTimingStart(layer: StyleLayer) {
        if (!this.options.gpuTiming) return;
        const ext = this.context.extTimerQuery;
        // This tries to time the draw call itself, but note that the cost for drawing a layer
        // may be dominated by the cost of uploading vertices to the GPU.
        // To instrument that, we'd need to pass the layerTimers object down into the bucket
        // uploading logic.
        let layerTimer = this.gpuTimers[layer.id];
        if (!layerTimer) {
            layerTimer = this.gpuTimers[layer.id] = {
                calls: 0,
                cpuTime: 0,
                query: ext.createQueryEXT()
            };
        }
        layerTimer.calls++;
        ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, layerTimer.query);
    }

    gpuTimingDeferredRenderStart() {
        if (this.options.gpuTimingDeferredRender) {
            const ext = this.context.extTimerQuery;
            const query = ext.createQueryEXT();
            this.deferredRenderGpuTimeQueries.push(query);
            ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);
        }
    }

    gpuTimingDeferredRenderEnd() {
        if (!this.options.gpuTimingDeferredRender) return;
        const ext = this.context.extTimerQuery;
        ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
    }

    gpuTimingEnd() {
        if (!this.options.gpuTiming) return;
        const ext = this.context.extTimerQuery;
        ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
    }

    collectGpuTimers(): GPUTimers {
        const currentLayerTimers = this.gpuTimers;
        this.gpuTimers = {};
        return currentLayerTimers;
    }

    collectDeferredRenderGpuQueries(): Array<any> {
        const currentQueries = this.deferredRenderGpuTimeQueries;
        this.deferredRenderGpuTimeQueries = [];
        return currentQueries;
    }

    queryGpuTimers(gpuTimers: GPUTimers): {[layerId: string]: number} {
        const layers = {};
        for (const layerId in gpuTimers) {
            const gpuTimer = gpuTimers[layerId];
            const ext = this.context.extTimerQuery;
            const gpuTime = ext.getQueryObjectEXT(gpuTimer.query, ext.QUERY_RESULT_EXT) / (1000 * 1000);
            ext.deleteQueryEXT(gpuTimer.query);
            layers[layerId] = (gpuTime: number);
        }
        return layers;
    }

    queryGpuTimeDeferredRender(gpuQueries: Array<any>): number {
        if (!this.options.gpuTimingDeferredRender) return 0;
        const ext = this.context.extTimerQuery;

        let gpuTime = 0;
        for (const query of gpuQueries) {
            gpuTime += ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT) / (1000 * 1000);
            ext.deleteQueryEXT(query);
        }

        return gpuTime;
    }

    /**
     * Transform a matrix to incorporate the *-translate and *-translate-anchor properties into it.
     * @param inViewportPixelUnitsUnits True when the units accepted by the matrix are in viewport pixels instead of tile units.
     * @returns {Float32Array} matrix
     * @private
     */
    translatePosMatrix(matrix: Float32Array, tile: Tile, translate: [number, number], translateAnchor: 'map' | 'viewport', inViewportPixelUnitsUnits?: boolean): Float32Array {
        if (!translate[0] && !translate[1]) return matrix;

        const angle = inViewportPixelUnitsUnits ?
            (translateAnchor === 'map' ? this.transform.angle : 0) :
            (translateAnchor === 'viewport' ? -this.transform.angle : 0);

        if (angle) {
            const sinA = Math.sin(angle);
            const cosA = Math.cos(angle);
            translate = [
                translate[0] * cosA - translate[1] * sinA,
                translate[0] * sinA + translate[1] * cosA
            ];
        }

        const translation = [
            inViewportPixelUnitsUnits ? translate[0] : pixelsToTileUnits(tile, translate[0], this.transform.zoom),
            inViewportPixelUnitsUnits ? translate[1] : pixelsToTileUnits(tile, translate[1], this.transform.zoom),
            0
        ];

        const translatedMatrix = new Float32Array(16);
        mat4.translate(translatedMatrix, matrix, translation);
        return translatedMatrix;
    }

    saveTileTexture(texture: Texture) {
        const textures = this._tileTextures[texture.size[0]];
        if (!textures) {
            this._tileTextures[texture.size[0]] = [texture];
        } else {
            textures.push(texture);
        }
    }

    getTileTexture(size: number): null | Texture {
        const textures = this._tileTextures[size];
        return textures && textures.length > 0 ? textures.pop() : null;
    }

    /**
     * Checks whether a pattern image is needed, and if it is, whether it is not loaded.
     *
* @returns true if a needed image is missing and rendering needs to be skipped.
     * @private
     */
    isPatternMissing(image: ?CrossFaded<ResolvedImage>): boolean {
        if (!image) return false;
        if (!image.from || !image.to) return true;
        const imagePosA = this.imageManager.getPattern(image.from.toString());
        const imagePosB = this.imageManager.getPattern(image.to.toString());
        return !imagePosA || !imagePosB;
    }

    /**
     * Returns #defines that would need to be injected into every Program
     * based on the current state of Painter.
     *
     * @returns {string[]}
     * @private
     */
    currentGlobalDefines(): string[] {
        const terrain = this.terrain && !this.terrain.renderingToTexture; // Enables elevation sampling in vertex shader.
        const rtt = this.terrain && this.terrain.renderingToTexture;
        const fog = this.style && this.style.fog;
        const defines = [];

        if (terrain) defines.push('TERRAIN');
        // When terrain is active, fog is rendered as part of draping, not as part of tile
        // rendering. Removing the fog flag during tile rendering avoids additional defines.
        if (fog && !rtt && fog.getOpacity(this.transform.pitch) !== 0.0) {
            defines.push('FOG');
        }
        if (rtt) defines.push('RENDER_TO_TEXTURE');
        if (this._showOverdrawInspector) defines.push('OVERDRAW_INSPECTOR');
        return defines;
    }

    useProgram(name: string, programConfiguration: ?ProgramConfiguration, fixedDefines: ?DynamicDefinesType[]): Program<any> {
        this.cache = this.cache || {};
        const defines = (((fixedDefines || []): any): string[]);

        const globalDefines = this.currentGlobalDefines();
        const allDefines = globalDefines.concat(defines);
        const key = Program.cacheKey(name, allDefines, programConfiguration);

        if (!this.cache[key]) {
            this.cache[key] = new Program(this.context, name, shaders[name], programConfiguration, programUniforms[name], allDefines);
        }
        return this.cache[key];
    }

    /*
     * Reset some GL state to default values to avoid hard-to-debug bugs
     * in custom layers.
     */
    setCustomLayerDefaults() {
        // Prevent custom layers from unintentionally modify the last VAO used.
        // All other state is state is restored on it's own, but for VAOs it's
        // simpler to unbind so that we don't have to track the state of VAOs.
        this.context.unbindVAO();

        // The default values for this state is meaningful and often expected.
        // Leaving this state dirty could cause a lot of confusion for users.
        this.context.cullFace.setDefault();
        this.context.frontFace.setDefault();
        this.context.cullFaceSide.setDefault();
        this.context.activeTexture.setDefault();
        this.context.pixelStoreUnpack.setDefault();
        this.context.pixelStoreUnpackPremultiplyAlpha.setDefault();
        this.context.pixelStoreUnpackFlipY.setDefault();
    }

    /*
     * Set GL state that is shared by all layers.
     */
    setBaseState() {
        const gl = this.context.gl;
        this.context.cullFace.set(false);
        this.context.viewport.set([0, 0, this.width, this.height]);
        this.context.blendEquation.set(gl.FUNC_ADD);
    }

    initDebugOverlayCanvas() {
        if (this.debugOverlayCanvas == null) {
            this.debugOverlayCanvas = window.document.createElement('canvas');
            this.debugOverlayCanvas.width = 512;
            this.debugOverlayCanvas.height = 512;
            const gl = this.context.gl;
            this.debugOverlayTexture = new Texture(this.context, this.debugOverlayCanvas, gl.RGBA);
        }
    }

    destroy() {
        if (this._terrain) {
            this._terrain.destroy();
        }
        if (this.globeSharedBuffers) {
            this.globeSharedBuffers.destroy();
        }
        this.emptyTexture.destroy();
        if (this.debugOverlayTexture) {
            this.debugOverlayTexture.destroy();
        }
    }

    prepareDrawTile() {
        if (this.terrain) {
            this.terrain.prepareDrawTile();
        }
    }

    prepareDrawProgram(context: Context, program: Program<*>, tileID: ?UnwrappedTileID) {

        // Fog is not enabled when rendering to texture so we
        // can safely skip uploading uniforms in that case
        if (this.terrain && this.terrain.renderingToTexture) {
            return;
        }

        const fog = this.style.fog;

        if (fog) {
            const fogOpacity = fog.getOpacity(this.transform.pitch);
            if (fogOpacity !== 0.0) {
                program.setFogUniformValues(context, fogUniformValues(this, fog, tileID, fogOpacity));
            }
        }
    }

    setTileLoadedFlag(flag: boolean) {
        this.tileLoaded = flag;
    }

    saveCanvasCopy() {
        this.frameCopies.push(this.canvasCopy());
        this.tileLoaded = false;
    }

    canvasCopy(): ?WebGLTexture {
        const gl = this.context.gl;
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.copyTexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, 0);
        return texture;
    }

    getCanvasCopiesAndTimestamps(): CanvasCopyInstances {
        return {
            canvasCopies: this.frameCopies,
            timeStamps: this.loadTimeStamps
        };
    }

    averageElevationNeedsEasing(): boolean {
        if (!this.transform._elevation) return false;

        const fog = this.style && this.style.fog;
        if (!fog) return false;

        const fogOpacity = fog.getOpacity(this.transform.pitch);
        if (fogOpacity === 0) return false;

        return true;
    }

    getBackgroundTiles(): {[key: number]: Tile} {
        const oldTiles = this._backgroundTiles;
        const newTiles = this._backgroundTiles = {};

        const tileSize = 512;
        const tileIDs = this.transform.coveringTiles({tileSize});
        for (const tileID of tileIDs) {
            newTiles[tileID.key] = oldTiles[tileID.key] || new Tile(tileID, tileSize, this.transform.tileZoom, this);
        }
        return newTiles;
    }

    clearBackgroundTiles() {
        this._backgroundTiles = {};
    }
}

export default Painter;
