package ai.cogmission.fxmaps.model;

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

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

import com.google.gson.annotations.SerializedName;
import com.google.gson.stream.MalformedJsonException;

/**
 * Abstraction of a given path which connects a series of {@link Waypoint}s.
 * 
 * @author cogmission
 * @see Waypoint
 */
public class Route {
    
    protected ObservableList<Waypoint> observableDelegate = FXCollections.observableArrayList();
    
    @SerializedName("waypoints")
    protected List<Waypoint> delegate = new ArrayList<>();
    
    protected List<Polyline> lines = new ArrayList<>();
    
    protected String name;
    protected Waypoint origin;
    protected Waypoint destination;
    
    protected boolean interimMarkersVisible;
    
    protected String id;
    

    /** Constructs a new {@code Route} */
    public Route(String name) {
        this.name = name;
        this.id = UUID.randomUUID().toString();
    }
    
    /**
     * Returns the name of this route.
     * @return name the name of this route.
     */
    public String getName() {
        return name;
    }
    
    /**
     * Returns this {@code Route}'s UUID in String form.
     * @return  this {@code Route}'s UUID in String form.
     */
    public String getId() {
        return id;
    }
    
    /**
     * Adds a {@link Waypoint} to this list
     * @param w
     */
    public void addWaypoint(Waypoint w) {
        if(observableDelegate.isEmpty()) {
            origin = w;
        }
        destination = w;
        observableDelegate.add(w);
    }
    
    /**
     * Removes the specified {@link Waypoint} from this list.
     * @param w
     */
    public void removeWaypoint(Waypoint w) {
        int loc = observableDelegate.indexOf(w);
        if(loc > 0) {
            Polyline p = w.getConnection();
                  
            // If removing the last waypoint, make next-to-last, the destination
            if(loc == observableDelegate.size() - 1) {
                destination = observableDelegate.get(loc - 1);
            }else{ // Removing waypoint from middle of route
                int lineIdx = lines.indexOf(p);
                Polyline nextLine = lines.get(lineIdx + 1);
                Waypoint prevWaypoint = observableDelegate.get(loc - 1);
                nextLine.getOptions().getPath().set(0, prevWaypoint.getLatLon());
                createUnderlying();
            }
            removeLine(p);
        }
        observableDelegate.remove(w);
    }
    
    /**
     * Removes all {@link Waypoint}s from this list.
     */
    public void removeAllWaypoints() {
        observableDelegate.clear();
        lines.clear();
    }
    
    /**
     * Returns the element at the specified index or 
     * throws an {@link IndexOutOfBoundsException}
     * 
     * @param index     the index of the Waypoint to return.
     * @return  the element at the specified index.
     * @throws  IndexOutOfBoundsException if index > size - 1
     */
    public Waypoint getWaypoint(int index) {
        return observableDelegate.get(index);
    }
    
    /**
     * Adds the specified {@link Waypoint} to this list at the
     * specified index.
     * 
     * @param index     the index to add the specified Waypoint
     * @param w         the Waypoint to add
     */
    public void addWaypoint(int index, Waypoint w) {
        observableDelegate.add(index, w);
    }
    
    /**
     * Adds the {@link ListChangeListener} which will be notified 
     * of changes to this list.
     * 
     * @param l     the listener to add
     */
    public void addListener(ListChangeListener<Waypoint> l) {
        observableDelegate.addListener(l);
    }
    
    /**
     * Removes the specified listener.
     * 
     * @param l     the listener to remove
     */
    public void removeListener(ListChangeListener<Waypoint> l) {
        observableDelegate.removeListener(l);
    }
    
    /**
     * Replaces the element at the specified index with the 
     * specified {@link Waypoint}.
     * 
     * @param index     the index to set
     * @param w         the Waypoint to set at the specified index.
     */
    public void setWaypoint(int index, Waypoint w) {
        observableDelegate.set(index, w);
    }
    
    /**
     * Returns the origin (starting) {@link Waypoint} or
     * null if this {@code Route} is empty. 
     * 
     * @return  the starting {@link Waypoint}
     */
    public Waypoint getOrigin() {
        return origin;
    }
    
    /**
     * Returns the destination (end point) {@link Waypoint} or
     * null if this {@code Route} is empty. Note, the returned
     * value may be the same as the origin if this route only 
     * contains 1 item.
     * 
     * @return  the end point {@link Waypoint}
     */
    public Waypoint getDestination() {
        return destination;
    }
    
