/*
 * Part of DNS zone file validator `validns`.
 *
 * Copyright 2025-2026 OARC, Inc.
 * Copyright 2011-2025 Anton Berezin <tobez@tobez.org>
 * Modified BSD license.
 * (See LICENSE file in the distribution.)
 *
 */
#include <sys/types.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "common.h"
#include "textparse.h"
#include "mempool.h"
#include "carp.h"
#include "rr.h"

static uint8_t double2loc_format(double val)
{
    if (val > 1000000000) {
        return (((uint8_t)(val / 1000000000)) << 4) | 9;
    } else if (val > 100000000) {
        return (((uint8_t)(val / 100000000)) << 4) | 8;
    } else if (val > 10000000) {
        return (((uint8_t)(val / 10000000)) << 4) | 7;
    } else if (val > 1000000) {
        return (((uint8_t)(val / 1000000)) << 4) | 6;
    } else if (val > 100000) {
        return (((uint8_t)(val / 100000)) << 4) | 5;
    } else if (val > 10000) {
        return (((uint8_t)(val / 10000)) << 4) | 4;
    } else if (val > 1000) {
        return (((uint8_t)(val / 1000)) << 4) | 3;
    } else if (val > 100) {
        return (((uint8_t)(val / 100)) << 4) | 2;
    } else if (val > 10) {
        return (((uint8_t)(val / 10)) << 4) | 1;
    } else {
        return (((uint8_t)(val)) << 4);
    }
}

static struct rr* loc_parse(char* name, long ttl, int type, char* s)
{
    struct rr_loc* rr = getmem(sizeof(*rr));
    long long      i;
    int            deg;
    int            min;
    double         sec, val;

    rr->version = 0;

    /* latitude block */
    i = extract_integer(&s, "degrees latitude", NULL);
    if (i < 0)
        return NULL;
    if (i > 90)
        return bitch("degrees latitude not in the range 0..90");
    deg = i;
    min = 0;
    sec = 0;
    if (isdigit(*s)) {
        i = extract_integer(&s, "minutes latitude", NULL);
        if (i < 0)
            return NULL;
        if (i > 59)
            return bitch("minutes latitude not in the range 0..59");
        min = i;

        if (isdigit(*s)) { /* restricted floating point, starting with a digit */
            if (extract_double(&s, "seconds latitude", &sec, 0) < 0)
                return NULL;
            if (sec < 0 || sec > 59.999)
                return bitch("seconds latitude not in the range 0..59.999");
        }
    }
    rr->latitude = sec * 1000 + .5 + min * 1000 * 60 + deg * 1000 * 60 * 60;
    if (*s == 'n' || *s == 'N') {
        s++;
        rr->latitude = 2147483648u + rr->latitude;
    } else if (*s == 's' || *s == 'S') {
        s++;
        rr->latitude = 2147483648u - rr->latitude;
    } else {
        return bitch("latitude: N or S is expected");
    }
    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        return bitch("latitude: N or S is expected");
    }
    s = skip_white_space(s);
    if (!s)
        return NULL;

    /* longitude block */
    i = extract_integer(&s, "degrees longitude", NULL);
    if (i < 0)
        return NULL;
    if (i > 180)
        return bitch("degrees longitude not in the range 0..90");
    deg = i;
    min = 0;
    sec = 0;
    if (isdigit(*s)) {
        i = extract_integer(&s, "minutes longitude", NULL);
        if (i < 0)
            return NULL;
        if (i > 59)
            return bitch("minutes longitude not in the range 0..59");
        min = i;

        if (isdigit(*s)) { /* restricted floating point, starting with a digit */
            if (extract_double(&s, "seconds longitude", &sec, 0) < 0)
                return NULL;
            if (sec < 0 || sec > 59.999)
                return bitch("seconds longitude not in the range 0..59.999");
        }
    }
    rr->longitude = sec * 1000 + .5 + min * 1000 * 60 + deg * 1000 * 60 * 60;
    if (*s == 'e' || *s == 'E') {
        s++;
        rr->longitude = 2147483648u + rr->longitude;
    } else if (*s == 'w' || *s == 'W') {
        s++;
        rr->longitude = 2147483648u - rr->longitude;
    } else {
        return bitch("longitude: E or W is expected");
    }
    if (*s && !isspace(*s) && *s != ';' && *s != ')') {
        return bitch("longitude: E or W is expected");
    }
    s = skip_white_space(s);
    if (!s)
        return NULL;

    if (extract_double(&s, "altitude", &val, 1) < 0)
        return NULL;
    if (val < -100000.00 || val > 42849672.95)
        return bitch("altitude is out of supported range");
    rr->altitude = (val + 100000.00) * 100 + 0.5;

    rr->size      = double2loc_format(1 * 100 + 0.5);      /* 1m      */
    rr->horiz_pre = double2loc_format(10000 * 100 + 0.5);  /* 10000m  */
    rr->vert_pre  = double2loc_format(10 * 100 + 0.5);     /* 10m     */

    if (*s) {
        if (extract_double(&s, "sphere size", &val, 1) < 0)
            return NULL;
        if (val < 0 || val > 90000000.00)
            return bitch("sphere size is out of supported range");
        rr->size = double2loc_format(val * 100 + 0.5);

        if (*s) {
            if (extract_double(&s, "horizontal precision", &val, 1) < 0)
                return NULL;
            if (val < 0 || val > 90000000.00)
                return bitch("horizontal precision is out of supported range");
            rr->horiz_pre = double2loc_format(val * 100 + 0.5);

            if (*s) {
                if (extract_double(&s, "vertical precision", &val, 1) < 0)
                    return NULL;
                if (val < 0 || val > 90000000.00)
                    return bitch("vertical precision is out of supported range");
                rr->vert_pre = double2loc_format(val * 100 + 0.5);
            }
        }
    }

    if (*s) {
        return bitch("garbage after valid LOC data");
    }

    return store_record(type, name, ttl, rr);
}

