// Copied from from https://github.com/nherment/node-nmea/blob/master/lib/Helper.js


const m_hex = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];


export function toHexString(v: number): string {
    const msn = (v >> 4) & 0x0f;
    const lsn = (v >> 0) & 0x0f;

    return m_hex[msn] + m_hex[lsn];
}


export function padLeft(value: string | number, length: number, paddingCharacter: string): string {
    let result = typeof value === "string" ? value : value.toFixed(0);

    while (result.length < length) {
        result = paddingCharacter + result;
    }

    return result;
}



// =========================================
// checksum related functions
// =========================================

/**
 * Checks that the given NMEA sentence has a valid checksum.
 */
export function validNmeaChecksum(nmeaSentence: string): boolean {
    const [sentenceWithoutChecksum, checksumString] = nmeaSentence.split("*");

    const correctChecksum = computeNmeaChecksum(sentenceWithoutChecksum);

    // checksum is a 2 digit hex value
    const actualChecksum = parseInt(checksumString, 16);

    return correctChecksum === actualChecksum;
}


/**
 * Generate a checksum for an NMEA sentence without the trailing "*xx".
 */
export function computeNmeaChecksum(sentenceWithoutChecksum: string): number {
    // init to first character value after the $
    let checksum = sentenceWithoutChecksum.charCodeAt(1);

    // process rest of characters, zero delimited
    for (let i = 2; i < sentenceWithoutChecksum.length; i += 1) {
        checksum = checksum ^ sentenceWithoutChecksum.charCodeAt(i);
    }

    // checksum is between 0x00 and 0xff
    checksum = checksum & 0xff;

    return checksum;
}


/**
 * Generate the correct trailing "*xx" footer for an NMEA sentence.
 */
export function createNmeaChecksumFooter(sentenceWithoutChecksum: string): string {
    return "*" + toHexString(computeNmeaChecksum(sentenceWithoutChecksum));
}


/**
 * Append the correct trailing "*xx" footer for an NMEA string and return the result.
 */
export function appendChecksumFooter(sentenceWithoutChecksum: string): string {
    return sentenceWithoutChecksum + createNmeaChecksumFooter(sentenceWithoutChecksum);
}



// =========================================
// field encoders
// =========================================

export function encodeFixed(value: number | undefined, decimalPlaces: number): string {
    if (value === undefined) {
        return "";
    }

    return value.toFixed(decimalPlaces);
}


/**
 * Encodes the latitude in the standard NMEA format "ddmm.mmmmmm".
 *
 * @param latitude Latitude in decimal degrees.
 */
export function encodeLatitude(latitude?: number): string {
    if (latitude === undefined) {
        return ",";
    }

    let hemisphere: string;
    if (latitude < 0) {
        hemisphere = "S";
        latitude = -latitude;
    } else {
        hemisphere = "N";
    }

    // get integer degrees
    const d = Math.floor(latitude);
    // latitude degrees are always 2 digits
    let s = padLeft(d, 2, "0");

    // get fractional degrees
    const f = latitude - d;
    // convert to fractional minutes
    const m = (f * 60.0);
    // format the fixed point fractional minutes "mm.mmmmmm"
    const t = padLeft(m.toFixed(6), 9, "0");

    s = s + t + "," + hemisphere;
    return s;
}


/**
 * Encodes the longitude in the standard NMEA format "dddmm.mmmmmm".
 *
 * @param longitude Longitude in decimal degrees.
 */
export function encodeLongitude(longitude?: number): string {
    if (longitude === undefined) {
        return ",";
    }

    let hemisphere: string;
    if (longitude < 0) {
        hemisphere = "W";
        longitude = -longitude;
    } else {
        hemisphere = "E";
    }

    // get integer degrees
    const d = Math.floor(longitude);
    // longitude degrees are always 3 digits
    let s = padLeft(d, 3, "0");

    // get fractional degrees
    const f = longitude - d;
    // convert to fractional minutes and round up to the specified precision
    const m = (f * 60.0);
    // format the fixed point fractional minutes "mm.mmmmmm"
    const t = padLeft(m.toFixed(6), 9, "0");

    s = s + t + "," + hemisphere;
    return s;
}


// 1 decimal, always meters
export function encodeAltitude(alt: number): string {
    if (alt === undefined) {
        return ",";
    }

    return alt.toFixed(1) + ",M";
}

// Some encodings don't want the unit
export function encodeAltitudeNoUnits(alt: number): string {
    if (alt === undefined) {
        return "";
    }

    return alt.toFixed(1);
}


// 1 decimal, always meters
export function encodeGeoidalSeperation(geoidalSep: number): string {
    if (geoidalSep === undefined) {
        return ",";
    }

    return geoidalSep.toFixed(1) + ",M";
}

// Some encodings don't want the unit
export function encodeGeoidalSeperationNoUnits(geoidalSep: number): string {
    if (geoidalSep === undefined) {
        return "";
    }

    return geoidalSep.toFixed(1);
}


// degrees
export function encodeDegrees(degrees?: number): string {
    if (degrees === undefined) {
        return "";
    }

    return padLeft(degrees.toFixed(2), 6, "0");
}


