import * as d3 from 'd3';
import _ from 'lodash';
import rbush from 'rbush';
import { dataFeatureIcons } from '../../data/index';
import { textDirection } from '../util/locale';

import {
    geoExtent,
    geoEuclideanDistance,
    geoInterp,
    geoPolygonIntersectsPolygon,
    geoPathLength
} from '../geo/index';

import { osmEntity } from '../osm/index';
import { utilDetect } from '../util/detect';

import {
    utilDisplayName,
    utilDisplayNameForPath,
    utilEntitySelector
} from '../util/index';


export function svgLabels(projection, context) {
    var path = d3.geoPath(projection),
        detected = utilDetect(),
        baselineHack = (detected.ie || detected.browser.toLowerCase() === 'edge'),
        rdrawn = rbush(),
        rskipped = rbush(),
        textWidthCache = {},
        entitybboxes = {};

    // Listed from highest to lowest priority
    var labelStack = [
        ['line', 'aeroway', '*', 12],
        ['line', 'highway', 'motorway', 12],
        ['line', 'highway', 'trunk', 12],
        ['line', 'highway', 'primary', 12],
        ['line', 'highway', 'secondary', 12],
        ['line', 'highway', 'tertiary', 12],
        ['line', 'highway', '*', 12],
        ['line', 'railway', '*', 12],
        ['line', 'waterway', '*', 12],
        ['area', 'aeroway', '*', 12],
        ['area', 'amenity', '*', 12],
        ['area', 'building', '*', 12],
        ['area', 'historic', '*', 12],
        ['area', 'leisure', '*', 12],
        ['area', 'man_made', '*', 12],
        ['area', 'natural', '*', 12],
        ['area', 'shop', '*', 12],
        ['area', 'tourism', '*', 12],
        ['area', 'camp_site', '*', 12],
        ['point', 'aeroway', '*', 10],
        ['point', 'amenity', '*', 10],
        ['point', 'building', '*', 10],
        ['point', 'historic', '*', 10],
        ['point', 'leisure', '*', 10],
        ['point', 'man_made', '*', 10],
        ['point', 'natural', '*', 10],
        ['point', 'shop', '*', 10],
        ['point', 'tourism', '*', 10],
        ['point', 'camp_site', '*', 10],
        ['line', 'name', '*', 12],
        ['area', 'name', '*', 12],
        ['point', 'name', '*', 10]
    ];


    function blacklisted(preset) {
        var noIcons = ['building', 'landuse', 'natural'];
        return _.some(noIcons, function(s) {
            return preset.id.indexOf(s) >= 0;
        });
    }


    function get(array, prop) {
        return function(d, i) { return array[i][prop]; };
    }


    function textWidth(text, size, elem) {
        var c = textWidthCache[size];
        if (!c) c = textWidthCache[size] = {};

        if (c[text]) {
            return c[text];

        } else if (elem) {
            c[text] = elem.getComputedTextLength();
            return c[text];

        } else {
            var str = encodeURIComponent(text).match(/%[CDEFcdef]/g);
            if (str === null) {
                return size / 3 * 2 * text.length;
            } else {
                return size / 3 * (2 * text.length + str.length);
            }
        }
    }


    function drawLinePaths(selection, entities, filter, classes, labels) {
        var paths = selection.selectAll('path')
            .filter(filter)
            .data(entities, osmEntity.key);

        paths.exit()
            .remove();

        paths.enter()
            .append('path')
            .style('stroke-width', get(labels, 'font-size'))
            .attr('id', function(d) { return 'labelpath-' + d.id; })
            .attr('class', classes)
            .merge(paths)
            .attr('d', get(labels, 'lineString'));
    }


    function drawLineLabels(selection, entities, filter, classes, labels) {
        var texts = selection.selectAll('text.' + classes)
            .filter(filter)
            .data(entities, osmEntity.key);

        texts.exit()
            .remove();

        texts.enter()
            .append('text')
            .attr('class', function(d, i) { return classes + ' ' + labels[i].classes + ' ' + d.id; })
            .attr('dy', baselineHack ? '0.35em' : null)
            .append('textPath')
            .attr('class', 'textpath');

        texts = selection.selectAll('text.' + classes);

        texts.selectAll('.textpath')
            .filter(filter)
            .data(entities, osmEntity.key)
            .attr('startOffset', '50%')
            .attr('xlink:href', function(d) { return '#labelpath-' + d.id; })
            .text(utilDisplayNameForPath);
    }


    function drawPointLabels(selection, entities, filter, classes, labels) {
        var texts = selection.selectAll('text.' + classes)
            .filter(filter)
            .data(entities, osmEntity.key);

        texts.exit()
            .remove();

        texts = texts.enter()
            .append('text')
            .attr('class', function(d, i) {
                return classes + ' ' + labels[i].classes + ' ' + d.id;
            })
            .merge(texts);

        texts
            .attr('x', get(labels, 'x'))
            .attr('y', get(labels, 'y'))
            .style('text-anchor', get(labels, 'textAnchor'))
            .text(utilDisplayName)
            .each(function(d, i) {
                textWidth(utilDisplayName(d), labels[i].height, this);
            });
    }


    function drawAreaLabels(selection, entities, filter, classes, labels) {
        entities = entities.filter(hasText);
        labels = labels.filter(hasText);
        drawPointLabels(selection, entities, filter, classes, labels);

        function hasText(d, i) {
            return labels[i].hasOwnProperty('x') && labels[i].hasOwnProperty('y');
        }
    }


    function drawAreaIcons(selection, entities, filter, classes, labels) {
        var icons = selection.selectAll('use.' + classes)
            .filter(filter)
            .data(entities, osmEntity.key);

        icons.exit()
            .remove();

        icons = icons.enter()
            .append('use')
            .attr('class', 'icon ' + classes)
            .attr('width', '17px')
            .attr('height', '17px')
            .merge(icons);

        icons
            .attr('transform', get(labels, 'transform'))
            .attr('xlink:href', function(d) {
                var preset = context.presets().match(d, context.graph()),
                    picon = preset && preset.icon;

                if (!picon)
                    return '';
                else {
                    var isMaki = dataFeatureIcons.indexOf(picon) !== -1;
                    return '#' + picon + (isMaki ? '-15' : '');
                }
            });
    }


    function drawCollisionBoxes(selection, rtree, which) {
        var showDebug = context.getDebug('collision'),
            classes = 'debug ' + which + ' ' +
                (which === 'debug-skipped' ? 'orange' : 'yellow');

        var debug = selection.selectAll('.layer-label-debug')
                .data(showDebug ? [true] : []);

        debug.exit()
            .remove();

        debug = debug.enter()
            .append('g')
            .attr('class', 'layer-label-debug')
            .merge(debug);

        if (showDebug) {
            var gj = rtree.all().map(function(d) {
                return { type: 'Polygon', coordinates: [[
                    [d.minX, d.minY],
                    [d.maxX, d.minY],
                    [d.maxX, d.maxY],
                    [d.minX, d.maxY],
                    [d.minX, d.minY]
                ]]};
            });

            var debugboxes = debug.selectAll('.' + which)
                .data(gj);

            debugboxes.exit()
                .remove();

            debugboxes = debugboxes.enter()
                .append('path')
                .attr('class', classes)
                .merge(debugboxes);

            debugboxes
                .attr('d', d3.geoPath());
        }
    }


    function drawLabels(selection, graph, entities, filter, dimensions, fullRedraw) {
        var lowZoom = context.surface().classed('low-zoom');

        var labelable = [], i, j, k, entity, geometry;
        for (i = 0; i < labelStack.length; i++) {
            labelable.push([]);
        }

        if (fullRedraw) {
            rdrawn.clear();
            rskipped.clear();
            entitybboxes = {};
        } else {
            for (i = 0; i < entities.length; i++) {
                entity = entities[i];
                var toRemove = []
                    .concat(entitybboxes[entity.id] || [])
                    .concat(entitybboxes[entity.id + 'I'] || []);

                for (j = 0; j < toRemove.length; j++) {
                    rdrawn.remove(toRemove[j]);
                    rskipped.remove(toRemove[j]);
                }
            }
        }

        // Split entities into groups specified by labelStack
        for (i = 0; i < entities.length; i++) {
            entity = entities[i];
            geometry = entity.geometry(graph);
            if (geometry === 'vertex') { geometry = 'point'; }  // treat vertex like point

            var preset = geometry === 'area' && context.presets().match(entity, graph),
                icon = preset && !blacklisted(preset) && preset.icon;

            if (!icon && !utilDisplayName(entity))
                continue;

            for (k = 0; k < labelStack.length; k++) {
                var matchGeom = labelStack[k][0],
                    matchKey = labelStack[k][1],
                    matchVal = labelStack[k][2],
                    hasVal = entity.tags[matchKey];

                if (geometry === matchGeom && hasVal && (matchVal === '*' || matchVal === hasVal)) {
                    labelable[k].push(entity);
                    break;
                }
            }
        }

        var positions = {
            point: [],
            line: [],
            area: []
        };

        var labelled = {
            point: [],
            line: [],
            area: []
        };

        // Try and find a valid label for labellable entities
        for (k = 0; k < labelable.length; k++) {
            var fontSize = labelStack[k][3];
            for (i = 0; i < labelable[k].length; i++) {
                entity = labelable[k][i];
                geometry = entity.geometry(graph);

                var getName = (geometry === 'line') ? utilDisplayNameForPath : utilDisplayName,
                    name = getName(entity),
                    width = name && textWidth(name, fontSize),
                    p;

                if (geometry === 'point') {
                    p = getPointLabel(entity, width, fontSize, geometry);
                } else if (geometry === 'vertex' && !lowZoom) {
                    // don't label vertices at low zoom because they don't have icons
                    p = getPointLabel(entity, width, fontSize, geometry);
                } else if (geometry === 'line') {
                    p = getLineLabel(entity, width, fontSize);
                } else if (geometry === 'area') {
                    p = getAreaLabel(entity, width, fontSize);
                }

                if (p) {
                    if (geometry === 'vertex') { geometry = 'point'; }  // treat vertex like point
                    p.classes = geometry + ' tag-' + labelStack[k][1];
                    positions[geometry].push(p);
                    labelled[geometry].push(entity);
                }
            }
        }


        function getPointLabel(entity, width, height, geometry) {
            var y = (geometry === 'point' ? -12 : 0),
                pointOffsets = {
                    ltr: [15, y, 'start'],
                    rtl: [-15, y, 'end']
                };

            var coord = projection(entity.loc),
                margin = 2,
                offset = pointOffsets[textDirection],
                p = {
                    height: height,
                    width: width,
                    x: coord[0] + offset[0],
                    y: coord[1] + offset[1],
                    textAnchor: offset[2]
                },
                bbox;

            if (textDirection === 'rtl') {
                bbox = {
                    minX: p.x - width - margin,
                    minY: p.y - (height / 2) - margin,
                    maxX: p.x + margin,
                    maxY: p.y + (height / 2) + margin
                };
            } else {
                bbox = {
                    minX: p.x - margin,
                    minY: p.y - (height / 2) - margin,
                    maxX: p.x + width + margin,
                    maxY: p.y + (height / 2) + margin
                };
            }

            if (tryInsert([bbox], entity.id, true)) {
                return p;
            }
        }


        function getLineLabel(entity, width, height) {
            var viewport = geoExtent(context.projection.clipExtent()).polygon(),
                nodes = _.map(graph.childNodes(entity), 'loc').map(projection),
                length = geoPathLength(nodes);

            if (length < width + 20) return;

            // % along the line to attempt to place the label
            var lineOffsets = [50, 45, 55, 40, 60, 35, 65, 30, 70,
                               25, 75, 20, 80, 15, 95, 10, 90, 5, 95];
            var margin = 3;

            for (var i = 0; i < lineOffsets.length; i++) {
                var offset = lineOffsets[i],
                    middle = offset / 100 * length,
                    start = middle - width / 2;

                if (start < 0 || start + width > length) continue;

                // generate subpath and ignore paths that are invalid or don't cross viewport.
                var sub = subpath(nodes, start, start + width);
                if (!sub || !geoPolygonIntersectsPolygon(viewport, sub, true)) {
                    continue;
                }

                var isReverse = reverse(sub);
                if (isReverse) {
                    sub = sub.reverse();
                }

                var bboxes = [],
                    boxsize = (height + 2) / 2;

                for (var j = 0; j < sub.length - 1; j++) {
                    var a = sub[j];
                    var b = sub[j + 1];
                    var num = Math.max(1, Math.floor(geoEuclideanDistance(a, b) / boxsize / 2));

                    for (var box = 0; box < num; box++) {
                        var p = geoInterp(a, b, box / num);
                        var x0 = p[0] - boxsize - margin;
                        var y0 = p[1] - boxsize - margin;
                        var x1 = p[0] + boxsize + margin;
                        var y1 = p[1] + boxsize + margin;

                        bboxes.push({
                            minX: Math.min(x0, x1),
                            minY: Math.min(y0, y1),
                            maxX: Math.max(x0, x1),
                            maxY: Math.max(y0, y1)
                        });
                    }
                }

                if (tryInsert(bboxes, entity.id, false)) {
                    return {
                        'font-size': height + 2,
                        lineString: lineString(sub),
                        startOffset: offset + '%'
                    };
                }
            }

            function reverse(p) {
                var angle = Math.atan2(p[1][1] - p[0][1], p[1][0] - p[0][0]);
                return !(p[0][0] < p[p.length - 1][0] && angle < Math.PI/2 && angle > -Math.PI/2);
            }

            function lineString(nodes) {
                return 'M' + nodes.join('L');
            }

            function subpath(nodes, from, to) {
                var sofar = 0,
                    start, end, i0, i1;

                for (var i = 0; i < nodes.length - 1; i++) {
                    var a = nodes[i],
                        b = nodes[i + 1];
                    var current = geoEuclideanDistance(a, b);
                    var portion;
                    if (!start && sofar + current >= from) {
                        portion = (from - sofar) / current;
                        start = [
                            a[0] + portion * (b[0] - a[0]),
                            a[1] + portion * (b[1] - a[1])
                        ];
                        i0 = i + 1;
                    }
                    if (!end && sofar + current >= to) {
                        portion = (to - sofar) / current;
                        end = [
                            a[0] + portion * (b[0] - a[0]),
                            a[1] + portion * (b[1] - a[1])
                        ];
                        i1 = i + 1;
                    }
                    sofar += current;
                }

                var ret = nodes.slice(i0, i1);
                ret.unshift(start);
                ret.push(end);
                return ret;
            }
        }


        function getAreaLabel(entity, width, height) {
            var centroid = path.centroid(entity.asGeoJSON(graph, true)),
                extent = entity.extent(graph),
                entitywidth = projection(extent[1])[0] - projection(extent[0])[0];

            if (isNaN(centroid[0]) || entitywidth < 20) return;

            var iconSize = 20,
                iconX = centroid[0] - (iconSize / 2),
                iconY = centroid[1] - (iconSize / 2),
                margin = 2,
                textOffset = iconSize + margin,
                p = { transform: 'translate(' + iconX + ',' + iconY + ')' };

            var bbox = {
                minX: iconX,
                minY: iconY,
                maxX: iconX + iconSize,
                maxY: iconY + iconSize
            };

            // try to add icon
            if (tryInsert([bbox], entity.id + 'I', true)) {
                if (width && entitywidth >= width + 20) {
                    var labelX = centroid[0],
                        labelY = centroid[1] + textOffset;

                    bbox = {
                        minX: labelX - (width / 2) - margin,
                        minY: labelY - (height / 2) - margin,
                        maxX: labelX + (width / 2) + margin,
                        maxY: labelY + (height / 2) + margin
                    };

                    // try to add label
                    if (tryInsert([bbox], entity.id, true)) {
                        p.x = labelX;
                        p.y = labelY;
                        p.textAnchor = 'middle';
                        p.height = height;
                    }
                }

                return p;
            }
        }


        function tryInsert(bboxes, id, saveSkipped) {
            var skipped = false,
                bbox;

            for (var i = 0; i < bboxes.length; i++) {
                bbox = bboxes[i];
                bbox.id = id;

                // Check that label is visible
                if (bbox.minX < 0 || bbox.minY < 0 || bbox.maxX > dimensions[0] || bbox.maxY > dimensions[1]) {
                    skipped = true;
                    break;
                }
                if (rdrawn.collides(bbox)) {
                    skipped = true;
                    break;
                }
            }

            entitybboxes[id] = bboxes;

            if (skipped) {
                if (saveSkipped) {
                    rskipped.load(bboxes);
                }
            } else {
                rdrawn.load(bboxes);
            }

            return !skipped;
        }


        var label = selection.selectAll('.layer-label'),
            halo = selection.selectAll('.layer-halo');

        // points
        drawPointLabels(label, labelled.point, filter, 'pointlabel', positions.point);
        drawPointLabels(halo, labelled.point, filter, 'pointlabel-halo', positions.point);

        // lines
        drawLinePaths(halo, labelled.line, filter, '', positions.line);
        drawLineLabels(label, labelled.line, filter, 'linelabel', positions.line);
        drawLineLabels(halo, labelled.line, filter, 'linelabel-halo', positions.line);

        // areas
        drawAreaLabels(label, labelled.area, filter, 'arealabel', positions.area);
        drawAreaLabels(halo, labelled.area, filter, 'arealabel-halo', positions.area);
        drawAreaIcons(label, labelled.area, filter, 'areaicon', positions.area);
        drawAreaIcons(halo, labelled.area, filter, 'areaicon-halo', positions.area);

        // debug
        drawCollisionBoxes(label, rskipped, 'debug-skipped');
        drawCollisionBoxes(label, rdrawn, 'debug-drawn');

        selection.call(filterLabels);
    }


    function filterLabels(selection) {
        var layers = selection
            .selectAll('.layer-label, .layer-halo');

        layers.selectAll('.proximate')
            .classed('proximate', false);

        var mouse = context.mouse(),
            graph = context.graph(),
            selectedIDs = context.selectedIDs(),
            ids = [],
            pad, bbox;

        // hide labels near the mouse
        if (mouse) {
            pad = 20;
            bbox = { minX: mouse[0] - pad, minY: mouse[1] - pad, maxX: mouse[0] + pad, maxY: mouse[1] + pad };
            ids.push.apply(ids, _.map(rdrawn.search(bbox), 'id'));
        }

        // hide labels along selected ways, or near selected vertices
        for (var i = 0; i < selectedIDs.length; i++) {
            var entity = graph.hasEntity(selectedIDs[i]);
            if (!entity) continue;
            var geometry = entity.geometry(graph);

            if (geometry === 'line') {
                ids.push(selectedIDs[i]);
            } else if (geometry === 'vertex') {
                var point = context.projection(entity.loc);
                pad = 10;
                bbox = { minX: point[0] - pad, minY: point[1] - pad, maxX: point[0] + pad, maxY: point[1] + pad };
                ids.push.apply(ids, _.map(rdrawn.search(bbox), 'id'));
            }
        }

        layers.selectAll(utilEntitySelector(ids))
            .classed('proximate', true);
    }


    var throttleFilterLabels = _.throttle(filterLabels, 100);


    drawLabels.observe = function(selection) {
        var listener = function() { throttleFilterLabels(selection); };
        selection.on('mousemove.hidelabels', listener);
        context.on('enter.hidelabels', listener);
    };


    drawLabels.off = function(selection) {
        throttleFilterLabels.cancel();
        selection.on('mousemove.hidelabels', null);
        context.on('enter.hidelabels', null);
    };


    return drawLabels;
}
