package org.optaweb.vehiclerouting.domain;

import static java.util.stream.Collectors.toList;

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

/**
 * Vehicle's itinerary (sequence of visits) and its depot. This entity cannot exist without the vehicle and the depot
 * but it's allowed to have no visits when the vehicle hasn't been assigned any (it's idle).
 * <p>
 * This entity describes part of a {@link RoutingPlan solution} of the vehicle routing problem
 * (assignment of a subset of visits to one of the vehicles).
 * It doesn't carry the data about physical tracks between adjacent visits.
 * Geographical data is held by {@link RouteWithTrack}.
 */
public class Route {

    private final Vehicle vehicle;
    private final Location depot;
    private final List<Location> visits;

    /**
     * Create a vehicle route.
     *
     * @param vehicle the vehicle assigned to this route (not {@code null})
     * @param depot vehicle's depot (not {@code null})
     * @param visits list of visits (not {@code null})
     */
    public Route(Vehicle vehicle, Location depot, List<Location> visits) {
        this.vehicle = Objects.requireNonNull(vehicle);
        this.depot = Objects.requireNonNull(depot);
        this.visits = new ArrayList<>(Objects.requireNonNull(visits));
        // TODO Probably remove this check when we have more types: new Route(Depot depot, List<Visit> visits).
        //      Then visits obviously cannot contain the depot. But will we still require that no visit has the same
        //      location as the depot? (I don't think so).
        if (visits.contains(depot)) {
            throw new IllegalArgumentException("Depot (" + depot + ") must not be one of the visits (" + visits + ")");
        }
        long uniqueVisits = visits.stream().distinct().count();
        if (uniqueVisits < visits.size()) {
            long duplicates = visits.size() - uniqueVisits;
            throw new IllegalArgumentException("Some visits have been visited multiple times (" + duplicates + ")");
        }
    }

    /**
     * The vehicle assigned to this route.
     *
     * @return route's vehicle (never {@code null})
     */
    public Vehicle vehicle() {
        return vehicle;
    }

    /**
     * Depot in which the route starts and ends.
     *
     * @return route's depot (never {@code null})
     */
    public Location depot() {
        return depot;
    }

    /**
     * List of vehicle's visits (not including the depot).
     *
     * @return list of visits
     */
    public List<Location> visits() {
        return Collections.unmodifiableList(visits);
    }

    @Override
    public String toString() {
        return "Route{" +
                "vehicle=" + vehicle +
                ", depot=" + depot.id() +
                ", visits=" + visits.stream().map(Location::id).collect(toList()) +
                '}';
    }
}
