import deepEqual from 'fast-deep-equal';
import { range as d3_range } from 'd3-array';

import {
    svgMarkerSegments, svgPath, svgRelationMemberTags, svgSegmentWay
} from './helpers';
import { svgTagClasses } from './tag_classes';

import { osmEntity } from '../osm';
import { utilArrayFlatten, utilArrayGroupBy } from '../util';
import { utilDetect } from '../util/detect';

/** @param {{ [key: string ]: string }} tags */
function onewayArrowColour(tags) {
    // the return value must be defined in ./defs.js
    if (tags.highway === 'construction' && tags.bridge) return 'white';
    if (tags.highway === 'pedestrian') return 'gray';
    if (tags.railway) return 'gray';
    if (tags.aeroway === 'runway') return 'white';

    return 'black';
}

export function svgLines(projection, context) {
    var detected = utilDetect();

    var highway_stack = {
        motorway: 0,
        motorway_link: 1,
        trunk: 2,
        trunk_link: 3,
        primary: 4,
        primary_link: 5,
        secondary: 6,
        tertiary: 7,
        unclassified: 8,
        residential: 9,
        service: 10,
        busway: 11,
        footway: 12
    };


    function drawTargets(selection, graph, entities, filter) {
        var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor ';
        var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor ';
        var getPath = svgPath(projection).geojson;
        var activeID = context.activeID();
        var base = context.history().base();

        // The targets and nopes will be MultiLineString sub-segments of the ways
        var data = { targets: [], nopes: [] };

        entities.forEach(function(way) {
            var features = svgSegmentWay(way, graph, activeID);
            data.targets.push.apply(data.targets, features.passive);
            data.nopes.push.apply(data.nopes, features.active);
        });


        // Targets allow hover and vertex snapping
        var targetData = data.targets.filter(getPath);
        var targets = selection.selectAll('.line.target-allowed')
            .filter(function(d) { return filter(d.properties.entity); })
            .data(targetData, function key(d) { return d.id; });

        // exit
        targets.exit()
            .remove();

        var segmentWasEdited = function(d) {
            var wayID = d.properties.entity.id;
            // if the whole line was edited, don't draw segment changes
            if (!base.entities[wayID] ||
                !deepEqual(graph.entities[wayID].nodes, base.entities[wayID].nodes)) {
                return false;
            }
            return d.properties.nodes.some(function(n) {
                return !base.entities[n.id] ||
                       !deepEqual(graph.entities[n.id].loc, base.entities[n.id].loc);
            });
        };

        // enter/update
        targets.enter()
            .append('path')
            .merge(targets)
            .attr('d', getPath)
            .attr('class', function(d) {
                return 'way line target target-allowed ' + targetClass + d.id;
            })
            .classed('segment-edited', segmentWasEdited);

        // NOPE
        var nopeData = data.nopes.filter(getPath);
        var nopes = selection.selectAll('.line.target-nope')
            .filter(function(d) { return filter(d.properties.entity); })
            .data(nopeData, function key(d) { return d.id; });

        // exit
        nopes.exit()
            .remove();

        // enter/update
        nopes.enter()
            .append('path')
            .merge(nopes)
            .attr('d', getPath)
            .attr('class', function(d) {
                return 'way line target target-nope ' + nopeClass + d.id;
            })
            .classed('segment-edited', segmentWasEdited);
    }


    function drawLines(selection, graph, entities, filter) {
        var base = context.history().base();

        function waystack(a, b) {
            var selected = context.selectedIDs();
            var scoreA = selected.indexOf(a.id) !== -1 ? 20 : 0;
            var scoreB = selected.indexOf(b.id) !== -1 ? 20 : 0;

            if (a.tags.highway) { scoreA -= highway_stack[a.tags.highway]; }
            if (b.tags.highway) { scoreB -= highway_stack[b.tags.highway]; }
            return scoreA - scoreB;
        }


        function drawLineGroup(selection, klass, isSelected) {
            // Note: Don't add `.selected` class in draw modes
            var mode = context.mode();
            var isDrawing = mode && /^draw/.test(mode.id);
            var selectedClass = (!isDrawing && isSelected) ? 'selected ' : '';

            var lines = selection
                .selectAll('path')
                .filter(filter)
                .data(getPathData(isSelected), osmEntity.key);

            lines.exit()
                .remove();

            // Optimization: Call expensive TagClasses only on enter selection. This
            // works because osmEntity.key is defined to include the entity v attribute.
            lines.enter()
                .append('path')
                .attr('class', function(d) {

                    var prefix = 'way line';

                    // if this line isn't styled by its own tags
                    if (!d.hasInterestingTags()) {

                        var parentRelations = graph.parentRelations(d);
                        var parentMultipolygons = parentRelations.filter(function(relation) {
                            return relation.isMultipolygon();
                        });

                        // and if it's a member of at least one multipolygon relation
                        if (parentMultipolygons.length > 0 &&
                            // and only multipolygon relations
                            parentRelations.length === parentMultipolygons.length) {
                            // then fudge the classes to style this as an area edge
                            prefix = 'relation area';
                        }
                    }

                    var oldMPClass = oldMultiPolygonOuters[d.id] ? 'old-multipolygon ' : '';
                    return prefix + ' ' + klass + ' ' + selectedClass + oldMPClass + d.id;
                })
                .classed('added', function(d) {
                    return !base.entities[d.id];
                })
                .classed('geometry-edited', function(d) {
                    return graph.entities[d.id] &&
                        base.entities[d.id] &&
                        !deepEqual(graph.entities[d.id].nodes, base.entities[d.id].nodes);
                })
                .classed('retagged', function(d) {
                    return graph.entities[d.id] &&
                        base.entities[d.id] &&
                        !deepEqual(graph.entities[d.id].tags, base.entities[d.id].tags);
                })
                .call(svgTagClasses())
                .merge(lines)
                .sort(waystack)
                .attr('d', getPath)
                .call(svgTagClasses().tags(svgRelationMemberTags(graph)));

            return selection;
        }


        function getPathData(isSelected) {
            return function() {
                var layer = this.parentNode.__data__;
                var data = pathdata[layer] || [];
                return data.filter(function(d) {
                    if (isSelected) {
                        return context.selectedIDs().indexOf(d.id) !== -1;
                    } else {
                        return context.selectedIDs().indexOf(d.id) === -1;
                    }
                });
            };
        }

        function addMarkers(layergroup, pathclass, groupclass, groupdata, marker) {
            var markergroup = layergroup
                .selectAll('g.' + groupclass)
                .data([pathclass]);

            markergroup = markergroup.enter()
                .append('g')
                .attr('class', groupclass)
                .merge(markergroup);

            var markers = markergroup
                .selectAll('path')
                .filter(filter)
                .data(
                    function data() { return groupdata[this.parentNode.__data__] || []; },
                    function key(d) { return [d.id, d.index]; }
                );

            markers.exit()
                .remove();

            markers = markers.enter()
                .append('path')
                .attr('class', pathclass)
                .merge(markers)
                .attr('marker-mid', marker)
                .attr('d', function(d) { return d.d; });

            if (detected.ie) {
                markers.each(function() { this.parentNode.insertBefore(this, this); });
            }
        }


        var getPath = svgPath(projection, graph);
        var ways = [];
        var onewaydata = {};
        var sideddata = {};
        var oldMultiPolygonOuters = {};

        for (var i = 0; i < entities.length; i++) {
            var entity = entities[i];
            if (entity.geometry(graph) === 'line'
                       // to render side-markers for coastlines (see
                       // https://github.com/openstreetmap/iD/issues/9293)
                    || entity.geometry(graph) === 'area' && entity.sidednessIdentifier
                        && entity.sidednessIdentifier() === 'coastline') {
                ways.push(entity);
            }
        }

        ways = ways.filter(getPath);
        const pathdata = utilArrayGroupBy(ways, (way) => Math.trunc(way.layer()));

        Object.keys(pathdata).forEach(function(k) {
            var v = pathdata[k];
            var onewayArr = v.filter(function(d) { return d.isOneWay(); });
            var onewaySegments = svgMarkerSegments(
                projection, graph, 36,
                entity => entity.isOneWayBackwards(),
                entity => entity.isBiDirectional(),
            );
            onewaydata[k] = utilArrayFlatten(onewayArr.map(onewaySegments));

            var sidedArr = v.filter(function(d) { return d.isSided(); });
            var sidedSegments = svgMarkerSegments(
                projection, graph, 30
            );
            sideddata[k] = utilArrayFlatten(sidedArr.map(sidedSegments));
        });


        var covered = selection.selectAll('.layer-osm.covered');     // under areas
        var uncovered = selection.selectAll('.layer-osm.lines');     // over areas
        var touchLayer = selection.selectAll('.layer-touch.lines');

        // Draw lines..
        [covered, uncovered].forEach(function(selection) {
            var range = (selection === covered ? d3_range(-10,0) : d3_range(0,11));
            var layergroup = selection
                .selectAll('g.layergroup')
                .data(range);

            layergroup = layergroup.enter()
                .append('g')
                .attr('class', function(d) { return 'layergroup layer' + String(d); })
                .merge(layergroup);

            layergroup
                .selectAll('g.linegroup')
                .data(['shadow', 'casing', 'stroke', 'shadow-highlighted', 'casing-highlighted', 'stroke-highlighted'])
                .enter()
                .append('g')
                .attr('class', function(d) { return 'linegroup line-' + d; });

            layergroup.selectAll('g.line-shadow')
                .call(drawLineGroup, 'shadow', false);
            layergroup.selectAll('g.line-casing')
                .call(drawLineGroup, 'casing', false);
            layergroup.selectAll('g.line-stroke')
                .call(drawLineGroup, 'stroke', false);

            layergroup.selectAll('g.line-shadow-highlighted')
                .call(drawLineGroup, 'shadow', true);
            layergroup.selectAll('g.line-casing-highlighted')
                .call(drawLineGroup, 'casing', true);
            layergroup.selectAll('g.line-stroke-highlighted')
                .call(drawLineGroup, 'stroke', true);

            addMarkers(layergroup, 'oneway', 'onewaygroup', onewaydata, (d) => {
                const category = onewayArrowColour(graph.entity(d.id).tags);
                return `url(#ideditor-oneway-marker-${category})`;
            });
            addMarkers(layergroup, 'sided', 'sidedgroup', sideddata,
                function marker(d) {
                    var category = graph.entity(d.id).sidednessIdentifier();
                    return 'url(#ideditor-sided-marker-' + category + ')';
                }
            );
        });

        // Draw touch targets..
        touchLayer
            .call(drawTargets, graph, ways, filter);
    }


    return drawLines;
}
