//
// GEOGRAPHIC UTILITY FUNCTIONS
// Great Circle calculation, Maidenhead grid calcs, etc.
//

// Calculate great circle bearing between two lat/lon points.
function calcBearing(lat1, lon1, lat2, lon2) {
    lat1 *= Math.PI / 180;
    lon1 *= Math.PI / 180;
    lat2 *= Math.PI / 180;
    lon2 *= Math.PI / 180;
    const lonDelta = lon2 - lon1;
    const y = Math.sin(lonDelta) * Math.cos(lat2);
    const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lonDelta);
    let bearing = Math.atan2(y, x);
    bearing = bearing * (180 / Math.PI);
    if (bearing < 0) {
        bearing += 360;
    }
    return bearing;
}

function calcDistanceKm(lat1, lon1, lat2, lon2) {
    const EARTH_RADIUS_KM = 6371;
    lat1 *= Math.PI / 180;
    lon1 *= Math.PI / 180;
    lat2 *= Math.PI / 180;
    lon2 *= Math.PI / 180;
    const latDelta = lat2 - lat1;
    const lonDelta = lon2 - lon1;
    const a = Math.sin(latDelta / 2) ** 2 +
        Math.cos(lat1) * Math.cos(lat2) * Math.sin(lonDelta / 2) ** 2;
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    return EARTH_RADIUS_KM * c;
}

// Convert a Maidenhead grid reference of arbitrary precision to the lat/long of the centre point of the square.
// Returns null if the grid format is invalid.
function latLonForGridCentre(grid) {
    let [lat, lon, latCellSize, lonCellSize] = latLonForGridSWCornerPlusSize(grid);
    if (lat != null && lon != null && latCellSize != null && lonCellSize != null) {
        return [lat + latCellSize / 2.0, lon + lonCellSize / 2.0];
    } else {
        return null;
    }
}

// Convert a Maidenhead grid reference of arbitrary precision to lat/long, including in the result the size of the
// lowest grid square. This is a utility method used by the main methods that return the centre, southwest, and
// northeast coordinates of a grid square.
// The return type is always an array of size 4. The elements in it are null if the grid format is invalid.
function latLonForGridSWCornerPlusSize(grid) {
    // Make sure we are in upper case so our maths works. Case is arbitrary for Maidenhead references
    grid = grid.toUpperCase();

    // Return null if our Maidenhead string is invalid or too short
    let len = grid.length;
    if (len <= 0 || (len % 2) !== 0) {
        return [null, null, null, null];
    }

    let lat = 0.0; // aggregated latitude
    let lon = 0.0; // aggregated longitude
    let latCellSize = 10; // Size in degrees latitude of the current cell. Starts at 20 and gets smaller as the calculation progresses
    let lonCellSize = 20; // Size in degrees longitude of the current cell. Starts at 20 and gets smaller as the calculation progresses
    let latCellNo; // grid latitude cell number this time
    let lonCellNo; // grid longitude cell number this time

    // Iterate through blocks (two-character sections)
    for (let block = 0; block * 2 < len; block += 1) {
        if (block % 2 === 0) {
            // Letters in this block
            lonCellNo = grid.charCodeAt(block * 2) - 'A'.charCodeAt(0);
            latCellNo = grid.charCodeAt(block * 2 + 1) - 'A'.charCodeAt(0);
            // Bail if the values aren't in range. Allowed values are A-R (0-17) for the first letter block, or
            // A-X (0-23) thereafter.
            let maxCellNo = (block === 0) ? 17 : 23;
            if (latCellNo < 0 || latCellNo > maxCellNo || lonCellNo < 0 || lonCellNo > maxCellNo) {
                return [null, null, null, null];
            }
        } else {
            // Numbers in this block
            lonCellNo = parseInt(grid.charAt(block * 2));
            latCellNo = parseInt(grid.charAt(block * 2 + 1));
            // Bail if the values aren't in range 0-9..
            if (latCellNo < 0 || latCellNo > 9 || lonCellNo < 0 || lonCellNo > 9) {
                return [null, null, null, null];
            }
        }

        // Aggregate the angles
        lat += latCellNo * latCellSize;
        lon += lonCellNo * lonCellSize;

        // Reduce the cell size for the next block, unless we are on the last cell.
        if (block * 2 < len - 2) {
            // Still have more work to do, so reduce the cell size
            if (block % 2 === 0) {
                // Just dealt with letters, next block will be numbers so cells will be 1/10 the current size
                latCellSize = latCellSize / 10.0;
                lonCellSize = lonCellSize / 10.0;
            } else {
                // Just dealt with numbers, next block will be letters so cells will be 1/24 the current size
                latCellSize = latCellSize / 24.0;
                lonCellSize = lonCellSize / 24.0;
            }
        }
    }

    // Offset back to (-180, -90) where the grid starts
    lon -= 180.0;
    lat -= 90.0;

    // Return nulls on maths errors
    if (isNaN(lat) || isNaN(lon) || isNaN(latCellSize) || isNaN(lonCellSize)) {
        return [null, null, null, null];
    }

    return [lat, lon, latCellSize, lonCellSize];
}
