// -*- mode: javascript; indent-tabs-mode: t; c-basic-offset: 8 -*-
"use strict";

let NBSP='\u00a0';
let NNBSP='\u202f';
let NUMSP='\u2007';
let DEGREES='\u00b0'
let ENDASH='\u2013';
let UP_TRIANGLE='\u25b2'; // U+25B2 BLACK UP-POINTING TRIANGLE
let DOWN_TRIANGLE='\u25bc'; // U+25BC BLACK DOWN-POINTING TRIANGLE
let EM_QUAD = '\u2001';

let TrackDirections = ["North","NE","East","SE","South","SW","West","NW"];
let TrackDirectionArrows = ["\u21e7","\u2b00","\u21e8","\u2b02","\u21e9","\u2b03","\u21e6","\u2b01"];

let UnitLabels = {
    'altitude': { metric: "m", imperial: "ft", nautical: "ft"},
    'speed': { metric: "km/h", imperial: "mph", nautical: "kt" },
    'distance': { metric: "km", imperial: "mi", nautical: "nmi" },
    'verticalRate': { metric: "m/s", imperial: "ft/min", nautical: "ft/min" },
    'distanceShort': { metric: "m", imperial: "ft", nautical: "m" }
};

let aircraftCategories = {
    'A0': 'Unspecified powered aircraft',
    'A1': `Light (< 15${NNBSP}500${NBSP}lb)`,
    'A2': `Small (15${NNBSP}500 to 75${NNBSP}000${NBSP}lb)`,
    'A3': `Large (75${NNBSP}000 to 300${NNBSP}000${NBSP}lb)`,
    'A4': 'High Vortex Large(aircraft such as B-757)',
    'A5': `Heavy (> 300${NNBSP}000${NBSP}lb)`,
    'A6': `High Performance (> 5${NBSP}g acceleration and > 400${NBSP}kt)`,
    'A7': 'Rotorcraft',
    'B0': 'Unspecified unpowered aircraft or UAV or spacecraft',
    'B1': 'Glider/sailplane',
    'B2': 'Lighter-than-Air',
    'B3': 'Parachutist/Skydiver',
    'B4': 'Ultralight/hang-glider/paraglider',
    'B6': 'Unmanned Aerial Vehicle',
    'B7': 'Space/Trans-atmospheric vehicle',
    'C0': 'Unspecified ground installation or vehicle',
    'C1': `Surface Vehicle ${ENDASH} Emergency Vehicle`,
    'C2': `Surface Vehicle ${ENDASH} Service Vehicle`,
    'C3': 'Fixed Ground or Tethered Obstruction'
};

// formatting helpers

function get_category_label(category) {
    if (!category)
        return '';
    let label = aircraftCategories[category];
    if (!label)
        return '';
    return label;
}

function get_unit_label(quantity, systemOfMeasurement) {
	let labels = UnitLabels[quantity];
	if (labels !== undefined && labels[systemOfMeasurement] !== undefined) {
		return labels[systemOfMeasurement];
	}
	return "";
}

// track in degrees (0..359)
function format_track_brief(track, rounded) {
	if (track == null){
		return "n/a";
	}

	return track.toFixed(rounded ? 0 : 1) + DEGREES;
}

// track in degrees (0..359)
function format_track_long(track, rounded) {
	if (track == null){
		return "n/a";
	}

	let trackDir = Math.floor((360 + track % 360 + 22.5) / 45) % 8;
	return  TrackDirections[trackDir] + ":" + NNBSP + track.toFixed(rounded ? 0 : 1) + DEGREES;
}
function format_track_arrow(track) {
	if (track == null){
		return "";
	}

	let trackDir = Math.floor((360 + track % 360 + 22.5) / 45) % 8;
	return  TrackDirectionArrows[trackDir];
}

