// Storage for the options that the server gives us. This will define our filters.
var options = {};
// Last time we updated the spots/alerts list on display.
var lastUpdateTime;


// For a parameter, such as dx_continent, get the query string for the current filter options.
function getQueryStringFor(parameter) {
    return parameter + "=" + encodeURIComponent(getSelectedFilterOptions(parameter));
}

// For a parameter, such as dx_continent, get the filter options that are currently selected in the UI.
function getSelectedFilterOptions(parameter) {
    return $(".filter-button-" + parameter).filter(function() {
        return this.checked;
    }).map(function() {
        return this.value;
    }).get().join(",");
}

// For a parameter, such as dx_continent, return true if all possible options are enabled. (In this case, we don't need
// to bother sending this as one of the query parameters to the API; no parameter provided implies "send everything".)
function allFilterOptionsSelected(parameter) {
    var filter = $(".filter-button-" + parameter).filter(function() {
        return !this.checked;
    }).get();
    return filter.length == 0;
}


// Generate a filter card with multiple toggle buttons plus All/None buttons.
function generateMultiToggleFilterCard(elementID, filterQuery, options) {
    // Create a button for each option
    options.forEach(o => {
        $(elementID).append(`<input type="checkbox" class="btn-check filter-button-${filterQuery} storeable-checkbox" name="options" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="btn btn-outline-primary" for="filter-button-${filterQuery}-${o}">${o}</label> `);
    });
    // Create All/None buttons
    $(elementID).append(` <span style="display: inline-block"><button id="filter-button-${filterQuery}-all" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('${filterQuery}', true);">All</button>&nbsp;<button id="filter-button-${filterQuery}-none" type="button" class="btn btn-outline-secondary" onclick="toggleFilterButtons('${filterQuery}', false);">None</button></span>`);
}

// Method called when "All" or "None" is clicked
function toggleFilterButtons(filterQuery, state) {
    $(".filter-button-" + filterQuery).each(function() {
        $(this).prop('checked', state);
    });
    filtersUpdated();
}

// Update the refresh timing display
function updateRefreshDisplay() {
    if (lastUpdateTime != null) {
        let count = REFRESH_INTERVAL_SEC;
        let secSinceUpdate = moment.duration(moment().diff(lastUpdateTime)).asSeconds();
        updatingString = "Updating..."
        if (secSinceUpdate < REFRESH_INTERVAL_SEC) {
            count = REFRESH_INTERVAL_SEC - secSinceUpdate;
            if (count <= 60) {
                var number = count.toFixed(0);
                updatingString = "<span class='nowrap'>Updating in " + number + " second" + (number != "1" ? "s" : "") + ".</span>";
            } else {
                var number = Math.round(count / 60.0).toFixed(0);
                updatingString = "<span class='nowrap'>Updating in " + number + " minute" + (number != "1" ? "s" : "") + ".</span>";
            }
        }
        $("#timing-container").html("Last updated at " + lastUpdateTime.format('HH:mm') + " UTC. " + updatingString);
    }
}

// Utility function to escape HTML characters from a string.
function escapeHtml(str) {
    if (typeof str !== 'string') {
        return '';
    }

    const escapeCharacter = (match) => {
        switch (match) {
            case '&': return '&amp;';
            case '<': return '&lt;';
            case '>': return '&gt;';
            case '"': return '&quot;';
            case '\'': return '&#039;';
            case '`': return '&#096;';
            default: return match;
        }
    };

    return str.replace(/[&<>"'`]/g, escapeCharacter);
}

// When the "use local time" field is changed, reload the table and save settings
function timeZoneUpdated() {
    updateTable();
    saveSettings();
}

// When one of the column toggle checkboxes are changed, reload the table and save settings
function columnsUpdated() {
    updateTable();
    saveSettings();
}

// 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;
    var lonDelta = lon2 - lon1;
    var y = Math.sin(lonDelta) * Math.cos(lat2);
    var x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(lonDelta);
    var bearing = Math.atan2(y, x);
    bearing = bearing * (180 / Math.PI);
    if ( bearing < 0 ) { bearing += 360; }
    return bearing;
}

// 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];
}

// Save settings to local storage
function saveSettings() {
    // Find all storeable UI elements, store a key of "element id:property name" mapped to the value of that
    // property. For a checkbox, that's the "checked" property.
    $(".storeable-checkbox").each(function() {
        localStorage.setItem("#" + $(this)[0].id + ":checked", JSON.stringify($(this)[0].checked));
    });
    $(".storeable-select").each(function() {
        localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value));
    });
    $(".storeable-text").each(function() {
        localStorage.setItem("#" + $(this)[0].id + ":value", JSON.stringify($(this)[0].value));
    });
}

// Load settings from local storage and set up the filter selectors
function loadSettings() {
    // Find all local storage entries and push their data to the corresponding UI element
    Object.keys(localStorage).forEach(function(key) {
        if (key.startsWith("#") && key.includes(":")) {
            // Split the key back into an element ID and a property
            var split = key.split(":");
            $(split[0]).prop(split[1], JSON.parse(localStorage.getItem(key)));
        }
    });
}
