// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.turnlanes.model;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.plugins.turnlanes.CollectionUtils;

public final class TurnlanesUtils {
    private static final Set<String> ROAD_HIGHWAY_VALUES = Collections.unmodifiableSet(new HashSet<>(Arrays
            .asList("motorway", "motorway_link", "trunk", "trunk_link", "primary", "primary_link", "secondary",
                    "secondary_link", "tertiary", "tertiary_link", "residential", "unclassified", "road", "living_street", "service",
                    "track", "pedestrian", "raceway", "services")));

    private TurnlanesUtils() {
        // Hide default constructor for utilities classes
    }

    public static boolean isRoad(Way w) {
        return ROAD_HIGHWAY_VALUES.contains(w.get("highway"));
    }

    public static List<Way> filterRoads(List<OsmPrimitive> of) {
        final List<Way> result = new ArrayList<>();

        for (OsmPrimitive p : of) {
            if (p.getType() == OsmPrimitiveType.WAY && isRoad((Way) p)) {
                result.add((Way) p);
            }
        }

        return result;
    }

    public static Node getMemberNode(Relation r, String role) {
        return getMember(r, role, OsmPrimitiveType.NODE).getNode();
    }

    public static Way getMemberWay(Relation r, String role) {
        return getMember(r, role, OsmPrimitiveType.WAY).getWay();
    }

    public static RelationMember getMember(Relation r, String role, OsmPrimitiveType type) {
        final List<RelationMember> candidates = getMembers(r, role, type);
        if (candidates.isEmpty()) {
            throw UnexpectedDataException.Kind.NO_MEMBER.chuck(role);
        } else if (candidates.size() > 1) {
            throw UnexpectedDataException.Kind.MULTIPLE_MEMBERS.chuck(role);
        }
        return candidates.get(0);
    }

    public static List<RelationMember> getMembers(Relation r, String role, OsmPrimitiveType type) {
        final List<RelationMember> result = getMembers(r, role);
        for (RelationMember m : getMembers(r, role)) {
            if (m.getType() != type) {
                throw UnexpectedDataException.Kind.WRONG_MEMBER_TYPE.chuck(role, m.getType(), type);
            }
        }
        return result;
    }

    public static List<RelationMember> getMembers(Relation r, String role) {
        final List<RelationMember> result = new ArrayList<>();
        for (RelationMember m : r.getMembers()) {
            if (m.getRole().equals(role)) {
                result.add(m);
            }
        }
        return result;
    }

    public static List<Node> getMemberNodes(Relation r, String role) {
        return mapMembers(getMembers(r, role, OsmPrimitiveType.NODE), Node.class);
    }

    public static List<Way> getMemberWays(Relation r, String role) {
        return mapMembers(getMembers(r, role, OsmPrimitiveType.WAY), Way.class);
    }

    private static <T> List<T> mapMembers(List<RelationMember> ms, Class<T> t) {
        final List<T> result = new ArrayList<>(ms.size());
        for (RelationMember m : ms) {
            result.add(t.cast(m.getMember()));
        }
        return result;
    }

    /**
     * Lines up given ways
     * @param a first way
     * @param b second way
     * @return the node at which {@code a} and {@code b} are connected
     */
    public static Node lineUp(Way a, Way b) {
        final Set<Node> s = new HashSet<>(Arrays.asList(a.firstNode(), a.lastNode(), b.firstNode(), b.lastNode()));
        if (a.firstNode() == a.lastNode() || b.firstNode().equals(b.lastNode()) || s.size() == 2) {
            throw new IllegalArgumentException("Cycles are not allowed.");
        } else if (s.size() == 4) {
            throw new IllegalArgumentException("Ways are not connected (at their first and last nodes).");
        }

        if (a.firstNode() == b.firstNode() || a.lastNode() == b.firstNode()) {
            return b.firstNode();
        } else if (a.firstNode() == b.lastNode() || a.lastNode() == b.lastNode()) {
            return b.lastNode();
        } else {
            throw new AssertionError();
        }
    }