// alt in feet
function format_altitude_brief(alt, vr, displayUnits, withUnits) {
	let alt_text;

	if (alt == null){
		return NBSP + '?' + NBSP;
	} else if (alt === "ground"){
		return "ground";
	}

	alt_text = Math.round(convert_altitude(alt, displayUnits)).toString();
    if (withUnits)
        alt_text += NNBSP + get_unit_label("altitude", displayUnits);

	// Vertical Rate Triangle
	let verticalRateTriangle = "";
	if (vr > 245){
		verticalRateTriangle = UP_TRIANGLE;
	} else if (vr < -245){
		verticalRateTriangle = DOWN_TRIANGLE;
	} else {
		verticalRateTriangle = ''
	}

	return verticalRateTriangle + alt_text.padStart(5, NUMSP);
}

// alt in feet
function format_altitude_long(alt, vr, displayUnits) {
	let alt_text = "";

	if (alt == null) {
		return "n/a";
	} else if (alt === "ground") {
		return "on ground";
	}

	alt_text = Math.round(convert_altitude(alt, displayUnits)).toString() + NNBSP + get_unit_label("altitude", displayUnits);

	if (vr > 192) {
		return UP_TRIANGLE + NNBSP + alt_text;
	} else if (vr < -192) {
		return DOWN_TRIANGLE + NNBSP + alt_text;
	} else {
		return alt_text;
	}
}

// alt in feet
function format_altitude(alt, displayUnits) {
	let alt_text = "";

	if (alt == null) {
		return "n/a";
	} else if (alt === "ground") {
		return "on ground";
	}

	alt_text = Math.round(convert_altitude(alt, displayUnits)).toString() + NNBSP + get_unit_label("altitude", displayUnits);

    return alt_text;
}


// alt ground/airborne
function format_onground (alt) {
	if (alt == null) {
		return "n/a";
	} else if (alt === "ground") {
		return "on ground";
	} else {
		return "airborne";
	}
}

// alt in feet
function convert_altitude(alt, displayUnits) {
	if (displayUnits === "metric") {
		return alt * 0.3048;  // feet to meters
	}

	return alt;
}

// speed in knots
function format_speed_brief(speed, displayUnits, withUnits) {
	if (speed == null || isNaN(speed)) {
		return "";
	}
    let speed_text = Math.round(convert_speed(speed, displayUnits)).toString();
    if (withUnits)
        speed_text += NNBSP + get_unit_label("speed", displayUnits);

	return speed_text;
}

// speed in knots
function format_speed_long(speed, displayUnits) {
	if (speed == null) {
		return "n/a";
	}

	let speed_text = Math.round(convert_speed(speed, displayUnits)) + NNBSP + get_unit_label("speed", displayUnits);

	return speed_text;
}

// speed in knots
function convert_speed(speed, displayUnits) {
	if (displayUnits === "metric") {
		return speed * 1.852;  // knots to kilometers per hour
	}
	else if (displayUnits === "imperial") {
		return speed * 1.151;  // knots to miles per hour
	}

	return speed;
}

// dist in meters
function format_distance_brief(dist, displayUnits) {
	if (dist == null) {
		return "";
	}

	return convert_distance(dist, displayUnits).toFixed(1);
}

// dist in meters
function format_distance_long(dist, displayUnits, fixed) {
	if (dist == null) {
		return "n/a";
	}

	if (typeof fixed === 'undefined') {
		fixed = 1;
	}

	let dist_text = convert_distance(dist, displayUnits).toFixed(fixed) + NNBSP + get_unit_label("distance", displayUnits);

	return dist_text;
}

function format_distance_short (dist, displayUnits) {
	if (dist == null) {
		return "n/a";
	}

	let dist_text = Math.round(convert_distance_short(dist, displayUnits)) + NNBSP + get_unit_label("distanceShort", displayUnits);

	return dist_text;
}

// dist in meters
function convert_distance(dist, displayUnits) {
	if (displayUnits === "metric") {
		return (dist / 1000); // meters to kilometres
	}
	else if (displayUnits === "imperial") {
		return (dist / 1609); // meters to miles
	}
	return (dist / 1852); // meters to nautical miles
}

// dist in meters
// converts meters to feet or just returns metres
function convert_distance_short(dist, displayUnits) {
	if (displayUnits === "imperial") {
		return (dist / 0.3048); // meters to feet
	}
	return dist; // just meters
}

