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

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

import java.util.ArrayList;
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.UndoRedoHandler;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
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 Turn {
    static Set<Turn> load(ModelContainer c, String role, OsmPrimitive primitive) {
        final Set<Turn> result = new HashSet<>();

        for (Relation r : org.openstreetmap.josm.tools.Utils.filteredCollection(primitive.getReferrers(), Relation.class)) {
            if (!r.isUsable() || !r.get("type").equals(Constants.TYPE_TURNS)) {
                continue;
            }

            for (RelationMember m : r.getMembers()) {
                if (m.getRole().equals(role) && m.getMember().equals(primitive)) {
                    result.addAll(load(c, r));
                }
            }
        }

        return result;
    }

    static Set<Turn> load(ModelContainer c, Relation r) {
        for (RelationMember m : r.getMembers()) {
            if (m.getRole().equals(Constants.TURN_ROLE_VIA)) {
                if (m.isNode()) {
                    return loadWithViaNode(c, r);
                } else if (m.isWay()) {
                    return loadWithViaWays(c, r);
                }
            }
        }

        throw new IllegalArgumentException("No via node or way(s).");
    }

    private static Set<Turn> loadWithViaWays(ModelContainer c, Relation r) {
        final Way from = TurnlanesUtils.getMemberWay(r, Constants.TURN_ROLE_FROM);
        final Way to = TurnlanesUtils.getMemberWay(r, Constants.TURN_ROLE_TO);

        if (!c.hasRoad(from) || !c.hasRoad(to)) {
            return Collections.emptySet();
        }

        final List<Way> tmp = TurnlanesUtils.getMemberWays(r, Constants.TURN_ROLE_VIA);
        final List<Road> via = new LinkedList<>();

        final Road.End fromRoadEnd = c.getJunction(TurnlanesUtils.lineUp(from, tmp.get(0))).getRoadEnd(from);

        Node n = fromRoadEnd.getJunction().getNode();
        final Iterator<Way> it = tmp.iterator();
        while (it.hasNext()) {
            final Way w = it.next();
            if (!c.hasRoad(w)) {
                return Collections.emptySet();
            }

            final Road v = c.getRoad(w);
            via.add(v);
            n = TurnlanesUtils.getOppositeEnd(w, n);

            if (!v.isPrimary()) {
                throw new IllegalStateException("The road is not part of the junction.");
            }

            final Iterator<Route.Segment> it2 = (v.getRoute().getFirstSegment().getWay().equals(w) ? v.getRoute()
                    .getSegments() : CollectionUtils.reverse(v.getRoute().getSegments())).iterator();
            it2.next(); // first is done

            while (it2.hasNext()) {
                final Way w2 = it2.next().getWay();
                n = TurnlanesUtils.getOppositeEnd(w2, n);

                if (!it.hasNext() || !w2.equals(it.next())) {
                    throw new IllegalStateException("The via ways of the relation do not form a road.");
                }
            }
        }
        final Road.End toRoadEnd = c.getJunction(n).getRoadEnd(to);

        final Set<Turn> result = new HashSet<>();
        for (int i : indices(r, Constants.TURN_KEY_LANES)) {
            result.add(new Turn(r, fromRoadEnd.getLane(Lane.Kind.REGULAR, i), via, toRoadEnd));
        }
        for (int i : indices(r, Constants.TURN_KEY_EXTRA_LANES)) {
            result.add(new Turn(r, fromRoadEnd.getExtraLane(i), via, toRoadEnd));
        }
        return result;
    }

    static List<Integer> indices(Relation r, String key) {
        final String joined = r.get(key);

        if (joined == null) {
            return new ArrayList<>(1);
        }

        final List<Integer> result = new ArrayList<>();
        for (String lane : Constants.SPLIT_PATTERN.split(joined)) {
            result.add(Integer.parseInt(lane));
        }

        return result;
    }

    private static Set<Turn> loadWithViaNode(ModelContainer c, Relation r) {
        final Way from = TurnlanesUtils.getMemberWay(r, Constants.TURN_ROLE_FROM);
        final Node via = TurnlanesUtils.getMemberNode(r, Constants.TURN_ROLE_VIA);
        final Way to = TurnlanesUtils.getMemberWay(r, Constants.TURN_ROLE_TO);

        if (!c.hasRoad(from) || !c.hasJunction(via) || !c.hasRoad(to)) {
            return Collections.emptySet();
        }

        final Junction j = c.getJunction(via);

        final Road.End fromRoadEnd = j.getRoadEnd(from);
        final Road.End toRoadEnd = j.getRoadEnd(to);

        final Set<Turn> result = new HashSet<>();
        for (int i : indices(r, Constants.TURN_KEY_LANES)) {
            result.add(new Turn(r, fromRoadEnd.getLane(Lane.Kind.REGULAR, i), Collections.<Road>emptyList(), toRoadEnd));
        }
        for (int i : indices(r, Constants.TURN_KEY_EXTRA_LANES)) {
            result.add(new Turn(r, fromRoadEnd.getExtraLane(i), Collections.<Road>emptyList(), toRoadEnd));
        }
        return result;
    }

    static String join(List<Integer> list) {
        if (list.isEmpty()) {
            return null;
        }

        final StringBuilder builder = new StringBuilder(list.size() * (2 + Constants.SEPARATOR.length()));

        for (int e : list) {
            builder.append(e).append(Constants.SEPARATOR);
        }

        builder.setLength(builder.length() - Constants.SEPARATOR.length());
        return builder.toString();
    }

    private final Relation relation;

    private final Lane from;
    private final List<Road> via;
    private final Road.End to;

    public Turn(Relation relation, Lane from, List<Road> via, Road.End to) {
        this.relation = relation;
        this.from = from;
        this.via = via;
        this.to = to;
    }

    public Lane getFrom() {
        return from;
    }

    public List<Road> getVia() {
        return via;
    }

    public Road.End getTo() {
        return to;
    }

    Relation getRelation() {
        return relation;
    }

    public void remove() {
        final GenericCommand cmd = new GenericCommand(relation.getDataSet(), tr("Delete turn."));

        remove(cmd);

        UndoRedoHandler.getInstance().add(cmd);
    }

    void remove(GenericCommand cmd) {
        final List<Integer> lanes = indices(relation, Constants.TURN_KEY_LANES);
        final List<Integer> extraLanes = indices(relation, Constants.TURN_KEY_EXTRA_LANES);

        // TODO understand & document
        if (lanes.size() + extraLanes.size() == 1 && (from.isExtra() ^ !lanes.isEmpty())) {
            cmd.backup(relation).setDeleted(true);
            // relation.getDataSet().removePrimitive(relation.getPrimitiveId());
        } else if (from.isExtra()) {
            extraLanes.remove(Integer.valueOf(from.getIndex()));
        } else {
            lanes.remove(Integer.valueOf(from.getIndex()));
        }

        cmd.backup(relation).put(Constants.TURN_KEY_LANES, lanes.isEmpty() ? null : join(lanes));
        cmd.backup(relation).put(Constants.TURN_KEY_EXTRA_LANES, extraLanes.isEmpty() ? null : join(extraLanes));
    }

    void fixReferences(GenericCommand cmd, boolean left, int index) {
        final List<Integer> fixed = new ArrayList<>();
        for (int i : indices(relation, Constants.TURN_KEY_EXTRA_LANES)) {
            if (left ? i < index : i > index) {
                fixed.add(left ? i + 1 : i - 1);
            } else {
                fixed.add(i);
            }
        }

        cmd.backup(relation).put(Constants.TURN_KEY_EXTRA_LANES, join(fixed));
    }
}
