// @flow

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 EXTENT from '../data/extent.js';
import FillExtrusionBucket from '../data/bucket/fill_extrusion_bucket.js';
import {
    fillExtrusionUniformValues,
    fillExtrusionPatternUniformValues,
    fillExtrusionMapUniformValues
} from './program/fill_extrusion_program.js';
import Point from '@mapbox/point-geometry';
import {OverscaledTileID} from '../source/tile_id.js';
import assert from 'assert';
import {mercatorXfromLng, mercatorYfromLat} from '../geo/mercator_coordinate.js';
import {globeToMercatorTransition} from '../geo/projection/globe_util.js';
import type Transform from '../geo/transform.js';
import {earthRadius} from '../geo/lng_lat.js';

import type Painter from './painter.js';
import SourceCache from '../source/source_cache.js';
import type FillExtrusionStyleLayer from '../style/style_layer/fill_extrusion_style_layer.js';

export default draw;

function draw(painter: Painter, source: SourceCache, layer: FillExtrusionStyleLayer, coords: Array<OverscaledTileID>) {
    const opacity = layer.paint.get('fill-extrusion-opacity');
    if (opacity === 0) {
        return;
    }
    if (painter.renderPass === 'translucent') {
        const depthMode = new DepthMode(painter.context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D);

        if (opacity === 1 && !layer.paint.get('fill-extrusion-pattern').constantOr((1: any))) {
            // 当透明度为1且没有fill-extrusion-pattern时的操作
            const colorMode = painter.colorModeForRenderPass();
            drawExtrusionTiles(painter, source, layer, coords, depthMode, StencilMode.disabled, colorMode);
        } else {
            // 当透明度不为1或者有fill-extrusion-pattern时的操作
            
            // Draw transparent buildings in two passes so that only the closest surface is drawn.
            // First draw all the extrusions into only the depth buffer. No colors are drawn.
            drawExtrusionTiles(painter, source, layer, coords, depthMode,
                StencilMode.disabled,
                ColorMode.disabled);

            // Then draw all the extrusions a second type, only coloring fragments if they have the
            // same depth value as the closest fragment in the previous pass. Use the stencil buffer
            // to prevent the second draw in cases where we have coincident polygons.
            drawExtrusionTiles(painter, source, layer, coords, depthMode,
                painter.stencilModeFor3D(),
                painter.colorModeForRenderPass());

            painter.resetStencilClippingMasks();
        }
    }
}

function fillExtrusionHeightLift(transform: Transform): number {
    if (transform.projection.name !== 'globe') {
        return 0;
    }
    // A rectangle covering globe is subdivided into a grid of 32 cells
    // This information can be used to deduce a minimum lift value so that
    // fill extrusions with 0 height will never go below the ground.
    const angle = Math.PI / 32.0;
    const tanAngle = Math.tan(angle);
    const r = earthRadius;
    return r * Math.sqrt(1.0 + 2.0 * tanAngle * tanAngle) - r;
}