// rate in ft/min
function format_vert_rate_brief(rate, displayUnits) {
	if (rate == null) {
		return "";
	}

	return convert_vert_rate(rate, displayUnits).toFixed(displayUnits === "metric" ? 1 : 0);
}

// rate in ft/min
function format_vert_rate_long(rate, displayUnits) {
	if (rate == null){
		return "n/a";
	}

	let rate_text = convert_vert_rate(rate, displayUnits).toFixed(displayUnits === "metric" ? 1 : 0) + NNBSP + get_unit_label("verticalRate", displayUnits);

	return rate_text;
}

// rate in ft/min
function convert_vert_rate(rate, displayUnits) {
	if (displayUnits === "metric") {
		return (rate / 196.85); // ft/min to m/s
	}

	return rate;
}

// p is a [lon, lat] coordinate
function format_latlng(p) {
	return p[1].toFixed(3) + DEGREES + "," + NNBSP + p[0].toFixed(3) + DEGREES;
}

function format_data_source(source) {
	switch (source) {
		case 'uat' :
			return "UAT";
		case 'mlat':
			return "MLAT";
		case 'adsb':
		case 'adsb_icao':
		case 'adsb_other':
			return "ADS-B";
		case 'adsb_icao_nt':
			return "ADS-B noTP";
		case 'adsr':
		case 'adsr_icao':
		case 'adsr_other':
			return "ADS-R or UAT";
		case 'tisb_icao':
		case 'tisb_trackfile':
		case 'tisb_other':
		case 'tisb':
			return "TIS-B";
		case 'modeS':
			return "Mode S";
		case 'ais':
			return "AIS";
		case 'mode_ac':
			return "Mode A/C";
        case 'adsc':
            return jaeroLabel;
        case 'other':
            return "Other";
	}

	return "Unknown";
}

function format_nac_p (value) {
	switch (value) {
		case 0:
			return "EPU ≥ 18.5 km";
		case 1:
			return "EPU < 18.5 km";
		case 2:
			return "EPU < 7.4 km";
		case 3:
			return "EPU < 3.7 km";
		case 4:
			return "EPU < 1.8 km";
		case 5:
			return "EPU < 926 m";
		case 6:
			return "EPU < 555 m";
		case 7:
			return "EPU < 185 m";
		case 8:
			return "EPU < 92 m";
		case 9:
			return "EPU < 30 m";
		case 10:
			return "EPU < 10 m";
		case 11:
			return "EPU < 3 m";
		default:
			return "n/a";

	}
}

function format_nac_v (value) {
	switch (value) {
		case 0:
			return "≥ 10 m/s";
		case 1:
			return "< 10 m/s";
		case 2:
			return "< 3 m/s";
		case 3:
			return "< 1 m/s";
		case 4:
			return "< 0.3 m/s";
		default:
			return "n/a";
	}
}

function format_duration(seconds) {
    if (seconds == null)
        return "n/a";
    if (seconds < 20)
        return seconds.toFixed(1) + ' s';
    if (seconds < 5 * 60)
        return seconds.toFixed(0) + ' s';
    if (seconds < 3 * 60 * 60)
        return (seconds/60).toFixed(0) + ' min';
    return (seconds/60/60).toFixed(0) + ' h';

}

function iOSVersion() {
  if (/iP(hone|od|ad)/.test(navigator.platform)) {
    // supports iOS 2.0 and later: <http://bit.ly/TJjs1V>
    var v = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);
    return [parseInt(v[1], 10), parseInt(v[2], 10), parseInt(v[3] || 0, 10)];
  }
}