export function encodeDate(date?: Date): string {
    if (date === undefined) {
        return "";
    }

    const year = date.getUTCFullYear();
    const month = date.getUTCMonth() + 1;
    const day = date.getUTCDate();

    return padLeft(day, 2, "0") + padLeft(month, 2, "0") + year.toFixed(0).substr(2);
}


export function encodeTime(date?: Date): string {
    if (date === undefined) {
        return "";
    }

    const hours = date.getUTCHours();
    const minutes = date.getUTCMinutes();
    const seconds = date.getUTCSeconds();

    return padLeft(hours, 2, "0") + padLeft(minutes, 2, "0") + padLeft(seconds, 2, "0");
}


export function encodeValue(value?: any): string {
    if (value === undefined) {
        return "";
    }

    return value.toString();
}



// =========================================
// field traditionalDecoders
// =========================================

/**
 * Parse the given string to a float, returning 0 for an empty string.
 */
export function parseFloatSafe(str: string): number {
    if (str === "") {
        return 0.0;
    }
    return parseFloat(str);
}


/**
 * Parse the given string to a integer, returning 0 for an empty string.
 */
export function parseIntSafe(i: string): number {
    if (i === "") {
        return 0;
    }

    return parseInt(i, 10);
}


/**
 * Parse the given string to a float if possible, returning 0 for an undefined
 * value and a string the the given string cannot be parsed.
 */
export function parseNumberOrString(str?: string): number | string {
    if (str === undefined) {
        return "";
    }

    const num = parseFloat(str);

    return isNaN(num) ? str : num;
}


/**
 * Parses coordinate given as "dddmm.mm", "ddmm.mm", "dmm.mm" or "mm.mm"
 */
export function parseDmCoordinate(coordinate: string): number {

    const dotIdx = coordinate.indexOf(".");

    if (dotIdx < 0) {
        return 0;
    }

    let degrees: string;
    let minutes: string;

    if (dotIdx >= 3) {
        degrees = coordinate.substring(0, dotIdx - 2);
        minutes = coordinate.substring(dotIdx - 2);
    } else {
        // no degrees, just minutes (nonstandard but a buggy unit might do this)
        degrees = "0";
        minutes = coordinate;
    }

    return (parseFloat(degrees) + (parseFloat(minutes) / 60.0));
}

/**
 * Parses latitude given as "ddmm.mm", "dmm.mm" or "mm.mm" (assuming zero
 * degrees) along with a given hemisphere of "N" or "S" into decimal degrees,
 * where north is positive and south is negative.
 */
export function parseLatitude(lat: string, hemi: string): number {
    const hemisphere = (hemi === "N") ? 1.0 : -1.0;

    return parseDmCoordinate(lat) * hemisphere;
}


/**
 * Parses latitude given as "dddmm.mm", "ddmm.mm", "dmm.mm" or "mm.mm" (assuming
 * zero degrees) along with a given hemisphere of "E" or "W" into decimal
 * degrees, where east is positive and west is negative.
 */
export function parseLongitude(lon: string, hemi: string): number {
    const hemisphere = (hemi === "E") ? 1.0 : -1.0;

    return parseDmCoordinate(lon) * hemisphere;
}

function getYearFromString(yearString: string, rmcCompatible: boolean): number {
    if (yearString.length === 4) {
        return Number(yearString);
    } else if (yearString.length === 2) {
        if (rmcCompatible) {
            // GPRMC date doesn't specify century. GPS came out in 1973 so if the year
            // is less than 73, assume it's 20xx, otherwise assume it is 19xx.
            let year = Number(yearString);

            if (year < 73) {
                year = 2000 + year;
            }
            else {
                year = 1900 + year;
            }

            return year;
        }
        else {
            return Number("20" + yearString);
        }
    }
    else {
        throw Error(`Unexpected year string: ${yearString}`);
    }
}

/**
 * Parses a UTC time and optionally a date and returns a Date
 * object.
 * @param {String} time Time the format "hhmmss" or "hhmmss.ss"
 * @param {String=} date Optional date in format the ddmmyyyy or ddmmyy
 * @returns {Date}
 */
export function parseTime(time: string, date?: string, rmcCompatible = false): Date {

    if (time === "") {
        return new Date(0);
    }

    const ret = new Date();

    if (date) {

        const year = date.slice(4);
        const month = parseInt(date.slice(2, 4), 10) - 1;
        const day = date.slice(0, 2);

        ret.setUTCFullYear(getYearFromString(year, rmcCompatible), Number(month), Number(day));
    }

    ret.setUTCHours(Number(time.slice(0, 2)));
    ret.setUTCMinutes(Number(time.slice(2, 4)));
    ret.setUTCSeconds(Number(time.slice(4, 6)));

    // Extract the milliseconds, since they can be not present, be 3 decimal place, or 2 decimal places, or other?
    const msStr = time.slice(7);
    const msExp = msStr.length;
    let ms = 0;
    if (msExp !== 0) {
        ms = parseFloat(msStr) * Math.pow(10, 3 - msExp);
    }
    ret.setUTCMilliseconds(Number(ms));

    return ret;
}