    /**
     * Returns all the {@link Waypoint}s of a given route.
     *  
     * @return  a list of all {@link Waypoint}s
     */
    public List<Waypoint> getWaypoints() {
        return observableDelegate;
    }
    
    /**
     * Adds a {@link Polyline} to this {@code Route}
     * @param line  the line to add
     */
    public void addLine(Polyline line) {
        lines.add(line);
    }
    
    /**
     * Removes the specified {@link Polyline} from this {@code Route}
     * @param line  the line to remove
     */
    public void removeLine(Polyline line) {
        lines.remove(line);
    }
    
    /**
     * Returns the list of {@link Polylines} which make up the legs
     * between {@link Waypoints}
     * @return  the list of lines
     */
    public List<Polyline> getLines() {
        return lines;
    }
    
    /**
     * Returns the number of {@link Waypoint}s in this {@code Route}
     * @return  the number of Waypoints
     */
    public int size() {
        return observableDelegate.size();
    }
    
    /**
     * Returns the interim {@link Waypoint}s of a given route. These
     * are defined as the waypoints which are not the origin nor 
     * destination waypoints.
     *  
     * @return  a list of {@link Waypoint}s minus the start and end
     */
    public List<Waypoint> getInterimWaypoints() {
        if(observableDelegate == null || observableDelegate.size() < 3) return Collections.emptyList();
        
        List<Waypoint> retVal = new ArrayList<>(observableDelegate);
        retVal.remove(0);
        retVal.remove(retVal.size() - 1);
        return retVal;  
    }
    
    public boolean getInterimMarkersVisible() {
        return interimMarkersVisible;
    }
    
    public void interimMarkersVisible(boolean b) {
        this.interimMarkersVisible = b;
    }
    
    /**
     * Called prior to serialization to load the serializable data structure.
     */
    public void preSerialize() {
        delegate.addAll(observableDelegate);
    }
    
    /**
     * Called following deserialization to load the observable list. The observable
     * list cannot be serialized using the current method, so we use a simple list
     * for serialization and then copy the data over, after deserialization.
     */
    public void postDeserialize() throws MalformedJsonException {
        observableDelegate = FXCollections.observableArrayList(delegate);
        delegate.clear();
        
        try {
            createUnderlying();
        }catch(NullPointerException npe) {
            throw new MalformedJsonException(npe.getMessage());
        }
    }
    
    public void createUnderlying() {
        // If deserializing in non-headless mode, build javascript peers
        if(Platform.isFxApplicationThread()) {
            if(origin == null || origin.getMarker() == null) {
                throw new NullPointerException("Route had malformed origin");
            }
            
            origin.getMarker().createUnderlying();
            destination.getMarker().createUnderlying();
            for(Waypoint wp : observableDelegate) {
                wp.getMarker().createUnderlying();
            }
            
            for(Polyline line : lines) {
                line.createUnderlying();
            }
        }
    }
    
    /**
     * Compares the specified {@code Route} with this Route using
     * only their paths.
     * 
     * @param other     the other route to compare
     * @return  true if both route paths are equal, false if not.
     */
    public boolean pathEquals(Route other) {
        if(observableDelegate == null) {
            if(other.observableDelegate != null)
                return false;
        } else if(!observableDelegate.equals(other.observableDelegate))
            return false;
        
        return true;
    }
    
    @Override
    public String toString() {
        return "Route [name=" + name + ", origin=" + getOrigin() + ", destination=" + getDestination() + 
                ", interim=" + getInterimWaypoints() + "]";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((destination == null) ? 0 : destination.hashCode());
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        result = prime * result + ((lines == null) ? 0 : lines.hashCode());
        result = prime * result + ((observableDelegate == null) ? 0 : observableDelegate.hashCode());
        result = prime * result + ((origin == null) ? 0 : origin.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj)
            return true;
        if(obj == null)
            return false;
        if(getClass() != obj.getClass())
            return false;
        Route other = (Route)obj;
        if(destination == null) {
            if(other.destination != null)
                return false;
        } else if(!destination.equals(other.destination))
            return false;
        if(id == null) {
            if(other.id != null)
                return false;
        } else if(!id.equals(other.id))
            return false;
        if(lines == null) {
            if(other.lines != null)
                return false;
        } else if(!lines.equals(other.lines))
            return false;
        if(observableDelegate == null) {
            if(other.observableDelegate != null)
                return false;
        } else if(!observableDelegate.equals(other.observableDelegate))
            return false;
        if(origin == null) {
            if(other.origin != null)
                return false;
        } else if(!origin.equals(other.origin))
            return false;
        return true;
    }

    
}