package edu.vtc.cis3720;

import java.io.*;
import java.util.Objects;
import java.util.Scanner;
import java.util.regex.MatchResult;

public final class GPS {
    private double longitude;  // (-180.0, +180.0]
    private double latitude;   // [ -90.0,  +90.0]

    public static class InvalidGPSValueException extends Exception
    {
        private double longitude;
        private double latitude;

        public InvalidGPSValueException(double badLongitude, double badLatitude) {
            longitude = badLongitude;
            latitude = badLatitude;
        }

        @Override
        public String toString() {
            return "invalid value: GPS{longitude=" + longitude + ", latitude=" + latitude + "}";
        }
    }

    public GPS(double longitude, double latitude)
            throws InvalidGPSValueException
    {
        if ((-180.0 < longitude && longitude <= 180.0) &&
                (-90.0 <= latitude && latitude <= 90.0)) {
            this.longitude = longitude;
            this.latitude = latitude;
        }
        else {
            throw new InvalidGPSValueException(longitude, latitude);
        }
    }

    public double getLongitude() {
        return longitude;
    }

    public double getLatitude() {
        return latitude;
    }


    private static boolean isValidLongitude(int degrees, int minutes, double seconds, char lonDirection)
    {
        if (degrees <   0 || degrees >  180) return false;
        if (minutes <   0 || minutes >=  60) return false;
        if (seconds < 0.0 || seconds >=  60.0) return false;
        if (degrees == 180 && !(minutes == 0 && seconds == 0.0)) return false;
        if (degrees == 180 && lonDirection == 'W') return false;
        return true;
    }


    private static boolean isValidLatitude(int degrees, int minutes, double seconds)
    {
        if (degrees <   0 || degrees >   90) return false;
        if (minutes <   0 || minutes >=  60) return false;
        if (seconds < 0.0 || seconds >=  60.0) return false;
        if (degrees == 90 && !(minutes == 0 && seconds == 0.0)) return false;
        return true;
    }


    public static GPS inputGPS(
            String prompt, InputStream in, PrintStream out, PrintStream error)
            throws IOException, InvalidGPSValueException
    {
        // Local variables to hold trial longitude values.
        int    lonDegrees;
        int    lonMinutes;
        double lonSeconds;
        char   lonDirection;

        // Local variables to hold trial latitude values.
        int    latDegrees;
        int    latMinutes;
        double latSeconds;
        char   latDirection;

        // Helper object to decode primitive values.
        BufferedReader br = new BufferedReader(new InputStreamReader(in));

        while (true) {
            out.print(prompt);
            String  line = br.readLine();
            Scanner sc   = new Scanner(line);

            try {
                // Check low level format.
                String lonPattern = "(\\d{1,3})\\s+(\\d{1,2})\\s+(\\d{1,2}\\.\\d+)([EW])";
                String latPattern = "(\\d{1,2})\\s+(\\d{1,2})\\s+(\\d{1,2}\\.\\d+)([NS])";
                sc.findInLine("^\\s*" + lonPattern + ",\\s*" + latPattern + "\\s*$");
                MatchResult result = sc.match();

                lonDegrees   = Integer.parseInt(result.group(1));
                lonMinutes   = Integer.parseInt(result.group(2));
                lonSeconds   = Double.parseDouble(result.group(3));
                lonDirection = result.group(4).charAt(0);

                latDegrees   = Integer.parseInt(result.group(5));
                latMinutes   = Integer.parseInt(result.group(6));
                latSeconds   = Double.parseDouble(result.group(7));
                latDirection = result.group(8).charAt(0);

                // Check high level constraints.
                if (isValidLongitude(lonDegrees, lonMinutes, lonSeconds, lonDirection) &&
                        isValidLatitude (latDegrees, latMinutes, latSeconds)) {
                    break;
                }
                else {
                    error.println("Error: not a valid GPS value; high level check failed.");
                }
            }
            catch (IllegalStateException ex) {
                error.println("Error: not a valid GPS value; bad format.");
            }
            sc.close();
        }

        double lonAngle = (double)lonDegrees + (double)lonMinutes/60.0 + lonSeconds/3600.0;
        double latAngle = (double)latDegrees + (double)latMinutes/60.0 + latSeconds/3600.0;

        return new GPS( (lonDirection == 'E' ? lonAngle : -lonAngle),
                        (latDirection == 'N' ? latAngle : -latAngle) );
    }


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GPS gps = (GPS) o;
        return Double.compare(gps.longitude, longitude) == 0 &&
                Double.compare(gps.latitude, latitude) == 0;
    }

    @Override
    public int hashCode() {
        return Objects.hash(longitude, latitude);
    }

    @Override
    public String toString() {
        return "GPS{longitude=" + longitude + ", latitude=" + latitude + "}";
    }
}