function wqi(data) {
    const INT32_MAX = 2147483647;
    const buffer = data.buffer;
    //console.log(buffer);
    let u32 = new Uint32Array(data.buffer, 0, 13);
    data.now = u32[0] / 1000 + u32[1] * 4294967.296;
    //console.log(data.now);
    let stride = u32[2];
    data.global_ac_count_withpos = u32[3];
    data.globeIndex = u32[4];

    let limits = new Int16Array(buffer, 20, 4);
    data.south =  limits[0];
    data.west =  limits[1];
    data.north =  limits[2];
    data.east =  limits[3];

    data.messages = u32[7];

    let s32 = new Int32Array(data.buffer, 0, stride / 4);
    let receiver_lat = s32[8] / 1e6;
    let receiver_lon = s32[9] / 1e6;


    const binCraftVersion = u32[10];

    data.messageRate = u32[11] / 10;

    const flags = u32[12];
    const useMessageRate = flags & (1 << 0);

    if (receiver_lat != 0 && receiver_lon != 0) {
        //console.log("receiver_lat: " + receiver_lat + " receiver_lon: " + receiver_lon);
        let position = {
            coords: {
                latitude: receiver_lat,
                longitude: receiver_lon,
            },
        };
        if (receiver_lat != SiteLat || receiver_lon != SiteLon) {
            onLocationChange(position);
        }
    }

    data.aircraft = [];
    for (let off = stride; off < buffer.byteLength; off += stride) {
        let ac = {}
        let u32 = new Uint32Array(buffer, off, stride / 4);
        let s32 = new Int32Array(buffer, off, stride / 4);
        let u16 = new Uint16Array(buffer, off, stride / 2);
        let s16 = new Int16Array(buffer, off, stride / 2);
        let u8 = new Uint8Array(buffer, off, stride);
        let t = s32[0] & (1<<24);
        ac.hex = (s32[0] & ((1<<24) - 1)).toString(16).padStart(6, '0');
        ac.hex = t ? ('~' + ac.hex) : ac.hex;

        if (binCraftVersion >= 20240218) {
            ac.seen = s32[1] / 10;
            ac.seen_pos = s32[27] / 10;
        } else {
            ac.seen_pos = u16[2] / 10;
            ac.seen = u16[3] / 10;
        }

        ac.lon = s32[2] / 1e6;
        ac.lat = s32[3] / 1e6;

        ac.baro_rate = s16[8] * 8;
        ac.geom_rate = s16[9] * 8;
        ac.alt_baro = s16[10] * 25;
        ac.alt_geom = s16[11] * 25;

        ac.nav_altitude_mcp = u16[12] * 4;
        ac.nav_altitude_fms = u16[13] * 4;
        ac.nav_qnh = s16[14] / 10;
        ac.nav_heading = s16[15] / 90;

        const s = u16[16].toString(16).padStart(4, '0');
        if (s[0] > '9') {
            ac.squawk = String(parseInt(s[0], 16)) + s[1] + s[2] + s[3];
        } else {
            ac.squawk = s;
        }
        ac.gs = s16[17] / 10;
        ac.mach = s16[18] / 1000;
        ac.roll = s16[19] / 100;

        ac.track = s16[20] / 90;
        ac.track_rate = s16[21] / 100;
        ac.mag_heading = s16[22] / 90;
        ac.true_heading = s16[23] / 90;

        ac.wd  = s16[24];
        ac.ws  = s16[25];
        ac.oat = s16[26];
        ac.tat = s16[27];

        ac.tas = u16[28];
        ac.ias = u16[29];
        ac.rc  = u16[30];

        if (useMessageRate) {
            ac.messageRate = u16[31] / 10;
        } else {
            ac.messages = u16[31];
        }

        ac.category = u8[64] ? u8[64].toString(16).toUpperCase() : undefined;
        ac.nic      = u8[65];

        let nav_modes = u8[66];
        ac.nav_modes = true;
        ac.emergency = u8[67] & 15;
        ac.type = (u8[67] & 240) >> 4;

        ac.airground = u8[68] & 15;
        ac.nav_altitude_src = (u8[68] & 240) >> 4;

        ac.sil_type = u8[69] & 15;
        ac.adsb_version = (u8[69] & 240) >> 4;

        ac.adsr_version = u8[70] & 15;
        ac.tisb_version = (u8[70] & 240) >> 4;

        ac.nac_p = u8[71] & 15;
        ac.nac_v = (u8[71] & 240) >> 4;

        ac.sil = u8[72] & 3;
        ac.gva = (u8[72] & 12) >> 2;
        ac.sda = (u8[72] & 48) >> 4;
        ac.nic_a = (u8[72] & 64) >> 6;
        ac.nic_c = (u8[72] & 128) >> 7;


        ac.flight = "";
        for (let i = 78; u8[i] && i < 86; i++) {
            ac.flight += String.fromCharCode(u8[i]);
        }

        ac.dbFlags = u16[43];

        ac.t = "";
        for (let i = 88; u8[i] && i < 92; i++) {
            ac.t += String.fromCharCode(u8[i]);
        }
        ac.r = "";
        for (let i = 92; u8[i] && i < 104; i++) {
            ac.r += String.fromCharCode(u8[i]);
        }
        ac.receiverCount = u8[104];

        if (binCraftVersion >= 20250403) {
            ac.rssi = (u8[105] * (50 / 255)) - 50;
        } else {
            let level = u8[105]*u8[105]/65025 + 1.125e-5;
            ac.rssi = 10 * Math.log(level)/Math.log(10);
        }

        ac.extraFlags = u8[106];
        ac.nogps = ac.extraFlags & 1;
        if (ac.nogps && nogpsOnly && s32[3] != INT32_MAX) {
            u8[73] |= 64;
            u8[73] |= 16;
        }

        // must come after the stuff above (validity bits)

        ac.nic_baro      = (u8[73] & 1);
        ac.alert1        = (u8[73] & 2);
        ac.spi           = (u8[73] & 4);
        ac.flight        = (u8[73] & 8)    ? ac.flight       : undefined;
        ac.alt_baro      = (u8[73] & 16)   ? ac.alt_baro     : undefined;
        ac.alt_geom      = (u8[73] & 32)   ? ac.alt_geom     : undefined;

        ac.lat           = (u8[73] & 64)   ? ac.lat          : undefined;
        ac.lon           = (u8[73] & 64)   ? ac.lon          : undefined;
        ac.seen_pos      = (u8[73] & 64)   ? ac.seen_pos     : undefined;

        ac.gs            = (u8[73] & 128)  ? ac.gs           : undefined;

        ac.ias           = (u8[74] & 1)    ? ac.ias          : undefined;
        ac.tas           = (u8[74] & 2)    ? ac.tas          : undefined;
        ac.mach          = (u8[74] & 4)    ? ac.mach         : undefined;
        ac.track         = (u8[74] & 8)    ? ac.track        : undefined;
        ac.calc_track    = !(u8[74] & 8)   ? ac.track        : undefined;
        ac.track_rate    = (u8[74] & 16)   ? ac.track_rate   : undefined;
        ac.roll          = (u8[74] & 32)   ? ac.roll         : undefined;
        ac.mag_heading   = (u8[74] & 64)   ? ac.mag_heading  : undefined;
        ac.true_heading  = (u8[74] & 128)  ? ac.true_heading : undefined;

        ac.baro_rate     = (u8[75] & 1)    ? ac.baro_rate    : undefined;
        ac.geom_rate     = (u8[75] & 2)    ? ac.geom_rate    : undefined;
        ac.nic_a         = (u8[75] & 4)    ? ac.nic_a        : undefined;
        ac.nic_c         = (u8[75] & 8)    ? ac.nic_c        : undefined;
        ac.nic_baro      = (u8[75] & 16)   ? ac.nic_baro     : undefined;
        ac.nac_p         = (u8[75] & 32)   ? ac.nac_p        : undefined;
        ac.nac_v         = (u8[75] & 64)   ? ac.nac_v        : undefined;
        ac.sil           = (u8[75] & 128)  ? ac.sil          : undefined;

        ac.gva                  = (u8[76] & 1)   ? ac.gva              : undefined;
        ac.sda                  = (u8[76] & 2)   ? ac.sda              : undefined;
        ac.squawk               = (u8[76] & 4)   ? ac.squawk           : undefined;
        ac.emergency            = (u8[76] & 8)   ? ac.emergency        : undefined;
        ac.spi                  = (u8[76] & 16)  ? ac.spi              : undefined;
        ac.nav_qnh              = (u8[76] & 32)  ? ac.nav_qnh          : undefined;
        ac.nav_altitude_mcp     = (u8[76] & 64)  ? ac.nav_altitude_mcp : undefined;
        ac.nav_altitude_fms     = (u8[76] & 128) ? ac.nav_altitude_fms : undefined;

        ac.nav_altitude_src     = (u8[77] & 1)   ? ac.nav_altitude_src : undefined;
        ac.nav_heading          = (u8[77] & 2)   ? ac.nav_heading      : undefined;
        ac.nav_modes            = (u8[77] & 4)   ? ac.nav_modes        : undefined;
        ac.alert1               = (u8[77] & 8)   ? ac.alert1           : undefined;
        ac.ws                   = (u8[77] & 16)  ? ac.ws               : undefined;
        ac.wd                   = (u8[77] & 16)  ? ac.wd               : undefined;
        ac.oat                  = (u8[77] & 32)  ? ac.oat              : undefined;
        ac.tat                  = (u8[77] & 32)  ? ac.tat              : undefined;

        if (ac.airground == 1)
            ac.alt_baro = "ground";

        if (ac.nav_modes) {
            ac.nav_modes = [];
            if (nav_modes & 1) ac.nav_modes.push('autopilot');
            if (nav_modes & 2) ac.nav_modes.push('vnav');
            if (nav_modes & 4) ac.nav_modes.push('alt_hold');
            if (nav_modes & 8) ac.nav_modes.push('approach');
            if (nav_modes & 16) ac.nav_modes.push('lnav');
            if (nav_modes & 32) ac.nav_modes.push('tcas');
        }
        switch (ac.type) {
            case  0: ac.type = 'adsb_icao';        break;
            case  1: ac.type = 'adsb_icao_nt';     break;
            case  2: ac.type = 'adsr_icao';        break;
            case  3: ac.type = 'tisb_icao';        break;
            case  4: ac.type = 'adsc';             break;
            case  5: ac.type = 'mlat';             break;
            case  6: ac.type = 'other';            break;
            case  7: ac.type = 'mode_s';           break;
            case  8: ac.type = 'adsb_other';       break;
            case  9: ac.type = 'adsr_other';       break;
            case 10: ac.type = 'tisb_trackfile';   break;
            case 11: ac.type = 'tisb_other';       break;
            case 12: ac.type = 'mode_ac';          break;
            default: ac.type = 'unknown';
        }
        const type4 = ac.type.slice(0, 4);
        if (type4 == 'adsb') {
            ac.version = ac.adsb_version;
        } else if (type4 == 'adsr') {
            ac.version = ac.adsr_version;
        } else if (type4 == 'tisb') {
            ac.version = ac.tisb_version;
        }

        if (binCraftVersion >= 20240218) {
            if (stride == 116) {
                ac.rId = u32[28].toString(16).padStart(8, '0');
            }
        } else {
            if (stride == 112) {
                ac.rId = u32[27].toString(16).padStart(8, '0');
            }
        }

        data.aircraft.push(ac);
    }
}

