import {
    geoIdentity as d3_geoIdentity,
    geoPath as d3_geoPath,
    geoStream as d3_geoStream
} from 'd3-geo';

import { geoVecAdd, geoVecAngle, geoVecLength } from '../geo';


// Touch targets control which other vertices we can drag a vertex onto.
//
// - the activeID - nope
// - 1 away (adjacent) to the activeID - yes (vertices will be merged)
// - 2 away from the activeID - nope (would create a self intersecting segment)
// - all others on a linear way - yes
// - all others on a closed way - nope (would create a self intersecting polygon)
//
// returns
// 0 = active vertex - no touch/connect
// 1 = passive vertex - yes touch/connect
// 2 = adjacent vertex - yes but pay attention segmenting a line here
//
export function svgPassiveVertex(node, graph, activeID) {
    if (!activeID) return 1;
    if (activeID === node.id) return 0;

    var parents = graph.parentWays(node);

    var i, j, nodes, isClosed, ix1, ix2, ix3, ix4, max;

    for (i = 0; i < parents.length; i++) {
        nodes = parents[i].nodes;
        isClosed = parents[i].isClosed();
        for (j = 0; j < nodes.length; j++) {   // find this vertex, look nearby
            if (nodes[j] === node.id) {
                ix1 = j - 2;
                ix2 = j - 1;
                ix3 = j + 1;
                ix4 = j + 2;

                if (isClosed) {  // wraparound if needed
                    max = nodes.length - 1;
                    if (ix1 < 0)   ix1 = max + ix1;
                    if (ix2 < 0)   ix2 = max + ix2;
                    if (ix3 > max) ix3 = ix3 - max;
                    if (ix4 > max) ix4 = ix4 - max;
                }

                if (nodes[ix1] === activeID) return 0;        // no - prevent self intersect
                else if (nodes[ix2] === activeID) return 2;   // ok - adjacent
                else if (nodes[ix3] === activeID) return 2;   // ok - adjacent
                else if (nodes[ix4] === activeID) return 0;   // no - prevent self intersect
                else if (isClosed && nodes.indexOf(activeID) !== -1) return 0;  // no - prevent self intersect
            }
        }
    }

    return 1;   // ok
}


/**
 *
 * @param {iD.Projection} projection
 * @param {iD.Graph} graph
 * @param {Number} dt spacing between segments
 * @param {Function<Boolean>} [shouldReverse]
 * @param {Function<Boolean>} [bothDirections]
 */
export function svgMarkerSegments(projection, graph, dt, shouldReverse = () => false, bothDirections = () => false) {
    /**
     * @param {iD.OsmWay} entity
     * @returns {[{id: String, d: String}]} list of svg path segments corres
     */
    return function(entity) {
        let i = 0;
        let offset = dt / 2;
        const segments = [];

        const clip = paddedClipExtent(projection);

        const coordinates = graph.childNodes(entity).map(function(n) { return n.loc; });
        let a, b;

        const _shouldReverse = shouldReverse(entity);
        const _bothDirections = bothDirections(entity);

        d3_geoStream({
            type: 'LineString',
            coordinates: coordinates
        }, projection.stream(clip({
            lineStart: function() {},
            lineEnd: function() { a = null; },
            point: function(x, y) {
                b = [x, y];

                if (a) {
                    let span = geoVecLength(a, b) - offset;

                    if (span >= 0) {
                        const heading = geoVecAngle(a, b);
                        const dx = dt * Math.cos(heading);
                        const dy = dt * Math.sin(heading);
                        let p = [
                            a[0] + offset * Math.cos(heading),
                            a[1] + offset * Math.sin(heading)
                        ];

                        // gather coordinates
                        const coord = [a, p];
                        for (span -= dt; span >= 0; span -= dt) {
                            p = geoVecAdd(p, [dx, dy]);
                            coord.push(p);
                        }
                        coord.push(b);

                        // generate svg paths
                        let segment = '';

                        if (!_shouldReverse || _bothDirections) {
                            for (let j = 0; j < coord.length; j++) {
                                segment += (j === 0 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];
                            }
                            segments.push({ id: entity.id, index: i++, d: segment });
                        }

                        if (_shouldReverse || _bothDirections) {
                            segment = '';
                            for (let j = coord.length - 1; j >= 0; j--) {
                                segment += (j === coord.length - 1 ? 'M' : 'L') + coord[j][0] + ',' + coord[j][1];
                            }
                            segments.push({ id: entity.id, index: i++, d: segment });
                        }
                    }

                    offset = -span;
                }

                a = b;
            }
        })));

        return segments;
    };
}


