import { select as d3_select } from 'd3-selection';
import { t } from '../core/localizer';

import { geoScaleToZoom, geoVecLength } from '../geo';
import { utilPrefixCSSProperty, utilTiler } from '../util';


export function rendererTileLayer(context) {
    var transformProp = utilPrefixCSSProperty('Transform');
    var tiler = utilTiler();

    var _tileSize = 256;
    var _projection;
    var _cache = {};
    var _tileOrigin;
    var _zoom;
    var _source;
    var _epsilon = 0;

    // Workaround to remove visible grid around tile borders on Chrome with dynamic epsilon for specific browser zoom levels
    // Should be removed when https://issues.chromium.org/issues/40084005 is resolved
    // See https://github.com/openstreetmap/iD/pull/10594
    if (window.chrome) {
        updateEpsilon();
        window.addEventListener('resize', updateEpsilon);
    }
    function updateEpsilon() {
        const pageZoom = Math.round(window.devicePixelRatio * 100);
        if (pageZoom % 25 === 0) {
            _epsilon = 0; // uses mix-blend-mode: plus-lighter
        } else if (pageZoom === 90) {
            _epsilon = 0.005;
        } else if (pageZoom === 110) {
            _epsilon = 0.002;
        } else {
            _epsilon = 0.003;
        }
    }

    function tileSizeAtZoom(d, z) {
        return ((d.tileSize * Math.pow(2, z - d[2])) / d.tileSize) + _epsilon;
    }


    function atZoom(t, distance) {
        var power = Math.pow(2, distance);
        return [
            Math.floor(t[0] * power),
            Math.floor(t[1] * power),
            t[2] + distance
        ];
    }


    function lookUp(d) {
        for (var up = -1; up > -d[2]; up--) {
            var tile = atZoom(d, up);
            if (_cache[_source.url(tile)] !== false) {
                return tile;
            }
        }
    }


    function uniqueBy(a, n) {
        var o = [];
        var seen = {};
        for (var i = 0; i < a.length; i++) {
            if (seen[a[i][n]] === undefined) {
                o.push(a[i]);
                seen[a[i][n]] = true;
            }
        }
        return o;
    }


    function addSource(d) {
        d.url = _source.url(d);
        d.tileSize = _tileSize;
        return d;
    }


    // Update tiles based on current state of `projection`.
    function background(selection) {
        _zoom = geoScaleToZoom(_projection.scale(), _tileSize);

        var pixelOffset;
        if (_source) {
            pixelOffset = [
                _source.offset()[0] * Math.pow(2, _zoom),
                _source.offset()[1] * Math.pow(2, _zoom)
            ];
        } else {
            pixelOffset = [0, 0];
        }

        var translate = [
            _projection.translate()[0] + pixelOffset[0],
            _projection.translate()[1] + pixelOffset[1]
        ];

        tiler
            .scale(_projection.scale() * 2 * Math.PI)
            .translate(translate);

        _tileOrigin = [
            _projection.scale() * Math.PI - translate[0],
            _projection.scale() * Math.PI - translate[1]
        ];

        render(selection);
    }


    // Derive the tiles onscreen, remove those offscreen and position them.
    // Important that this part not depend on `_projection` because it's
    // rentered when tiles load/error (see #644).
    function render(selection) {
        if (!_source) return;
        var requests = [];
        var showDebug = context.getDebug('tile') && !_source.overlay;

        if (_source.validZoom(_zoom)) {
            tiler.skipNullIsland(!!_source.overlay);

            tiler().forEach(function(d) {
                addSource(d);
                if (d.url === '') return;
                if (typeof d.url !== 'string') return; // Workaround for #2295
                requests.push(d);
                if (_cache[d.url] === false && lookUp(d)) {
                    requests.push(addSource(lookUp(d)));
                }
            });

            requests = uniqueBy(requests, 'url').filter(function(r) {
                // don't re-request tiles which have failed in the past
                return _cache[r.url] !== false;
            });
        }

        function load(d3_event, d) {
            _cache[d.url] = true;
            d3_select(this)
                .on('error', null)
                .on('load', null);
            render(selection);
        }

        function error(d3_event, d) {
            _cache[d.url] = false;
            d3_select(this)
                .on('error', null)
                .on('load', null)
                .remove();
            render(selection);
        }

        function imageTransform(d) {
            var ts = d.tileSize * Math.pow(2, _zoom - d[2]);
            var scale = tileSizeAtZoom(d, _zoom);
            return 'translate(' +
                ((d[0] * ts) * _tileSize / d.tileSize - _tileOrigin[0]
            ) + 'px,' +
                ((d[1] * ts) * _tileSize / d.tileSize - _tileOrigin[1]
            ) + 'px) ' +
                'scale(' + scale * _tileSize / d.tileSize + ',' + scale * _tileSize / d.tileSize + ')';
        }

        function tileCenter(d) {
            var ts = d.tileSize * Math.pow(2, _zoom - d[2]);
            return [
                ((d[0] * ts) - _tileOrigin[0] + (ts / 2)),
                ((d[1] * ts) - _tileOrigin[1] + (ts / 2))
            ];
        }

        function debugTransform(d) {
            var coord = tileCenter(d);
            return 'translate(' + coord[0] + 'px,' + coord[1] + 'px)';
        }


        // Pick a representative tile near the center of the viewport
        // (This is useful for sampling the imagery vintage)
        var dims = tiler.size();
        var mapCenter = [dims[0] / 2, dims[1] / 2];
        var minDist = Math.max(dims[0], dims[1]);
        var nearCenter;

        requests.forEach(function(d) {
            var c = tileCenter(d);
            var dist = geoVecLength(c, mapCenter);
            if (dist < minDist) {
                minDist = dist;
                nearCenter = d;
            }
        });


        var image = selection.selectAll('img')
            .data(requests, function(d) { return d.url; });

        image.exit()
            .style(transformProp, imageTransform)
            .classed('tile-removing', true)
            .classed('tile-center', false)
            .each(function() {
                var tile = d3_select(this);
                window.setTimeout(function() {
                    if (tile.classed('tile-removing')) {
                        tile.remove();
                    }
                }, 120);
            });

        image.enter()
          .append('img')
            .attr('class', 'tile')
            .attr('alt', '')
            .attr('draggable', 'false')
            .style('width', _tileSize + 'px')
            .style('height', _tileSize + 'px')
            .attr('src', function(d) { return d.url; })
            .on('error', error)
            .on('load', load)
          .merge(image)
            .style(transformProp, imageTransform)
            .classed('tile-debug', showDebug)
            .classed('tile-removing', false)
            .classed('tile-center', function(d) { return d === nearCenter; });



        var debug = selection.selectAll('.tile-label-debug')
            .data(showDebug ? requests : [], function(d) { return d.url; });

        debug.exit()
            .remove();

        if (showDebug) {
            var debugEnter = debug.enter()
                .append('div')
                .attr('class', 'tile-label-debug');

            debugEnter
                .append('div')
                .attr('class', 'tile-label-debug-coord');

            debugEnter
                .append('div')
                .attr('class', 'tile-label-debug-vintage');

            debug = debug.merge(debugEnter);

            debug
                .style(transformProp, debugTransform);

            debug
                .selectAll('.tile-label-debug-coord')
                .text(function(d) { return d[2] + ' / ' + d[0] + ' / ' + d[1]; });

            debug
                .selectAll('.tile-label-debug-vintage')
                .each(function(d) {
                    var span = d3_select(this);
                    var center = context.projection.invert(tileCenter(d));
                    _source.getMetadata(center, d, function(err, result) {
                        if (result && result.vintage && result.vintage.range) {
                          span.text(result.vintage.range);
                        } else {
                          span.text('');
                          span.call(t.append('info_panels.background.vintage'));
                          span.append('span').text(': ');
                          span.call(t.append('info_panels.background.unknown'));
                        }
                    });
                });
        }

    }


    background.projection = function(val) {
        if (!arguments.length) return _projection;
        _projection = val;
        return background;
    };


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


    background.source = function(val) {
        if (!arguments.length) return _source;
        _source = val;
        _tileSize = _source.tileSize;
        _cache = {};
        tiler.tileSize(_source.tileSize).zoomExtent(_source.zoomExtent);
        return background;
    };


    return background;
}
