// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.validation.tests;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.WaySegment;
import org.openstreetmap.josm.data.validation.OsmValidator;
import org.openstreetmap.josm.data.validation.Severity;
import org.openstreetmap.josm.data.validation.Test;
import org.openstreetmap.josm.data.validation.TestError;
import org.openstreetmap.josm.data.validation.util.ValUtil;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.tools.Logging;

/**
 * Tests if there are segments that crosses in the same layer
 *
 * @author frsantos
 */
public abstract class CrossingWays extends Test {
    protected static final int CROSSING_WAYS = 601;

    static final String HIGHWAY = "highway";
    static final String RAILWAY = "railway";
    static final String WATERWAY = "waterway";

    /** All way segments, grouped by cells */
    private final Map<Point2D, List<WaySegment>> cellSegments = new HashMap<>(1000);
    /** The already detected ways in error */
    private final Map<List<Way>, List<WaySegment>> seenWays = new HashMap<>(50);

    /**
     * General crossing ways test.
     */
    public static class Ways extends CrossingWays {

        /**
         * Constructs a new crossing {@code Ways} test.
         */
        public Ways() {
            super(tr("Crossing ways"));
        }

        @Override
        public boolean isPrimitiveUsable(OsmPrimitive w) {
            return super.isPrimitiveUsable(w)
                    && !isProposedOrAbandoned(w)
                    && (isHighway(w)
                    || w.hasKey(WATERWAY)
                    || isRailway(w)
                    || isCoastline(w)
                    || isBuilding(w));
        }

        @Override
        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
            if (w1 == w2)
                return false;
            if (!Objects.equals(getLayer(w1), getLayer(w2))) {
                return true;
            }
            if (w1.hasKey(HIGHWAY) && w2.hasKey(HIGHWAY) && !Objects.equals(w1.get("level"), w2.get("level"))) {
                return true;
            }
            if (isSubwayOrTramOrRazed(w2)) {
                return true;
            }
            if (isCoastline(w1) != isCoastline(w2)) {
                return true;
            }
            if ((w1.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w2.hasTag(WATERWAY, "riverbank"))
             || (w2.hasTag(WATERWAY, "river", "stream", "canal", "drain", "ditch") && w1.hasTag(WATERWAY, "riverbank"))) {
                return true;
            }
            return isProposedOrAbandoned(w2);
        }