function ItemCache(maxItems) {
    this.maxItems = maxItems;
    this.items = {};
    this.keys = [];
}
ItemCache.prototype.clear = function() {
    this.items = {};
    this.keys = [];
}
ItemCache.prototype.get = function(key) {
    return this.items[key];
}
ItemCache.prototype.add = function(key, value) {

    if (!(key in this.items)) {
        this.keys.push(key);
    }
    this.items[key] = value;

    if (this.maxItems && this.maxItems > 0) {
        while (this.keys.length > this.maxItems) {
            const key = this.keys.shift();
            delete this.items[key];
        }
    }
}

function itemCacheTest() {
    let a = new ItemCache(4);
    a.add(8, 4);
    a.add(5, 2);
    a.add(4, 2);
    a.add(3, 2);
    a.add(1, 2);
    a.add(1, 3);
    a.add(1, 5);
    let items = JSON.stringify(a.items)
    let keys = JSON.stringify(a.keys);
    const expectedItems = '{"1":5,"3":2,"4":2,"5":2}';
    const expectedKeys = '[5,4,3,1]';
    if (items != expectedItems || keys != expectedKeys || g.get(1) != 5) {
        console.error(`ItemCache broken!`);
        console.log(`got:      items: ${items} keys: ${keys}`);
        console.log(`expected: items: ${expectedItems} keys: ${expectedKeys}`);
    } else {
        console.log(`ItemCache tested correctly!`);
    }
}