function drawExtrusionTiles(painter, source, layer, coords, depthMode, stencilMode, colorMode) {
    const context = painter.context;
    const gl = context.gl;
    const tr = painter.transform;
    const patternProperty = layer.paint.get('fill-extrusion-pattern');
    const mapProperty = layer.paint.get('fill-extrusion-map');
    const image = patternProperty.constantOr((1: any));
    const map = mapProperty.constantOr((1: any));
    const crossfade = layer.getCrossfadeParameters();
    const opacity = layer.paint.get('fill-extrusion-opacity');
    const roofcolor = layer.paint.get('fill-extrusion-roofcolor');
    const heightLift = fillExtrusionHeightLift(tr);
    const isGlobeProjection = tr.projection.name === 'globe';
    const globeToMercator = isGlobeProjection ? globeToMercatorTransition(tr.zoom) : 0.0;
    const mercatorCenter = [mercatorXfromLng(tr.center.lng), mercatorYfromLat(tr.center.lat)];
    const baseDefines = ([]: any);
    if (isGlobeProjection) {
        baseDefines.push('PROJECTION_GLOBE_VIEW');
    }
    
    for (const coord of coords) {
        if (coord.overscaledZ !== SourceCache.buildingZoom) continue
        const tile = source.getTile(coord);
        const bucket: ?FillExtrusionBucket = (tile.getBucket(layer): any);
        if (!bucket || bucket.projection !== tr.projection.name) continue;

        const programConfiguration = bucket.programConfigurations.get(layer.id);
        let shaderName = 'fillExtrusion';
        if (image) {
            shaderName = 'fillExtrusionPattern';
        } else if(map) {
            shaderName = 'fillExtrusionMap';
        }
        const program = painter.useProgram(shaderName, programConfiguration, baseDefines);

        if (painter.terrain) {
            const terrain = painter.terrain;
            if (painter.style.terrainSetForDrapingOnly()) {
                terrain.setupElevationDraw(tile, program, {useMeterToDem: true});
            } else {
                if (!bucket.enableTerrain) continue;
                terrain.setupElevationDraw(tile, program, {useMeterToDem: true});
                flatRoofsUpdate(context, source, coord, bucket, layer, terrain);
                if (!bucket.centroidVertexBuffer) {
                    const attrIndex: number | void = program.attributes['a_centroid_pos'];
                    if (attrIndex !== undefined) gl.vertexAttrib2f(attrIndex, 0, 0);
                }
            }
        }
        if (image) {
            painter.context.activeTexture.set(gl.TEXTURE0);
            tile.imageAtlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
            programConfiguration.updatePaintBuffers(crossfade);
        }
        if (map) {
            tile.imageAtlasTextures.forEach((texture, index) => {
                painter.context.activeTexture.set(gl[`TEXTURE${index}`]);
                texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
                programConfiguration.updatePaintBuffers(crossfade);
            })
        }
        const constantPattern = patternProperty.constantOr(null);
        const constantMap = mapProperty.constantOr(null);
        if (constantPattern && tile.imageAtlas) {
            const atlas = tile.imageAtlas;
            const posTo = atlas.patternPositions[constantPattern.to.toString()];
            const posFrom = atlas.patternPositions[constantPattern.from.toString()];
            if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom);
        }
        if (constantMap && tile.imageAtlas) {
            const atlas = tile.imageAtlas;
            const posTo = atlas.mapPositions[constantMap.to.toString()];
            const posFrom = atlas.mapPositions[constantMap.from.toString()];
            if (posTo && posFrom) programConfiguration.setConstantPatternPositions(posTo, posFrom);
        }

        const matrix = painter.translatePosMatrix(
            coord.projMatrix,
            tile,
            layer.paint.get('fill-extrusion-translate'),
            layer.paint.get('fill-extrusion-translate-anchor'));

        const invMatrix = tr.projection.createInversionMatrix(tr, coord.canonical);

        const shouldUseVerticalGradient = layer.paint.get('fill-extrusion-vertical-gradient');
        let uniformValues
        if (image) {
            uniformValues = fillExtrusionPatternUniformValues(matrix, painter, shouldUseVerticalGradient, { opacity, roofcolor}, coord,
                crossfade, tile, heightLift, globeToMercator, mercatorCenter, invMatrix);
        } else if (map) {
            uniformValues = fillExtrusionMapUniformValues(matrix, painter, shouldUseVerticalGradient, { opacity, roofcolor}, coord,
                crossfade, tile, heightLift, globeToMercator, mercatorCenter, invMatrix);
        } else {
            uniformValues = fillExtrusionUniformValues(matrix, painter, shouldUseVerticalGradient, { opacity, roofcolor}, coord,
                heightLift, globeToMercator, mercatorCenter, invMatrix)
        }
        // const uniformValues = image ?
        //     fillExtrusionPatternUniformValues(matrix, painter, shouldUseVerticalGradient, opacity, coord,
        //         crossfade, tile, heightLift, globeToMercator, mercatorCenter, invMatrix) :
        //     fillExtrusionUniformValues(matrix, painter, shouldUseVerticalGradient, opacity, coord,
        //         heightLift, globeToMercator, mercatorCenter, invMatrix);

        painter.prepareDrawProgram(context, program, coord.toUnwrapped());

        assert(!isGlobeProjection || bucket.layoutVertexExtBuffer);
        program.draw(context, context.gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW,
            uniformValues, layer.id, bucket.layoutVertexBuffer, map ? bucket.uvBuffer : null, bucket.indexBuffer,
            bucket.segments, layer.paint, painter.transform.zoom,
            programConfiguration,
            painter.terrain ? bucket.centroidVertexBuffer : null,
            isGlobeProjection ? bucket.layoutVertexExtBuffer : null);
    }
}

