package seng302.Model;

import java.util.Comparator;
import java.util.List;

/**
 * GPS Coordinate for the world map.
 * Created by esa46 on 15/03/17.
 */
public class GPSCoordinate {

    private double latitude;
    private double longitude;

    /**
     * Constructor Method
     *
     * @param latitude  latitude the coordinate is located at.
     * @param longitude Longitude that the coordinate is located at.
     */
    public GPSCoordinate(double latitude, double longitude) {
        this.latitude = latitude;
        this.longitude = longitude;
    }

    /**
     * Gets the Latitude that the Coordinate is at.
     *
     * @return Returns the latitude of the Coordinate.
     */
    public double getLatitude() {
        return latitude;
    }

    /**
     * Gets the Longitude that the Coordinate is at.
     *
     * @return Returns the longitude of the Coordinate.
     */
    public double getLongitude() {
        return longitude;
    }

    /**
     * To String method of the Coordinate in the form Latitude: $f, Longitude: $f.
     *
     * @return A String representation of the GPSCoordinate Class.
     */
    public String toString() {
        return String.format("Latitude: %f, Longitude: %f", latitude, longitude);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        GPSCoordinate that = (GPSCoordinate) o;

        if (Math.abs(this.latitude - that.latitude) > 1e-7) return false;
        return (Math.abs(this.longitude - that.longitude) < 1e-7);
    }

    @Override
    public int hashCode() {
        int result;
        long temp;
        temp = Double.doubleToLongBits(latitude);
        result = (int) (temp ^ (temp >>> 32));
        temp = Double.doubleToLongBits(longitude);
        result = 31 * result + (int) (temp ^ (temp >>> 32));
        return result;
    }

    /**
     * Calculates min and max values and passed it to calculate if coordinate is in the boundary
     * @param coordinate coordinate of interest
     * @param boundary List of points which make a boundry
     * @return true if coordinate is in the boundary
     */
    public static boolean isInsideBoundary(GPSCoordinate coordinate, List<GPSCoordinate> boundary) {
        double maxLatitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude();
        double maxLongitude = boundary.stream().max(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude();
        double minLatitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLatitude)).get().getLatitude();
        double minLongitude = boundary.stream().min(Comparator.comparingDouble(GPSCoordinate::getLongitude)).get().getLongitude();
        return isInsideBoundary(coordinate, boundary, new GPSCoordinate(minLatitude, minLongitude),
                new GPSCoordinate(maxLatitude, maxLongitude));
    }

    /**
     * Calculates if the coordinate is in the boundary
     * @param coordinate coordinate of interest
     * @param boundary List of points which make a boundary
     * @param minValues max GPS
     * @param maxValues min GPS
     * @return true if coordinate is in the boundary
     */
    public static boolean isInsideBoundary(GPSCoordinate coordinate, List<GPSCoordinate> boundary, GPSCoordinate minValues, GPSCoordinate maxValues) {
        double minLat = minValues.getLatitude();
        double minLon = minValues.getLongitude();
        double maxLat = maxValues.getLatitude();
        double maxLon = maxValues.getLongitude();
        double coordinateLat = coordinate.getLatitude();
        double coordinateLon = coordinate.getLongitude();
        int length = boundary.size();

        boolean inside = false;

        // End computation early
        if (coordinateLat <= minLat || coordinateLat >= maxLat || coordinateLon <= minLon || coordinateLon >= maxLon) {
            return false;
        }

        // Check if inside using ray casting algorithm
        for (int i = 0, j = length - 1; i < length; j = i++) {
            if (intersects(boundary.get(i), boundary.get(j), coordinate)) {
                inside = !inside;
            }


        }
        return inside;
    }

    /**
     * Helper function to find if a point is in a boundary
     * @param boundaryA
     * @param boundaryB
     * @param coordinate
     * @return true if a line from the point intersects the two boundary points
     */
    private static boolean intersects(GPSCoordinate boundaryA, GPSCoordinate boundaryB, GPSCoordinate coordinate) {
        double boundaryALat = boundaryA.getLatitude();
        double boundaryALon = boundaryA.getLongitude();
        double boundaryBLat = boundaryB.getLatitude();
        double boundaryBLon = boundaryB.getLongitude();
        double coordinateLat = coordinate.getLatitude();
        double coordinateLon = coordinate.getLongitude();

        if (boundaryALat > boundaryBLat) {
            return intersects(boundaryB, boundaryA, coordinate);
        }
        if (coordinateLat == boundaryALat || coordinateLat == boundaryBLat) {
            // Move coordinate off intersection line
            coordinateLat += 1e-9;
        }
        if (coordinateLat > boundaryBLat || coordinateLat < boundaryALat || coordinateLon > Double.max(boundaryALon, boundaryBLon)) {
            return false;
        }
        if (coordinateLon < Double.min(boundaryALon, boundaryBLon)) {
            return true;
        }

        double aRatio = (coordinateLat - boundaryALat) / (coordinateLon - boundaryALon);
        double bRatio = (coordinateLat - boundaryBLat) / (coordinateLon - boundaryBLon);
        return aRatio >= bRatio;

    }
}

