class BingTileFunctions {

    @Description("Given a geometry and a zoom level, returns the minimum set of Bing tiles that fully covers that geometry")
    @ScalarFunction("geometry_to_bing_tiles")
    @SqlType("array(" + BingTileType.NAME + ")")
    public static Block geometryToBingTiles(@SqlType(GEOMETRY_TYPE_NAME) Slice input, @SqlType(StandardTypes.INTEGER) long zoomLevelInput)
    {
        checkZoomLevel(zoomLevelInput);

        int zoomLevel = toIntExact(zoomLevelInput);

        OGCGeometry ogcGeometry = deserialize(input);
        if (ogcGeometry.isEmpty()) {
            return EMPTY_TILE_ARRAY;
        }

        Envelope envelope = getEnvelope(ogcGeometry);

        checkLatitude(envelope.getYMin(), LATITUDE_SPAN_OUT_OF_RANGE);
        checkLatitude(envelope.getYMax(), LATITUDE_SPAN_OUT_OF_RANGE);
        checkLongitude(envelope.getXMin(), LONGITUDE_SPAN_OUT_OF_RANGE);
        checkLongitude(envelope.getXMax(), LONGITUDE_SPAN_OUT_OF_RANGE);

        boolean pointOrRectangle = isPointOrRectangle(ogcGeometry, envelope);

        BingTile leftUpperTile = latitudeLongitudeToTile(envelope.getYMax(), envelope.getXMin(), zoomLevel);
        BingTile rightLowerTile = getTileCoveringLowerRightCorner(envelope, zoomLevel);

        // XY coordinates start at (0,0) in the left upper corner and increase left to right and top to bottom
        long tileCount = (long) (rightLowerTile.getX() - leftUpperTile.getX() + 1) * (rightLowerTile.getY() - leftUpperTile.getY() + 1);

        checkGeometryToBingTilesLimits(ogcGeometry, envelope, pointOrRectangle, tileCount, zoomLevel);

        BlockBuilder blockBuilder = BIGINT.createBlockBuilder(null, toIntExact(tileCount));
        if (pointOrRectangle || zoomLevel <= OPTIMIZED_TILING_MIN_ZOOM_LEVEL) {
            // Collect tiles covering the bounding box and check each tile for intersection with the geometry.
            // Skip intersection check if geometry is a point or rectangle. In these cases, by definition,
            // all tiles covering the bounding box intersect the geometry.
            for (int x = leftUpperTile.getX(); x <= rightLowerTile.getX(); x++) {
                for (int y = leftUpperTile.getY(); y <= rightLowerTile.getY(); y++) {
                    BingTile tile = BingTile.fromCoordinates(x, y, zoomLevel);
                    if (pointOrRectangle || !disjoint(tileToEnvelope(tile), ogcGeometry)) {
                        BIGINT.writeLong(blockBuilder, tile.encode());
                    }
                }
            }
        }
        else {
            // Intersection checks above are expensive. The logic below attempts to reduce the number
            // of these checks. The idea is to identify large tiles which are fully covered by the
            // geometry. For each such tile, we can cheaply compute all the containing tiles at
            // the right zoom level and append them to results in bulk. This way we perform a single
            // containment check instead of 2 to the power of level delta intersection checks, where
            // level delta is the difference between the desired zoom level and level of the large
            // tile covered by the geometry.
            BingTile[] tiles = getTilesInBetween(leftUpperTile, rightLowerTile, OPTIMIZED_TILING_MIN_ZOOM_LEVEL);
            for (BingTile tile : tiles) {
                appendIntersectingSubtiles(ogcGeometry, zoomLevel, tile, blockBuilder);
            }
        }

        return blockBuilder.build();
    }

    private  BingTileFunctions();

}