        @Override
        String createMessage(Way w1, Way w2) {
            if (isBuilding(w1)) {
                return tr("Crossing buildings");
            } else if (w1.hasKey(WATERWAY) && w2.hasKey(WATERWAY)) {
                return tr("Crossing waterways");
            } else if ((w1.hasKey(HIGHWAY) && w2.hasKey(WATERWAY))
                    || (w2.hasKey(HIGHWAY) && w1.hasKey(WATERWAY))) {
                return tr("Crossing waterway/highway");
            } else {
                return tr("Crossing ways");
            }
        }
    }

    /**
     * Crossing boundaries ways test.
     */
    public static class Boundaries extends CrossingWays {

        /**
         * Constructs a new crossing {@code Boundaries} test.
         */
        public Boundaries() {
            super(tr("Crossing boundaries"));
        }

        @Override
        public boolean isPrimitiveUsable(OsmPrimitive p) {
            return super.isPrimitiveUsable(p) && p.hasKey("boundary")
                    && (!(p instanceof Relation) || (((Relation) p).isMultipolygon() && !((Relation) p).hasIncompleteMembers()));
        }

        @Override
        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
            return !Objects.equals(w1.get("boundary"), w2.get("boundary"));
        }

        @Override
        String createMessage(Way w1, Way w2) {
            return tr("Crossing boundaries");
        }

        @Override
        public void visit(Relation r) {
            for (Way w : r.getMemberPrimitives(Way.class)) {
                visit(w);
            }
        }
    }

    /**
     * Crossing barriers ways test.
     */
    public static class Barrier extends CrossingWays {

        /**
         * Constructs a new crossing {@code Barrier} test.
         */
        public Barrier() {
            super(tr("Crossing barriers"));
        }

        @Override
        public boolean isPrimitiveUsable(OsmPrimitive p) {
            return super.isPrimitiveUsable(p) && p.hasKey("barrier");
        }

        @Override
        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
            return !Objects.equals(getLayer(w1), getLayer(w2));
        }

        @Override
        String createMessage(Way w1, Way w2) {
            return tr("Crossing barriers");
        }
    }

    /**
     * Self crossing ways test (for all the rest)
     */
    public static class SelfCrossing extends CrossingWays {
        CrossingWays.Ways normalTest = new Ways();
        CrossingWays.Barrier barrierTest = new Barrier();
        CrossingWays.Boundaries boundariesTest = new Boundaries();

        /**
         * Constructs a new SelfIntersection test.
         */
        public SelfCrossing() {
            super(tr("Self crossing"));
        }

        @Override
        public boolean isPrimitiveUsable(OsmPrimitive p) {
            return super.isPrimitiveUsable(p) && !(normalTest.isPrimitiveUsable(p) || barrierTest.isPrimitiveUsable(p)
                    || boundariesTest.isPrimitiveUsable(p));
        }

        @Override
        boolean ignoreWaySegmentCombination(Way w1, Way w2) {
            return w1 != w2; // should not happen
        }

        @Override
        String createMessage(Way w1, Way w2) {
            return tr("Self-crossing ways");
        }
    }

    /**
     * Constructs a new {@code CrossingWays} test.
     * @param title The test title
     * @since 6691
     */
    public CrossingWays(String title) {
        super(title, tr("This test checks if two roads, railways, waterways or buildings crosses in the same layer, " +
                "but are not connected by a node."));
    }

    @Override
    public void startTest(ProgressMonitor monitor) {
        super.startTest(monitor);
        cellSegments.clear();
        seenWays.clear();
    }

    @Override
    public void endTest() {
        super.endTest();
        cellSegments.clear();
        seenWays.clear();
    }

    static String getLayer(OsmPrimitive w) {
        String layer1 = w.get("layer");
        if ("0".equals(layer1)) {
            layer1 = null; // 0 is default value for layer.
        }
        return layer1;
    }

    static boolean isCoastline(OsmPrimitive w) {
        return w.hasTag("natural", "water", "coastline") || w.hasTag("landuse", "reservoir");
    }

    static boolean isHighway(OsmPrimitive w) {
        return w.hasTagDifferent(HIGHWAY, "rest_area", "services");
    }

    static boolean isRailway(OsmPrimitive w) {
        return w.hasKey(RAILWAY) && !isSubwayOrTramOrRazed(w);
    }

    static boolean isSubwayOrTramOrRazed(OsmPrimitive w) {
        return w.hasTag(RAILWAY, "subway", "tram", "razed");
    }

    static boolean isProposedOrAbandoned(OsmPrimitive w) {
        return w.hasTag(HIGHWAY, "proposed") || w.hasTag(RAILWAY, "proposed", "abandoned");
    }

    abstract boolean ignoreWaySegmentCombination(Way w1, Way w2);

    abstract String createMessage(Way w1, Way w2);

    @Override
    public void visit(Way w) {
        if (this instanceof SelfCrossing) {
            // free memory, we are not interested in previous ways
            cellSegments.clear();
            seenWays.clear();
        }

        int nodesSize = w.getNodesCount();
        for (int i = 0; i < nodesSize - 1; i++) {
            final WaySegment es1 = new WaySegment(w, i);
            final EastNorth en1 = es1.getFirstNode().getEastNorth();
            final EastNorth en2 = es1.getSecondNode().getEastNorth();
            if (en1 == null || en2 == null) {
                Logging.warn("Crossing ways test skipped "+es1);
                continue;
            }
            for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) {
                for (WaySegment es2 : segments) {
                    List<Way> prims;
                    List<WaySegment> highlight;

                    if (!es1.intersects(es2) || ignoreWaySegmentCombination(es1.way, es2.way)) {
                        continue;
                    }

                    prims = new ArrayList<>();
                    prims.add(es1.way);
                    if (es1.way != es2.way)
                        prims.add(es2.way);
                    if ((highlight = seenWays.get(prims)) == null) {
                        highlight = new ArrayList<>();
                        highlight.add(es1);
                        highlight.add(es2);

                        final String message = createMessage(es1.way, es2.way);
                        errors.add(TestError.builder(this, Severity.WARNING, CROSSING_WAYS)
                                .message(message)
                                .primitives(prims)
                                .highlightWaySegments(highlight)
                                .build());
                        seenWays.put(prims, highlight);
                    } else {
                        highlight.add(es1);
                        highlight.add(es2);
                    }
                }
                segments.add(es1);
            }
        }
    }

    /**
     * Returns all the cells this segment crosses.  Each cell contains the list
     * of segments already processed
     * @param cellSegments map with already collected way segments
     * @param n1 The first EastNorth
     * @param n2 The second EastNorth
     * @return A list with all the cells the segment crosses
     */
    public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, EastNorth n1, EastNorth n2) {

        List<List<WaySegment>> cells = new ArrayList<>();
        for (Point2D cell : ValUtil.getSegmentCells(n1, n2, OsmValidator.getGridDetail())) {
            List<WaySegment> segments = cellSegments.get(cell);
            if (segments == null) {
                segments = new ArrayList<>();
                cellSegments.put(cell, segments);
            }
            cells.add(segments);
        }
        return cells;
    }
}