    public static Node getOppositeEnd(Way w, Node n) {
        final boolean first = n.equals(w.firstNode());
        final boolean last = n.equals(w.lastNode());

        if (first && last) {
            throw new IllegalArgumentException("Way starts as well as ends at the given node.");
        } else if (first) {
            return w.lastNode();
        } else if (last) {
            return w.firstNode();
        } else {
            throw new IllegalArgumentException("Way neither starts nor ends at given node.");
        }
    }

    /**
     * Orders the {@code ways} such that the combined ways out of each returned list form a path (in
     * order) from one node out of {@code nodes} to another out of {@code nodes}.
     *
     * <ul>
     * <li>Each way is used exactly once.</li>
     * <li>Paths contain no {@code nodes} excepting the first and last nodes.</li>
     * <li>Paths contain no loops w.r.t. the ways' first and last nodes</li>
     * </ul>
     *
     * @param ways
     *            ways to be ordered
     * @param nodes
     *            start/end nodes
     * @return ordered list
     * @throws IllegalArgumentException
     *             if the ways can't be ordered
     */
    public static List<Route> orderWays(Iterable<Way> ways, Iterable<Node> nodes) {
        final List<Way> ws = new LinkedList<>(CollectionUtils.toList(ways));
        final Set<Node> ns = new HashSet<>(CollectionUtils.toList(nodes));

        final List<Route> result = new ArrayList<>();

        while (!ws.isEmpty()) {
            result.add(findPath(ws, ns));
        }

        return result;
    }

    private static Route findPath(List<Way> ws, Set<Node> ns) {
        final Way w = findPathSegment(ws, ns);
        final boolean first = ns.contains(w.firstNode());
        final boolean last = ns.contains(w.lastNode());

        if (first && last) {
            return Route.create(Arrays.asList(w), w.firstNode());
        } else if (!first && !last) {
            throw new AssertionError();
        }

        final List<Way> result = new ArrayList<>();
        result.add(w);
        Node n = first ? w.lastNode() : w.firstNode();
        while (true) {
            final Way next = findPathSegment(ws, Arrays.asList(n));
            result.add(next);
            n = getOppositeEnd(next, n);

            if (ns.contains(n)) {
                return Route.create(result, first ? w.firstNode() : w.lastNode());
            }
        }
    }

    private static Way findPathSegment(List<Way> ws, Collection<Node> ns) {
        final Iterator<Way> it = ws.iterator();

        while (it.hasNext()) {
            final Way w = it.next();

            if (ns.contains(w.firstNode()) || ns.contains(w.lastNode())) {
                it.remove();
                return w;
            }
        }

        throw new IllegalArgumentException("Ways can't be ordered.");
    }

    public static Iterable<Way> flattenVia(Node start, List<Road> via, Node end) {
        final List<Way> result = new ArrayList<>();

        Node n = start;
        for (Road r : via) {
            final Iterable<Route.Segment> segments = r.getRoute().getFirstSegment().getWay().isFirstLastNode(n) ? r
                    .getRoute().getSegments() : CollectionUtils.reverse(r.getRoute().getSegments());

            for (Route.Segment s : segments) {
                result.add(s.getWay());
                n = getOppositeEnd(s.getWay(), n);
            }
        }
        if (!end.equals(n)) {
            throw new IllegalArgumentException("The given via ways don't end at the given node.");
        }

        return result;
    }

    public static int parseIntTag(OsmPrimitive primitive, String tag) {
        final String value = primitive.get(tag);

        if (value != null) {
            try {
                return Integer.parseInt(value);
            } catch (NumberFormatException e) {
                throw UnexpectedDataException.Kind.INVALID_TAG_FORMAT.chuck(tag, value);
            }
        }

        throw UnexpectedDataException.Kind.MISSING_TAG.chuck(tag);
    }
}
