import { deepEqual } from 'fast-equals';

import { actionAddVertex } from '../actions/add_vertex';
import { actionChangeTags } from '../actions/change_tags';
import { actionMergeNodes } from '../actions/merge_nodes';
import { actionExtract } from '../actions/extract';
import { modeSelect } from '../modes/select';
import { osmJoinWays } from '../osm/multipolygon';
import { osmNodeGeometriesForTags, osmTagSuggestingArea } from '../osm/tags';
import { presetManager } from '../presets';
import { geoHasSelfIntersections, geoSphericalDistance } from '../geo';
import { t } from '../core/localizer';
import { utilTagText } from '../util';
import { utilDisplayLabel } from '../util/utilDisplayLabel';
import { validationIssue, validationIssueFix } from '../core/validation';


export function validationMismatchedGeometry() {
    var type = 'mismatched_geometry';

    function tagSuggestingLineIsArea(entity) {
        if (entity.type !== 'way' || entity.isClosed()) return null;

        var tagSuggestingArea = entity.tagSuggestingArea();

        if (!tagSuggestingArea) {
            return null;
        }

        var asLine = presetManager.matchTags(tagSuggestingArea, 'line');
        var asArea = presetManager.matchTags(tagSuggestingArea, 'area');
        if (asLine && asArea && deepEqual(asLine.tags, asArea.tags)) {
            // this tag also allows lines and making this an area wouldn't matter
            return null;
        }

        if (asLine.isFallback() && asArea.isFallback() && !deepEqual(tagSuggestingArea, { area: 'yes' })) {
            // if the entity matches the fallback preset, regardless of the
            // geometry, then changing the geometry will not help.
            return null;
        }

        return tagSuggestingArea;
    }


    function makeConnectEndpointsFixOnClick(way, graph) {
        // must have at least three nodes to close this automatically
        if (way.nodes.length < 3) return null;

        var nodes = graph.childNodes(way), testNodes;
        var firstToLastDistanceMeters = geoSphericalDistance(nodes[0].loc, nodes[nodes.length-1].loc);

        // if the distance is very small, attempt to merge the endpoints
        if (firstToLastDistanceMeters < 0.75) {
            testNodes = nodes.slice();   // shallow copy
            testNodes.pop();
            testNodes.push(testNodes[0]);
            // make sure this will not create a self-intersection
            if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {
                return function(context) {
                    var way = context.entity(this.issue.entityIds[0]);
                    context.perform(
                        actionMergeNodes([way.nodes[0], way.nodes[way.nodes.length-1]], nodes[0].loc),
                        t('issues.fix.connect_endpoints.annotation')
                    );
                };
            }
        }

        // if the points were not merged, attempt to close the way
        testNodes = nodes.slice();   // shallow copy
        testNodes.push(testNodes[0]);
        // make sure this will not create a self-intersection
        if (!geoHasSelfIntersections(testNodes, testNodes[0].id)) {
            return function(context) {
                var wayId = this.issue.entityIds[0];
                var way = context.entity(wayId);
                var nodeId = way.nodes[0];
                var index = way.nodes.length;
                context.perform(
                    actionAddVertex(wayId, nodeId, index),
                    t('issues.fix.connect_endpoints.annotation')
                );
            };
        }
    }

    function areaTaggedAsLineIssue(entity) {

        var tagSuggestingArea = tagSuggestingLineIsArea(entity);
        if (!tagSuggestingArea) return null;

        var validAsLine = false;
        var presetAsLine = presetManager.matchTags(entity.tags, 'line');
        if (presetAsLine) {
            validAsLine = true;
            var key = Object.keys(tagSuggestingArea)[0];
            if (presetAsLine.tags[key] && presetAsLine.tags[key] === '*') {
                // only matches a fallback preset of the tag which is suggesting to be an area
                validAsLine = false;
            }
            if (Object.keys(presetAsLine.tags).length === 0) {
                // only matches the fallback "line" preset
                validAsLine = false;
            }
        }

        return new validationIssue({
            type: type,
            subtype: 'area_as_line',
            severity: 'warning',
            message: function(context) {
                var entity = context.hasEntity(this.entityIds[0]);
                return entity ? t.append('issues.tag_suggests_area.message', {
                    feature: utilDisplayLabel(entity, 'area', true /* verbose */),
                    tag: utilTagText({ tags: tagSuggestingArea })
                }) : '';
            },
            reference: showReference,
            entityIds: [entity.id],
            hash: JSON.stringify(tagSuggestingArea),
            dynamicFixes: function(context) {

                var fixes = [];

                var entity = context.entity(this.entityIds[0]);
                var connectEndsOnClick = makeConnectEndpointsFixOnClick(entity, context.graph());

                if (!validAsLine) {
                    // only suggest to "connect the ends" if the feature is not also valid as a line
                    fixes.push(new validationIssueFix({
                        title: t.append('issues.fix.connect_endpoints.title'),
                        onClick: connectEndsOnClick
                    }));
                }

                fixes.push(new validationIssueFix({
                    icon: 'iD-operation-delete',
                    title: t.append('issues.fix.remove_tag.title'),
                    onClick: function(context) {
                        var entityId = this.issue.entityIds[0];
                        var entity = context.entity(entityId);
                        var tags = Object.assign({}, entity.tags);  // shallow copy
                        for (var key in tagSuggestingArea) {
                            delete tags[key];
                        }
                        context.perform(
                            actionChangeTags(entityId, tags),
                            t('issues.fix.remove_tag.annotation')
                        );
                    }
                }));

                return fixes;
            }
        });


        function showReference(selection) {
            selection.selectAll('.issue-reference')
                .data([0])
                .enter()
                .append('div')
                .attr('class', 'issue-reference')
                .call(t.append('issues.tag_suggests_area.reference'));
        }
    }

    function vertexPointIssue(entity, graph) {
        // we only care about nodes
        if (entity.type !== 'node') return null;

        // ignore tagless points
        if (Object.keys(entity.tags).length === 0) return null;

        // address lines are special so just ignore them
        if (entity.isOnAddressLine(graph)) return null;

        var geometry = entity.geometry(graph);
        var allowedGeometries = osmNodeGeometriesForTags(entity.tags);

        if (geometry === 'point' && !allowedGeometries.point && allowedGeometries.vertex) {

            return new validationIssue({
                type: type,
                subtype: 'vertex_as_point',
                severity: 'warning',
                message: function(context) {
                    var entity = context.hasEntity(this.entityIds[0]);
                    return entity ? t.append('issues.vertex_as_point.message', {
                        feature: utilDisplayLabel(entity, 'vertex', true /* verbose */)
                    }) : '';
                },
                reference: function showReference(selection) {
                    selection.selectAll('.issue-reference')
                        .data([0])
                        .enter()
                        .append('div')
                        .attr('class', 'issue-reference')
                        .call(t.append('issues.vertex_as_point.reference'));
                },
                entityIds: [entity.id],
                dynamicFixes: () => [new validationIssueFix({
                    icon: 'iD-operation-move',
                    title: t.append('issues.fix.reposition_point_to_vertex.title')
                })]
            });

        } else if (geometry === 'vertex' && !allowedGeometries.vertex && allowedGeometries.point) {

            return new validationIssue({
                type: type,
                subtype: 'point_as_vertex',
                severity: 'warning',
                message: function(context) {
                    var entity = context.hasEntity(this.entityIds[0]);
                    return entity ? t.append('issues.point_as_vertex.message', {
                        feature: utilDisplayLabel(entity, 'point', true /* verbose */)
                    }) : '';
                },
                reference: function showReference(selection) {
                    selection.selectAll('.issue-reference')
                        .data([0])
                        .enter()
                        .append('div')
                        .attr('class', 'issue-reference')
                        .call(t.append('issues.point_as_vertex.reference'));
                },
                entityIds: [entity.id],
                dynamicFixes: extractPointDynamicFixes
            });
        }

        return null;
    }


    function otherMismatchIssue(entity, graph) {
        // ignore boring features
        if (!entity.hasInterestingTags()) return null;

        if (entity.type !== 'node' && entity.type !== 'way') return null;

        // address lines are special so just ignore them
        if (entity.type === 'node' && entity.isOnAddressLine(graph)) return null;

        var sourceGeom = entity.geometry(graph);

        // order matters. if there are multiple valid geometries,
        // suggest way geometry for ways, and node geometry for nodes.
        var targetGeoms = entity.type === 'node'
            ? ['point', 'vertex', 'line', 'area']
            : ['line', 'area', 'point', 'vertex'];

        var asSource = presetManager.match(entity, graph);

        const targetGeom = targetGeoms.find(nodeGeom => {
            if (nodeGeom === sourceGeom) return false;

            const asTarget = presetManager.matchTags(
                entity.tags,
                nodeGeom,
                entity.extent(graph).center(),
            );
            if (!asSource || !asTarget ||
                asSource === asTarget ||
                // sometimes there are two presets with the same tags for different geometries
                deepEqual(asSource.tags, asTarget.tags)) return false;

            if (asTarget.isFallback()) return false;

            var primaryKey = Object.keys(asTarget.tags)[0];

            // special case: buildings-as-points are discouraged by iD, but common in OSM, so ignore them
            if (primaryKey === 'building') return false;

            if (asTarget.tags[primaryKey] === '*') return false;

            return asSource.isFallback() || asSource.tags[primaryKey] === '*';
        });

        if (!targetGeom) return null;

        var subtype = targetGeom + '_as_' + sourceGeom;

        var referenceId = targetGeom + '_as_' + sourceGeom;

        // reuse translations
        if (referenceId === 'vertex_as_line') referenceId = 'point_as_line';
        if (referenceId === 'vertex_as_area') referenceId = 'point_as_area';
        if (referenceId === 'line_as_vertex') referenceId = 'line_as_point';
        if (referenceId === 'area_as_vertex') referenceId = 'area_as_point';

        var dynamicFixes;
        if (targetGeom === 'point') {
            dynamicFixes = extractPointDynamicFixes;

        } else if (sourceGeom === 'area' && targetGeom === 'line') {
            dynamicFixes = lineToAreaDynamicFixes;
        }
        if (
            sourceGeom === 'line' &&
            targetGeom === 'area' &&
            entity.type === 'way' &&
            entity.isClosed()
        ) {
            // the line is already closed, but iD is not treating it
            // as a closed line. This is trivial to fix by adding area=yes.
            dynamicFixes = closedAreaToLineDynamicFixes;
        }

        return new validationIssue({
            type: type,
            subtype: subtype,
            severity: 'warning',
            message: function(context) {
                var entity = context.hasEntity(this.entityIds[0]);
                return entity ? t.append('issues.' + referenceId + '.message', {
                    feature: utilDisplayLabel(entity, targetGeom, true /* verbose */)
                }) : '';
            },
            reference: function showReference(selection) {
                selection.selectAll('.issue-reference')
                    .data([0])
                    .enter()
                    .append('div')
                    .attr('class', 'issue-reference')
                    .call(t.append('issues.mismatched_geometry.reference'));
            },
            entityIds: [entity.id],
            dynamicFixes: dynamicFixes
        });
    }

    function lineToAreaDynamicFixes(context) {

        var convertOnClick;

        var entityId = this.entityIds[0];
        var entity = context.entity(entityId);
        var tags = Object.assign({}, entity.tags);  // shallow copy
        delete tags.area;
        if (!osmTagSuggestingArea(tags)) {
            // if removing the area tag would make this a line, offer that as a quick fix
            convertOnClick = function(context) {
                var entityId = this.issue.entityIds[0];
                var entity = context.entity(entityId);
                var tags = Object.assign({}, entity.tags);  // shallow copy
                if (tags.area) {
                    delete tags.area;
                }
                context.perform(
                    actionChangeTags(entityId, tags),
                    t('issues.fix.convert_to_line.annotation')
                );
            };
        }

        return [
            new validationIssueFix({
                icon: 'iD-icon-line',
                title: t.append('issues.fix.convert_to_line.title'),
                onClick: convertOnClick
            })
        ];
    }

    function closedAreaToLineDynamicFixes() {
        const fix = new validationIssueFix({
            icon: 'iD-icon-area',
            title: t.append('issues.fix.convert_to_area.title'),
            onClick: function(context) {
                const entityId = this.issue.entityIds[0];
                const entity = context.entity(entityId);
                const newTags = {
                    ...entity.tags,
                    area: 'yes',
                };
                context.perform(
                    actionChangeTags(entityId, newTags),
                    t('issues.fix.convert_to_line.annotation')
                );
            },
        });
        return [fix];
    }

    function extractPointDynamicFixes(context) {

        var entityId = this.entityIds[0];

        var extractOnClick = null;
        if (!context.hasHiddenConnections(entityId)) {

            extractOnClick = function(context) {
                var entityId = this.issue.entityIds[0];
                var action = actionExtract(entityId, context.projection);
                context.perform(
                    action,
                    t('operations.extract.annotation', { n: 1 })
                );
                // re-enter mode to trigger updates
                context.enter(modeSelect(context, [action.getExtractedNodeID()]));
            };
        }

        return [
            new validationIssueFix({
                icon: 'iD-operation-extract',
                title: t.append('issues.fix.extract_point.title'),
                onClick: extractOnClick
            })
        ];
    }

    function unclosedMultipolygonPartIssues(entity, graph) {

        if (entity.type !== 'relation' ||
            !entity.isMultipolygon() ||
            entity.isDegenerate() ||
            // cannot determine issues for incompletely-downloaded relations
            !entity.isComplete(graph)) return [];

        var sequences = osmJoinWays(entity.members, graph);

        var issues = [];

        for (var i in sequences) {
            var sequence = sequences[i];

            if (!sequence.nodes) continue;

            var firstNode = sequence.nodes[0];
            var lastNode = sequence.nodes[sequence.nodes.length - 1];

            // part is closed if the first and last nodes are the same
            if (firstNode === lastNode) continue;

            var issue = new validationIssue({
                type: type,
                subtype: 'unclosed_multipolygon_part',
                severity: 'warning',
                message: function(context) {
                    var entity = context.hasEntity(this.entityIds[0]);
                    return entity ? t.append('issues.unclosed_multipolygon_part.message', {
                        feature: utilDisplayLabel(entity, context.graph(), true /* verbose */)
                    }) : '';
                },
                reference: showReference,
                loc: sequence.nodes[0].loc,
                entityIds: [entity.id],
                hash: sequence.map(function(way) {
                    return way.id;
                }).join()
            });
            issues.push(issue);
        }

        return issues;

        function showReference(selection) {
            selection.selectAll('.issue-reference')
                .data([0])
                .enter()
                .append('div')
                .attr('class', 'issue-reference')
                .call(t.append('issues.unclosed_multipolygon_part.reference'));
        }
    }

    var validation = function checkMismatchedGeometry(entity, graph) {
        var vertexPoint = vertexPointIssue(entity, graph);
        if (vertexPoint) return [vertexPoint];

        var areaAsLine = areaTaggedAsLineIssue(entity);
        if (areaAsLine) return [areaAsLine];

        var mismatch = otherMismatchIssue(entity, graph);
        if (mismatch) return [mismatch];

        return unclosedMultipolygonPartIssues(entity, graph);
    };

    validation.type = type;

    return validation;
}
