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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.openstreetmap.josm.plugins.turnlanes.gui.RoadGui.ViaConnector;
import org.openstreetmap.josm.plugins.turnlanes.model.Junction;
import org.openstreetmap.josm.plugins.turnlanes.model.Lane;
import org.openstreetmap.josm.plugins.turnlanes.model.Road;

abstract class State {
    static class AllTurns extends State {
        private final State wrapped;

        AllTurns(State wrapped) {
            this.wrapped = wrapped;
        }

        public State unwrap() {
            return wrapped;
        }

        @Override
        State carryOver(GuiContainer newContainer) {
            return new AllTurns(wrapped.carryOver(newContainer));
        }
    }

    static class Connecting extends State {
        private final Lane lane;
        private final List<RoadGui.ViaConnector> vias;

        Connecting(Lane lane) {
            this(lane, Collections.<RoadGui.ViaConnector>emptyList());
        }

        Connecting(Lane lane, List<ViaConnector> vias) {
            this.lane = lane;
            this.vias = vias;
        }

        public Connecting next(RoadGui.ViaConnector via) {
            if (vias.isEmpty()) {
                return new Connecting(lane, Collections.unmodifiableList(Arrays.asList(via)));
            }

            final List<RoadGui.ViaConnector> tmp = new ArrayList<>(vias.size() + 1);
            final boolean even = (vias.size() & 1) == 0;
            final RoadGui.ViaConnector last = vias.get(vias.size() - 1);

            if (last.equals(via) || (!even && last.getRoadEnd().getJunction().equals(via.getRoadEnd().getJunction()))) {
                return pop().next(via);
            }

            if (vias.size() >= 2) {
                if (lane.getOutgoingJunction().equals(via.getRoadEnd().getJunction())) {
                    return new Connecting(lane);
                } else if (via.equals(getBacktrackViaConnector())) {
                    return new Connecting(lane, vias.subList(0, vias.size() - 1));
                }
            }

            for (RoadGui.ViaConnector v : vias) {
                tmp.add(v);

                if (!(even && v.equals(last)) && v.getRoadEnd().getJunction().equals(via.getRoadEnd().getJunction())) {
                    return new Connecting(lane, Collections.unmodifiableList(tmp));
                }
            }

            tmp.add(via);
            return new Connecting(lane, Collections.unmodifiableList(tmp));
        }

        public Junction getJunction() {
            return vias.isEmpty() ? lane.getOutgoingJunction() : vias.get(vias.size() - 1).getRoadEnd().getJunction();
        }

        public RoadGui.ViaConnector getBacktrackViaConnector() {
            return vias.size() < 2 ? null : vias.get(vias.size() - 2);
        }

        public List<RoadGui.ViaConnector> getViaConnectors() {
            return vias;
        }

        public Lane getLane() {
            return lane;
        }

        public Connecting pop() {
            return new Connecting(lane, vias.subList(0, vias.size() - 1));
        }
    }

    static class Invalid extends State {
        private final State wrapped;

        Invalid(State wrapped) {
            this.wrapped = wrapped;
        }

        public State unwrap() {
            return wrapped;
        }
    }

    static class Dirty extends State {
        private final State wrapped;

        Dirty(State wrapped) {
            this.wrapped = wrapped;
        }

        public State unwrap() {
            return wrapped;
        }

        @Override
        State carryOver(GuiContainer newContainer) {
            return new Dirty(wrapped.carryOver(newContainer));
        }
    }

    static class Default extends State {
        Default() {}
    }

    static class IncomingActive extends State {
        private final Road.End roadEnd;

        IncomingActive(Road.End roadEnd) {
            this.roadEnd = roadEnd;
        }

        public Road.End getRoadEnd() {
            return roadEnd;
        }

        @Override
        State carryOver(GuiContainer newContainer) {
            if (newContainer.getModel().equals(roadEnd.getRoad().getContainer())) {
                return this;
            }

            final Junction newJunction = newContainer.getModel().getJunction(roadEnd.getJunction().getNode());

            for (Road.End e : newJunction.getRoadEnds()) {
                if (e.isToEnd() && e.getWay().equals(roadEnd.getWay())) {
                    return new IncomingActive(e);
                }
            }

            return new Default();
        }
    }

    static class OutgoingActive extends State {
        private final LaneGui lane;

        OutgoingActive(LaneGui lane) {
            this.lane = lane;
        }

        public LaneGui getLane() {
            return lane;
        }

        @Override
        State delete() {
            if (!lane.getModel().isExtra()) {
                return this;
            }

            lane.getModel().remove();

            return new Invalid(this);
        }

        @Override
        State carryOver(GuiContainer newContainer) {
            if (newContainer.equals(lane.getContainer())) {
                return this;
            }

            final Lane model = lane.getModel();
            final Junction newJunction = newContainer.getModel().getJunction(model.getOutgoingJunction().getNode());

            for (Road.End e : newJunction.getRoadEnds()) {
                if (e.isToEnd() && e.getWay().equals(model.getOutgoingRoadEnd().getWay())) {
                    for (Lane l : e.getLanes()) { // e.getLane(...) can fail on lane removal
                        if (l.getKind() == model.getKind() && l.getIndex() == model.getIndex()) {
                            return new OutgoingActive(newContainer.getGui(l));
                        }
                    }

                    break;
                }
            }

            return new Default();
        }
    }

    State delete() {
        return this;
    }

    State carryOver(GuiContainer newContainer) {
        return this;
    }
}
