import { range as d3_range } from 'd3-array';
import { clamp } from 'es-toolkit/compat';

import { geoExtent, geoScaleToZoom } from '../geo';

/** @typedef {{ id: string; xyz: import('../geo/vector').Vec3; extent: geoExtent }} Tile */

export function utilTiler() {
    var _size = [256, 256];
    var _scale = 256;
    var _tileSize = 256;
    var _zoomExtent = [0, 20];
    var _translate = [_size[0] / 2, _size[1] / 2];
    var _margin = 0;
    var _skipNullIsland = false;


    function nearNullIsland(tile) {
        var x = tile[0];
        var y = tile[1];
        var z = tile[2];
        if (z >= 7) {
            var center = Math.pow(2, z - 1);
            var width = Math.pow(2, z - 6);
            var min = center - (width / 2);
            var max = center + (width / 2) - 1;
            return x >= min && x <= max && y >= min && y <= max;
        }
        return false;
    }


    function tiler() {
        var z = geoScaleToZoom(_scale / (2 * Math.PI), _tileSize);
        var z0 = clamp(Math.round(z), _zoomExtent[0], _zoomExtent[1]);
        var tileMin = 0;
        var tileMax = Math.pow(2, z0) - 1;
        var log2ts = Math.log(_tileSize) * Math.LOG2E;
        var k = Math.pow(2, z - z0 + log2ts);
        var origin = [
            (_translate[0] - _scale / 2) / k,
            (_translate[1] - _scale / 2) / k
        ];

        var cols = d3_range(
            clamp(Math.floor(-origin[0]) - _margin,               tileMin, tileMax + 1),
            clamp(Math.ceil(_size[0] / k - origin[0]) + _margin,  tileMin, tileMax + 1)
        );
        var rows = d3_range(
            clamp(Math.floor(-origin[1]) - _margin,               tileMin, tileMax + 1),
            clamp(Math.ceil(_size[1] / k - origin[1]) + _margin,  tileMin, tileMax + 1)
        );

        var tiles = [];
        for (var i = 0; i < rows.length; i++) {
            var y = rows[i];
            for (var j = 0; j < cols.length; j++) {
                var x = cols[j];

                if (i >= _margin && i <= rows.length - _margin &&
                    j >= _margin && j <= cols.length - _margin) {
                    tiles.unshift([x, y, z0]);  // tiles in view at beginning
                } else {
                    tiles.push([x, y, z0]);     // tiles in margin at the end
                }
            }
        }

        tiles.translate = origin;
        tiles.scale = k;

        return tiles;
    }


    /**
     * getTiles() returns an array of tiles that cover the map view
     * @returns {false | Tile[]}
     */
    tiler.getTiles = function(projection) {
        var origin = [
            projection.scale() * Math.PI - projection.translate()[0],
            projection.scale() * Math.PI - projection.translate()[1]
        ];

        this
            .size(projection.clipExtent()[1])
            .scale(projection.scale() * 2 * Math.PI)
            .translate(projection.translate());

        var tiles = tiler();
        var ts = tiles.scale;

        return tiles
            .map(function(tile) {
                if (_skipNullIsland && nearNullIsland(tile)) {
                    return false;
                }
                var x = tile[0] * ts - origin[0];
                var y = tile[1] * ts - origin[1];
                return {
                    id: tile.toString(),
                    xyz: tile,
                    extent: geoExtent(
                        projection.invert([x, y + ts]),
                        projection.invert([x + ts, y])
                    )
                };
            }).filter(Boolean);
    };


    /**
     * getGeoJSON() returns a FeatureCollection for debugging tiles
     */
    tiler.getGeoJSON = function(projection) {
        var features = tiler.getTiles(projection).map(function(tile) {
            return {
                type: 'Feature',
                properties: {
                    id: tile.id,
                    name: tile.id
                },
                geometry: {
                    type: 'Polygon',
                    coordinates: [ tile.extent.polygon() ]
                }
            };
        });

        return {
            type: 'FeatureCollection',
            features: features
        };
    };


    tiler.tileSize = function(val) {
        if (!arguments.length) return _tileSize;
        _tileSize = val;
        return tiler;
    };


    /** @type {GetSet<typeof tiler, Vec2>} */
    tiler.zoomExtent = function(val) {
        if (!arguments.length) return _zoomExtent;
        _zoomExtent = val;
        return tiler;
    };


    tiler.size = function(val) {
        if (!arguments.length) return _size;
        _size = val;
        return tiler;
    };


    tiler.scale = function(val) {
        if (!arguments.length) return _scale;
        _scale = val;
        return tiler;
    };


    tiler.translate = function(val) {
        if (!arguments.length) return _translate;
        _translate = val;
        return tiler;
    };


    // number to extend the rows/columns beyond those covering the viewport
    tiler.margin = function(val) {
        if (!arguments.length) return _margin;
        _margin = +val;
        return tiler;
    };


    /** @type {GetSet<typeof tiler, boolean>} */
    tiler.skipNullIsland = function(val) {
        if (!arguments.length) return _skipNullIsland;
        _skipNullIsland = val;
        return tiler;
    };


    return tiler;
}