/**
 * @param {iD.Projection} projection
 * @param {iD.Graph} graph
 * @param {Boolean} isArea
 */
export function svgPath(projection, graph, isArea) {
    const cache = {};
    const project = projection.stream;
    const clip = paddedClipExtent(projection, isArea);
    const path = d3_geoPath()
        .projection({stream: function(output) { return project(clip(output)); }});

    const svgpath = function(entity) {
        if (entity.id in cache) {
            return cache[entity.id];
        } else {
            return cache[entity.id] = path(entity.asGeoJSON(graph));
        }
    };

    svgpath.geojson = function(d) {
        if (d.__featurehash__ !== undefined) {
            if (d.__featurehash__ in cache) {
                return cache[d.__featurehash__];
            } else {
                return cache[d.__featurehash__] = path(d);
            }
        } else {
            return path(d);
        }
    };

    return svgpath;
}


export function svgPointTransform(projection) {
    var svgpoint = function(entity) {
        // http://jsperf.com/short-array-join
        var pt = projection(entity.loc);
        return 'translate(' + pt[0] + ',' + pt[1] + ')';
    };

    svgpoint.geojson = function(d) {
        return svgpoint(d.properties.entity);
    };

    return svgpoint;
}


export function svgRelationMemberTags(graph) {
    return function(entity) {
        var tags = entity.tags;
        var shouldCopyMultipolygonTags = !entity.hasInterestingTags();
        graph.parentRelations(entity).forEach(function(relation) {
            var type = relation.tags.type;
            if ((type === 'multipolygon' && shouldCopyMultipolygonTags) || type === 'boundary') {
                tags = Object.assign({}, relation.tags, tags);
            }
        });
        return tags;
    };
}


export function svgSegmentWay(way, graph, activeID) {
    // When there is no activeID, we can memoize this expensive computation
    if (activeID === undefined) {
        return graph.transient(way, 'waySegments', getWaySegments);
    } else {
        return getWaySegments();
    }

    function getWaySegments() {
        var isActiveWay = (way.nodes.indexOf(activeID) !== -1);
        var features = { passive: [], active: [] };
        var start = {};
        var end = {};
        var node, type;

        for (var i = 0; i < way.nodes.length; i++) {
            node = graph.entity(way.nodes[i]);
            type = svgPassiveVertex(node, graph, activeID);
            end = { node: node, type: type };

            if (start.type !== undefined) {
                if (start.node.id === activeID || end.node.id === activeID) {
                    // push nothing
                } else if (isActiveWay && (start.type === 2 || end.type === 2)) {   // one adjacent vertex
                    pushActive(start, end, i);
                } else if (start.type === 0 && end.type === 0) {   // both active vertices
                    pushActive(start, end, i);
                } else {
                    pushPassive(start, end, i);
                }
            }

            start = end;
        }

        return features;

        function pushActive(start, end, index) {
            features.active.push({
                type: 'Feature',
                id: way.id + '-' + index + '-nope',
                properties: {
                    nope: true,
                    target: true,
                    entity: way,
                    nodes: [start.node, end.node],
                    index: index
                },
                geometry: {
                    type: 'LineString',
                    coordinates: [start.node.loc, end.node.loc]
                }
            });
        }

        function pushPassive(start, end, index) {
            features.passive.push({
                type: 'Feature',
                id: way.id + '-' + index,
                properties: {
                    target: true,
                    entity: way,
                    nodes: [start.node, end.node],
                    index: index
                },
                geometry: {
                    type: 'LineString',
                    coordinates: [start.node.loc, end.node.loc]
                }
            });
        }
    }
}


/**
 * Returns a d3 projection stream that clips the given geometries to an
 * extent that is slightly padded.
 *
 * Explanation of magic numbers:
 * "padding" here allows space for strokes to extend beyond the viewport,
 * so that the stroke isn't drawn along the edge of the viewport when
 * the shape is clipped.
 * When drawing lines, pad viewport by 5px.
 * When drawing areas, pad viewport by 65px in each direction to allow
 * for 60px area fill stroke (see ".fill-partial path.fill" css rule)
 *
 * @param {import('../geo/raw_mercator').Projection} projection
 * @param {Boolean} isArea
 */
function paddedClipExtent(projection, isArea = false) {
    var padding = isArea ? 65 : 5;
    var viewport = projection.clipExtent();
    var paddedExtent = [
        [viewport[0][0] - padding, viewport[0][1] - padding],
        [viewport[1][0] + padding, viewport[1][1] + padding]
    ];
    return d3_geoIdentity().clipExtent(paddedExtent).stream;
}