static const long long poweroften[10] = {
    1LL, 10LL, 100LL, 1000LL, 10000LL,
    100000LL, 1000000LL, 10000000LL, 100000000LL, 1000000000LL
};

/* Render a centimeter quantity as meters into dst: trims trailing
 * fraction zeros and any bare decimal point, appends 'm', keeps an
 * explicit sign so sub-meter negatives are not swallowed by truncation. */
static void loc_cm2str(char* dst, size_t dstsize, long long cm)
{
    const char* sign = cm < 0 ? "-" : "";
    long long   acm  = cm < 0 ? -cm : cm;
    long long   m    = acm / 100;
    int         frac = (int)(acm % 100);

    if (frac == 0)
        snprintf(dst, dstsize, "%s%lldm", sign, m);
    else if (frac % 10 == 0)
        snprintf(dst, dstsize, "%s%lld.%dm", sign, m, frac / 10);
    else
        snprintf(dst, dstsize, "%s%lld.%02dm", sign, m, frac);
}

static char* loc_human(struct rr* rrv)
{
    RRCAST(loc);
    char         s[256];
    char         alt[32], siz[32], hp[32], vp[32];
    uint32_t     v;
    unsigned int lat_d, lat_m, lat_s, lat_f;
    unsigned int lon_d, lon_m, lon_s, lon_f;
    char         ns, ew;

    if (rr->latitude >= 2147483648u) {
        ns = 'N';
        v  = rr->latitude - 2147483648u;
    } else {
        ns = 'S';
        v  = 2147483648u - rr->latitude;
    }
    lat_d = v / 3600000u;
    lat_m = (v / 60000u) % 60u;
    lat_s = (v % 60000u) / 1000u;
    lat_f = v % 1000u;

    if (rr->longitude >= 2147483648u) {
        ew = 'E';
        v  = rr->longitude - 2147483648u;
    } else {
        ew = 'W';
        v  = 2147483648u - rr->longitude;
    }
    lon_d = v / 3600000u;
    lon_m = (v / 60000u) % 60u;
    lon_s = (v % 60000u) / 1000u;
    lon_f = v % 1000u;

    loc_cm2str(alt, sizeof(alt), (long long)rr->altitude - 10000000LL);
    loc_cm2str(siz, sizeof(siz),
        (long long)(rr->size >> 4) * poweroften[rr->size & 0x0f]);
    loc_cm2str(hp, sizeof(hp),
        (long long)(rr->horiz_pre >> 4) * poweroften[rr->horiz_pre & 0x0f]);
    loc_cm2str(vp, sizeof(vp),
        (long long)(rr->vert_pre >> 4) * poweroften[rr->vert_pre & 0x0f]);

    snprintf(s, sizeof(s), "%u %u %u.%03u %c %u %u %u.%03u %c %s %s %s %s",
        lat_d, lat_m, lat_s, lat_f, ns,
        lon_d, lon_m, lon_s, lon_f, ew,
        alt, siz, hp, vp);
    return quickstrdup_temp(s);
}

static struct binary_data loc_wirerdata(struct rr* rrv)
{
    RRCAST(loc);

    return compose_binary_data("1111444", 1,
        rr->version, rr->size,
        rr->horiz_pre, rr->vert_pre,
        rr->latitude, rr->longitude, rr->altitude);
}

struct rr_methods loc_methods = { loc_parse, loc_human, loc_wirerdata, NULL, NULL };
