package seng302.Model;

import org.geotools.referencing.GeodeticCalculator;
import org.opengis.geometry.DirectPosition;
import seng302.Constants;

import java.awt.geom.Point2D;
import java.util.Comparator;
import java.util.List;

/**
 * GPS Coordinate for the world map, containing a longitude and latitude.
 * Created by esa46 on 15/03/17.
 */
public class GPSCoordinate {

    /**
     * The latitude of the coordinate.
     */
    private double latitude;

    /**
     * The longitude of the coordinate.
     */
    private double longitude;



    /**
     * Constructs a GPSCoordinate from a latitude and longitude value.
     * @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) {
        int length = boundary.size();

        boolean inside = 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;

    }

    /**
     * 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();
        // End computation early
        if (coordinateLat <= minLat || coordinateLat >= maxLat || coordinateLon <= minLon || coordinateLon >= maxLon) {
            return false;
        } else {
            return isInsideBoundary(coordinate, boundary);
        }
    }


    /**
     * Helper function to find if a point is in a boundary
     * @param boundaryA The first coordinate of the boundary.
     * @param boundaryB The second coordinate of the bounary.
     * @param coordinate The coordinate to test.
     * @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;

    }



    /**
     * Calculates the azimuth between two points.
     * @param start The starting point.
     * @param end The ending point.
     * @return The azimuth from the start point to the end point.
     */
    public static Azimuth calculateAzimuth(GPSCoordinate start, GPSCoordinate end) {

        GeodeticCalculator calc = new GeodeticCalculator();

        calc.setStartingGeographicPoint(start.getLongitude(), start.getLatitude());
        calc.setDestinationGeographicPoint(end.getLongitude(), end.getLatitude());

        return Azimuth.fromDegrees(calc.getAzimuth());
    }


    /**
     * Calculates the bearing between two points.
     * @param start The starting point.
     * @param end The ending point.
     * @return The Bearing from the start point to the end point.
     */
    public static Bearing calculateBearing(GPSCoordinate start, GPSCoordinate end) {

        //Calculates the azimuth between the two points.
        Azimuth azimuth = GPSCoordinate.calculateAzimuth(start, end);

        //And converts it into a bearing.
        return Bearing.fromAzimuth(azimuth);
    }


    /**
     * Calculates the distance, in meters, between two points.
     * Note: all other distance calculations (e.g., nautical miles between two points) should use this, and convert from meters to their desired unit.
     * @param start The starting point.
     * @param end The ending point.
     * @return The distance, in meters, between the two given points.
     */
    public static double calculateDistanceMeters(GPSCoordinate start, GPSCoordinate end) {

        GeodeticCalculator calc = new GeodeticCalculator();

        calc.setStartingGeographicPoint(start.getLongitude(), start.getLatitude());
        calc.setDestinationGeographicPoint(end.getLongitude(), end.getLatitude());

        double distanceMeters = calc.getOrthodromicDistance();

        return  distanceMeters;
    }


    /**
     * Calculates the distance, in nautical miles, between two points.
     * @param start The starting point.
     * @param end The ending point.
     * @return The distance, in nautical miles, between the two given points.
     */
    public static double calculateDistanceNauticalMiles(GPSCoordinate start, GPSCoordinate end) {
        //Find distance in meters.
        double distanceMeters = GPSCoordinate.calculateDistanceMeters(start, end);

        //Convert to nautical miles.
        double distanceNauticalMiles = distanceMeters / Constants.NMToMetersConversion;

        return distanceNauticalMiles;
    }


    /**
     * Calculates the GPS position an entity will be at, given a starting position, distance (in meters), and an azimuth.
     *
     * @param oldCoordinates GPS coordinates of the entity's starting position.
     * @param distanceMeters The distance in meters.
     * @param azimuth The entity's current azimuth.
     * @return The entity's new coordinate.
     */
    public static GPSCoordinate calculateNewPosition(GPSCoordinate oldCoordinates, double distanceMeters, Azimuth azimuth) {


        GeodeticCalculator calc = new GeodeticCalculator();

        //Set starting position.
        calc.setStartingGeographicPoint(oldCoordinates.getLongitude(), oldCoordinates.getLatitude());

        //Set direction.
        calc.setDirection(azimuth.degrees(), distanceMeters);

        //Get the destination.
        Point2D destinationPoint = calc.getDestinationGeographicPoint();

        return new GPSCoordinate(destinationPoint.getY(), destinationPoint.getX());
    }


    /**
     * Calculates the average coordinate of two coordinates.
     * @param point1 The first coordinate to average.
     * @param point2 The second coordinate to average.
     * @return The average of the two coordinates.
     */
    public static GPSCoordinate calculateAverageCoordinate(GPSCoordinate point1, GPSCoordinate point2) {

        //Calculate distance between them.
        double distanceMeters = GPSCoordinate.calculateDistanceMeters(point1, point2);
        //We want the average, so get half the distance between points.
        distanceMeters = distanceMeters / 2d;

        //Calculate azimuth between them.
        Azimuth azimuth = GPSCoordinate.calculateAzimuth(point1, point2);

        //Calculate the middle coordinate.
        GPSCoordinate middleCoordinate = GPSCoordinate.calculateNewPosition(point1, distanceMeters, azimuth);

        return middleCoordinate;

    }

}