// Flat roofs array is prepared in the bucket, except for buildings that are on tile borders.
// For them, join pieces, calculate joined size here, and then upload data.
function flatRoofsUpdate(context, source, coord, bucket, layer, terrain) {
    // For all four borders: 0 - left, 1, right, 2 - top, 3 - bottom
    const neighborCoord = [
        coord => {
            let x = coord.canonical.x - 1;
            let w = coord.wrap;
            if (x < 0) {
                x = (1 << coord.canonical.z) - 1;
                w--;
            }
            return new OverscaledTileID(coord.overscaledZ, w, coord.canonical.z, x, coord.canonical.y);
        },
        coord => {
            let x = coord.canonical.x + 1;
            let w = coord.wrap;
            if (x === 1 << coord.canonical.z) {
                x = 0;
                w++;
            }
            return new OverscaledTileID(coord.overscaledZ, w, coord.canonical.z, x, coord.canonical.y);
        },
        coord => new OverscaledTileID(coord.overscaledZ, coord.wrap, coord.canonical.z, coord.canonical.x,
            (coord.canonical.y === 0 ? 1 << coord.canonical.z : coord.canonical.y) - 1),
        coord => new OverscaledTileID(coord.overscaledZ, coord.wrap, coord.canonical.z, coord.canonical.x,
            coord.canonical.y === (1 << coord.canonical.z) - 1 ? 0 : coord.canonical.y + 1)
    ];

    const getLoadedBucket = (nid) => {
        const minzoom = source.getSource().minzoom;
        const getBucket = (key) => {
            const n = source.getTileByID(key);
            if (n && n.hasData()) {
                return n.getBucket(layer);
            }
        };
        // Look one tile zoom above and under. We do this to avoid flickering and
        // use the content in Z-1 and Z+1 buckets until Z bucket is loaded or handle
        // behavior on borders between different zooms.
        const zoomLevels = [0, -1, 1];
        for (const i of zoomLevels) {
            const z = nid.overscaledZ + i;
            if (z < minzoom) continue;
            const key = nid.calculateScaledKey(nid.overscaledZ + i);
            const b = getBucket(key);
            if (b) {
                return b;
            }
        }
    };

    const projectedToBorder = [0, 0, 0]; // [min, max, maxOffsetFromBorder]
    const xjoin = (a, b) => {
        projectedToBorder[0] = Math.min(a.min.y, b.min.y);
        projectedToBorder[1] = Math.max(a.max.y, b.max.y);
        projectedToBorder[2] = EXTENT - b.min.x > a.max.x ? b.min.x - EXTENT : a.max.x;
        return projectedToBorder;
    };
    const yjoin = (a, b) => {
        projectedToBorder[0] = Math.min(a.min.x, b.min.x);
        projectedToBorder[1] = Math.max(a.max.x, b.max.x);
        projectedToBorder[2] = EXTENT - b.min.y > a.max.y ? b.min.y - EXTENT : a.max.y;
        return projectedToBorder;
    };
    const projectCombinedSpanToBorder = [
        (a, b) => xjoin(a, b),
        (a, b) => xjoin(b, a),
        (a, b) => yjoin(a, b),
        (a, b) => yjoin(b, a)
    ];

    const centroid = new Point(0, 0);
    const error = 3; // Allow intrusion of a building to the building with adjacent wall.

    let demTile, neighborDEMTile, neighborTileID;

    const flatBase = (min, max, edge, verticalEdge, maxOffsetFromBorder) => {
        const points = [[verticalEdge ? edge : min, verticalEdge ? min : edge, 0], [verticalEdge ? edge : max, verticalEdge ? max : edge, 0]];

        const coord3 = maxOffsetFromBorder < 0 ? EXTENT + maxOffsetFromBorder : maxOffsetFromBorder;
        const thirdPoint = [verticalEdge ? coord3 : (min + max) / 2, verticalEdge ? (min + max) / 2 : coord3, 0];
        if ((edge === 0 && maxOffsetFromBorder < 0) || (edge !== 0 && maxOffsetFromBorder > 0)) {
            // Third point is inside neighbor tile, not in the |coord| tile.
            terrain.getForTilePoints(neighborTileID, [thirdPoint], true, neighborDEMTile);
        } else {
            points.push(thirdPoint);
        }
        terrain.getForTilePoints(coord, points, true, demTile);
        return Math.max(points[0][2], points[1][2], thirdPoint[2]) / terrain.exaggeration();
    };

    // Process all four borders: get neighboring tile
    for (let i = 0; i < 4; i++) {
        // borders / borderDoneWithNeighborZ: 0 - left, 1, right, 2 - top, 3 - bottom
        // bucket's border i is neighboring bucket's border j:
        const j = (i < 2 ? 1 : 5) - i;
        // Sort by border intersection area minimums, ascending.
        const a = bucket.borders[i];
        if (a.length === 0) continue;
        const nid = neighborTileID = neighborCoord[i](coord);
        const nBucket = getLoadedBucket(nid);
        if (!nBucket || !(nBucket instanceof FillExtrusionBucket) || !nBucket.enableTerrain) continue;
        if (bucket.borderDoneWithNeighborZ[i] === nBucket.canonical.z &&
            nBucket.borderDoneWithNeighborZ[j] === bucket.canonical.z) {
            continue;
        }

        neighborDEMTile = terrain.findDEMTileFor(nid);
        if (!neighborDEMTile || !neighborDEMTile.dem) continue;
        if (!demTile) {
            const dem = terrain.findDEMTileFor(coord);
            if (!(dem && dem.dem)) return; // defer update until an elevation tile is available.
            demTile = dem;
        }
        const b = nBucket.borders[j];
        let ib = 0;

        const updateNeighbor = nBucket.borderDoneWithNeighborZ[j] !== bucket.canonical.z;
        // If neighbors are of different canonical z, we cannot join parts but show
        // all without flat roofs.
        if (bucket.canonical.z !== nBucket.canonical.z) {
            for (const index of a) {
                bucket.encodeCentroid(undefined, bucket.featuresOnBorder[index], false);
            }
            if (updateNeighbor) {
                for (const index of b) {
                    nBucket.encodeCentroid(undefined, nBucket.featuresOnBorder[index], false);
                }
                nBucket.borderDoneWithNeighborZ[j] = bucket.canonical.z;
                nBucket.needsCentroidUpdate = true;
            }
            bucket.borderDoneWithNeighborZ[i] = nBucket.canonical.z;
            bucket.needsCentroidUpdate = true;
            continue;
        }

        for (let ia = 0; ia < a.length; ia++) {
            const parta = bucket.featuresOnBorder[a[ia]];
            const partABorderRange = parta.borders[i];
            // Find all nBucket parts that share the border overlap.
            let partb;
            while (ib < b.length) {
                // Pass all that are before the overlap.
                partb = nBucket.featuresOnBorder[b[ib]];
                const partBBorderRange = partb.borders[j];
                if (partBBorderRange[1] > partABorderRange[0] + error) break;
                if (updateNeighbor) nBucket.encodeCentroid(undefined, partb, false);
                ib++;
            }
            if (partb && ib < b.length) {
                const saveIb = ib;
                let count = 0;
                while (true) {
                    // Collect all parts overlapping parta on the edge, to make sure it is only one.
                    const partBBorderRange = partb.borders[j];
                    if (partBBorderRange[0] > partABorderRange[1] - error) break;
                    count++;
                    if (++ib === b.length) break;
                    partb = nBucket.featuresOnBorder[b[ib]];
                }
                partb = nBucket.featuresOnBorder[b[saveIb]];

                // If any of a or b crosses more than one tile edge, don't support flat roof.
                if (parta.intersectsCount() > 1 || partb.intersectsCount() > 1 || count !== 1) {
                    if (count !== 1) {
                        ib = saveIb; // rewind unprocessed ib so that it is processed again for the next ia.
                    }

                    bucket.encodeCentroid(undefined, parta, false);
                    if (updateNeighbor) nBucket.encodeCentroid(undefined, partb, false);
                    continue;
                }

                // Now we have 1-1 matching of parts in both tiles that share the edge. Calculate flat base elevation
                // as average of three points: 2 are edge points (combined span projected to border) and one is point of
                // span that has maximum offset to border.
                const span = projectCombinedSpanToBorder[i](parta, partb);
                const edge = (i % 2) ? EXTENT - 1 : 0;
                centroid.x = flatBase(span[0], Math.min(EXTENT - 1, span[1]), edge, i < 2, span[2]);
                centroid.y = 0;
                assert(parta.vertexArrayOffset !== undefined && parta.vertexArrayOffset < bucket.layoutVertexArray.length);
                bucket.encodeCentroid(centroid, parta, false);

                assert(partb.vertexArrayOffset !== undefined && partb.vertexArrayOffset < nBucket.layoutVertexArray.length);
                if (updateNeighbor) nBucket.encodeCentroid(centroid, partb, false);
            } else {
                assert(parta.intersectsCount() > 1 || (partb && partb.intersectsCount() > 1)); // expected at the end of border, when buildings cover corner (show building w/o flat roof).
                bucket.encodeCentroid(undefined, parta, false);
            }
        }

        bucket.borderDoneWithNeighborZ[i] = nBucket.canonical.z;
        bucket.needsCentroidUpdate = true;
        if (updateNeighbor) {
            nBucket.borderDoneWithNeighborZ[j] = bucket.canonical.z;
            nBucket.needsCentroidUpdate = true;
        }
    }

    if (bucket.needsCentroidUpdate || (!bucket.centroidVertexBuffer && bucket.centroidVertexArray.length !== 0)) {
        bucket.uploadCentroid(context);
    }
}
