/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 4 -*- */
/* weather-metar.c - Weather server functions (METAR)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see
 * <http://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <regex.h>

#define MATEWEATHER_I_KNOW_THIS_IS_UNSTABLE
#include "weather.h"
#include "weather-priv.h"

enum {
    TIME_RE,
    WIND_RE,
    VIS_RE,
    COND_RE,
    CLOUD_RE,
    TEMP_RE,
    PRES_RE,

    RE_NUM
};

/* Return time of weather report as secs since epoch UTC */
static time_t
make_time (gint utcDate, gint utcHour, gint utcMin)
{
    const time_t now = time (NULL);
    struct tm tm;

    localtime_r (&now, &tm);

    /* If last reading took place just before midnight UTC on the
     * first, adjust the date downward to allow for the month
     * change-over.  This ASSUMES that the reading won't be more than
     * 24 hrs old! */
    if ((utcDate > tm.tm_mday) && (tm.tm_mday == 1)) {
        tm.tm_mday = 0; /* mktime knows this is the last day of the previous
                         * month. */
    } else {
        tm.tm_mday = utcDate;
    }
    tm.tm_hour = utcHour;
    tm.tm_min  = utcMin;
    tm.tm_sec  = 0;

    /* mktime() assumes value is local, not UTC.  Use tm_gmtoff to compensate */
#ifdef HAVE_TM_TM_GMOFF
    return tm.tm_gmtoff + mktime (&tm);
#elif defined HAVE_TIMEZONE
    return timezone + mktime (&tm);
#endif
}

static void
metar_tok_time (gchar *tokp, WeatherInfo *info)
{
    gint day, hr, min;

    sscanf (tokp, "%2u%2u%2u", &day, &hr, &min);
    info->update = make_time (day, hr, min);
}

static void
metar_tok_wind (gchar *tokp, WeatherInfo *info)
{
    gchar sdir[4], sspd[4], sgust[4];
    gint dir, spd = -1;
    gchar *gustp;
    size_t glen;

    strncpy (sdir, tokp, 3);
    sdir[3] = 0;
    dir = (!strcmp (sdir, "VRB")) ? -1 : atoi (sdir);

    memset (sspd, 0, sizeof (sspd));
    glen = strspn (tokp + 3, CONST_DIGITS);
    strncpy (sspd, tokp + 3, glen);
    spd = atoi (sspd);
    tokp += glen + 3;

    gustp = strchr (tokp, 'G');
    if (gustp) {
        memset (sgust, 0, sizeof (sgust));
        glen = strspn (gustp + 1, CONST_DIGITS);
        strncpy (sgust, gustp + 1, glen);
        tokp = gustp + 1 + glen;
    }

    if (!strcmp (tokp, "MPS"))
        info->windspeed = WINDSPEED_MS_TO_KNOTS ((WeatherWindSpeed)spd);
    else
        info->windspeed = (WeatherWindSpeed)spd;

    if ((349 <= dir) || (dir <= 11))
        info->wind = WIND_N;
    else if ((12 <= dir) && (dir <= 33))
        info->wind = WIND_NNE;
    else if ((34 <= dir) && (dir <= 56))
        info->wind = WIND_NE;
    else if ((57 <= dir) && (dir <= 78))
        info->wind = WIND_ENE;
    else if ((79 <= dir) && (dir <= 101))
        info->wind = WIND_E;
    else if ((102 <= dir) && (dir <= 123))
        info->wind = WIND_ESE;
    else if ((124 <= dir) && (dir <= 146))
        info->wind = WIND_SE;
    else if ((147 <= dir) && (dir <= 168))
        info->wind = WIND_SSE;
    else if ((169 <= dir) && (dir <= 191))
        info->wind = WIND_S;
    else if ((192 <= dir) && (dir <= 213))
        info->wind = WIND_SSW;
    else if ((214 <= dir) && (dir <= 236))
        info->wind = WIND_SW;
    else if ((237 <= dir) && (dir <= 258))
        info->wind = WIND_WSW;
    else if ((259 <= dir) && (dir <= 281))
        info->wind = WIND_W;
    else if ((282 <= dir) && (dir <= 303))
        info->wind = WIND_WNW;
    else if ((304 <= dir) && (dir <= 326))
        info->wind = WIND_NW;
    else if ((327 <= dir) && (dir <= 348))
        info->wind = WIND_NNW;
}

static void
metar_tok_vis (gchar *tokp, WeatherInfo *info)
{
    gchar *pfrac, *pend, *psp;
    gchar sval[6];
    gint num, den, val;

    memset (sval, 0, sizeof (sval));

    if (!strcmp (tokp,"CAVOK")) {
        // "Ceiling And Visibility OK": visibility >= 10 KM
        info->visibility=10000. / VISIBILITY_SM_TO_M (1.);
        info->sky = SKY_CLEAR;
    } else if (0 != (pend = strstr (tokp, "SM"))) {
        // US observation: field ends with "SM"
        pfrac = strchr (tokp, '/');
        if (pfrac) {
            if (*tokp == 'M') {
                info->visibility = 0.001;
            } else {
                num = (*(pfrac - 1) - '0');
                strncpy (sval, pfrac + 1, pend - pfrac - 1);
                den = atoi (sval);
                info->visibility =
                    ((WeatherVisibility)num / ((WeatherVisibility)den));

                psp = strchr (tokp, ' ');
                if (psp) {
                    *psp = '\0';
                    val = atoi (tokp);
                    info->visibility += (WeatherVisibility)val;
                }
            }
        } else {
            strncpy (sval, tokp, pend - tokp);
            val = atoi (sval);
            info->visibility = (WeatherVisibility)val;
        }
    } else {
        // International observation: NNNN(DD NNNNDD)?
        // For now: use only the minimum visibility and ignore its direction
        strncpy (sval, tokp, strspn (tokp, CONST_DIGITS));
        val = atoi (sval);
        info->visibility = (WeatherVisibility)val / VISIBILITY_SM_TO_M (1.);
    }
}

static void
metar_tok_cloud (gchar *tokp, WeatherInfo *info)
{
    gchar stype[4], salt[4];

    strncpy (stype, tokp, 3);
    stype[3] = 0;
    if (strlen (tokp) == 6) {
        strncpy (salt, tokp + 3, 3);
        salt[3] = 0;
    }

    if (!strcmp (stype, "CLR")) {
        info->sky = SKY_CLEAR;
    } else if (!strcmp (stype, "SKC")) {
        info->sky = SKY_CLEAR;
    } else if (!strcmp (stype, "NSC")) {
        info->sky = SKY_CLEAR;
    } else if (!strcmp (stype, "BKN")) {
        info->sky = SKY_BROKEN;
    } else if (!strcmp (stype, "SCT")) {
        info->sky = SKY_SCATTERED;
    } else if (!strcmp (stype, "FEW")) {
        info->sky = SKY_FEW;
    } else if (!strcmp (stype, "OVC")) {
        info->sky = SKY_OVERCAST;
    }
}

static void
metar_tok_pres (gchar *tokp, WeatherInfo *info)
{
    if (*tokp == 'A') {
        gchar sintg[3], sfract[3];
        gint intg, fract;

        strncpy (sintg, tokp + 1, 2);
        sintg[2] = 0;
        intg = atoi (sintg);

        strncpy (sfract, tokp + 3, 2);
        sfract[2] = 0;
        fract = atoi (sfract);

        info->pressure = (WeatherPressure)intg + (((WeatherPressure)fract)/100.0);
    } else {  /* *tokp == 'Q' */
        gchar spres[5];
        gint pres;

        strncpy (spres, tokp + 1, 4);
        spres[4] = 0;
        pres = atoi (spres);

        info->pressure = PRESSURE_MBAR_TO_INCH ((WeatherPressure)pres);
    }
}

static void
metar_tok_temp (gchar *tokp, WeatherInfo *info)
{
    gchar *ptemp, *pdew, *psep;

    psep = strchr (tokp, '/');
    *psep = 0;
    ptemp = tokp;
    pdew = psep + 1;

    info->temp = (*ptemp == 'M') ? TEMP_C_TO_F (-atoi (ptemp + 1))
        : TEMP_C_TO_F (atoi (ptemp));
    if (*pdew) {
        info->dew = (*pdew == 'M') ? TEMP_C_TO_F (-atoi (pdew + 1))
            : TEMP_C_TO_F (atoi (pdew));
    } else {
        info->dew = -1000.0;
    }
}

static void
metar_tok_cond (gchar *tokp, WeatherInfo *info)
{
    gchar squal[3], sphen[4];
    gchar *pphen;

    if ((strlen (tokp) > 3) && ((*tokp == '+') || (*tokp == '-')))
        ++tokp;   /* FIX */

    if ((*tokp == '+') || (*tokp == '-'))
        pphen = tokp + 1;
    else if (strlen (tokp) < 4)
        pphen = tokp;
    else
        pphen = tokp + 2;

    memset (squal, 0, sizeof (squal));
    strncpy (squal, tokp, pphen - tokp);
    squal[pphen - tokp] = 0;

    memset (sphen, 0, sizeof (sphen));
    strncpy (sphen, pphen, sizeof (sphen));
    sphen[sizeof (sphen)-1] = '\0';

    /* Defaults */
    info->cond.qualifier = QUALIFIER_NONE;
    info->cond.phenomenon = PHENOMENON_NONE;
    info->cond.significant = FALSE;

    if (!strcmp (squal, "")) {
        info->cond.qualifier = QUALIFIER_MODERATE;
    } else if (!strcmp (squal, "-")) {
        info->cond.qualifier = QUALIFIER_LIGHT;
    } else if (!strcmp (squal, "+")) {
        info->cond.qualifier = QUALIFIER_HEAVY;
    } else if (!strcmp (squal, "VC")) {
        info->cond.qualifier = QUALIFIER_VICINITY;
    } else if (!strcmp (squal, "MI")) {
        info->cond.qualifier = QUALIFIER_SHALLOW;
    } else if (!strcmp (squal, "BC")) {
        info->cond.qualifier = QUALIFIER_PATCHES;
    } else if (!strcmp (squal, "PR")) {
        info->cond.qualifier = QUALIFIER_PARTIAL;
    } else if (!strcmp (squal, "TS")) {
        info->cond.qualifier = QUALIFIER_THUNDERSTORM;
    } else if (!strcmp (squal, "BL")) {
        info->cond.qualifier = QUALIFIER_BLOWING;
    } else if (!strcmp (squal, "SH")) {
        info->cond.qualifier = QUALIFIER_SHOWERS;
    } else if (!strcmp (squal, "DR")) {
        info->cond.qualifier = QUALIFIER_DRIFTING;
    } else if (!strcmp (squal, "FZ")) {
        info->cond.qualifier = QUALIFIER_FREEZING;
    } else {
        return;
    }

    if (!strcmp (sphen, "DZ")) {
        info->cond.phenomenon = PHENOMENON_DRIZZLE;
    } else if (!strcmp (sphen, "RA")) {
        info->cond.phenomenon = PHENOMENON_RAIN;
    } else if (!strcmp (sphen, "SN")) {
        info->cond.phenomenon = PHENOMENON_SNOW;
    } else if (!strcmp (sphen, "SG")) {
        info->cond.phenomenon = PHENOMENON_SNOW_GRAINS;
    } else if (!strcmp (sphen, "IC")) {
        info->cond.phenomenon = PHENOMENON_ICE_CRYSTALS;
    } else if (!strcmp (sphen, "PE")) {
        info->cond.phenomenon = PHENOMENON_ICE_PELLETS;
    } else if (!strcmp (sphen, "GR")) {
        info->cond.phenomenon = PHENOMENON_HAIL;
    } else if (!strcmp (sphen, "GS")) {
        info->cond.phenomenon = PHENOMENON_SMALL_HAIL;
    } else if (!strcmp (sphen, "UP")) {
        info->cond.phenomenon = PHENOMENON_UNKNOWN_PRECIPITATION;
    } else if (!strcmp (sphen, "BR")) {
        info->cond.phenomenon = PHENOMENON_MIST;
    } else if (!strcmp (sphen, "FG")) {
        info->cond.phenomenon = PHENOMENON_FOG;
    } else if (!strcmp (sphen, "FU")) {
        info->cond.phenomenon = PHENOMENON_SMOKE;
    } else if (!strcmp (sphen, "VA")) {
        info->cond.phenomenon = PHENOMENON_VOLCANIC_ASH;
    } else if (!strcmp (sphen, "SA")) {
        info->cond.phenomenon = PHENOMENON_SAND;
    } else if (!strcmp (sphen, "HZ")) {
        info->cond.phenomenon = PHENOMENON_HAZE;
    } else if (!strcmp (sphen, "PY")) {
        info->cond.phenomenon = PHENOMENON_SPRAY;
    } else if (!strcmp (sphen, "DU")) {
        info->cond.phenomenon = PHENOMENON_DUST;
    } else if (!strcmp (sphen, "SQ")) {
        info->cond.phenomenon = PHENOMENON_SQUALL;
    } else if (!strcmp (sphen, "SS")) {
        info->cond.phenomenon = PHENOMENON_SANDSTORM;
    } else if (!strcmp (sphen, "DS")) {
        info->cond.phenomenon = PHENOMENON_DUSTSTORM;
    } else if (!strcmp (sphen, "PO")) {
        info->cond.phenomenon = PHENOMENON_DUST_WHIRLS;
    } else if (!strcmp (sphen, "+FC")) {
        info->cond.phenomenon = PHENOMENON_TORNADO;
    } else if (!strcmp (sphen, "FC")) {
        info->cond.phenomenon = PHENOMENON_FUNNEL_CLOUD;
    } else {
        return;
    }

    if ((info->cond.qualifier != QUALIFIER_NONE) || (info->cond.phenomenon != PHENOMENON_NONE))
        info->cond.significant = TRUE;
}

#define TIME_RE_STR  "([0-9]{6})Z"
#define WIND_RE_STR  "(([0-9]{3})|VRB)([0-9]?[0-9]{2})(G[0-9]?[0-9]{2})?(KT|MPS)"
#define VIS_RE_STR   "((([0-9]?[0-9])|(M?([12] )?([1357]/1?[0-9])))SM)|" \
    "([0-9]{4}(N|NE|E|SE|S|SW|W|NW( [0-9]{4}(N|NE|E|SE|S|SW|W|NW))?)?)|" \
    "CAVOK"
#define COND_RE_STR  "(-|\\+)?(VC|MI|BC|PR|TS|BL|SH|DR|FZ)?(DZ|RA|SN|SG|IC|PE|GR|GS|UP|BR|FG|FU|VA|SA|HZ|PY|DU|SQ|SS|DS|PO|\\+?FC)"
#define CLOUD_RE_STR "((CLR|BKN|SCT|FEW|OVC|SKC|NSC)([0-9]{3}|///)?(CB|TCU|///)?)"
#define TEMP_RE_STR  "(M?[0-9][0-9])/(M?(//|[0-9][0-9])?)"
#define PRES_RE_STR  "(A|Q)([0-9]{4})"

/* POSIX regular expressions do not allow us to express "match whole words
 * only" in a simple way, so we have to wrap them all into
 *   (^| )(...regex...)( |$)
 */
#define RE_PREFIX "(^| )("
#define RE_SUFFIX ")( |$)"

static regex_t metar_re[RE_NUM];
static void (*metar_f[RE_NUM]) (gchar *tokp, WeatherInfo *info);

static void
metar_init_re (void)
{
    static gboolean initialized = FALSE;
    if (initialized)
        return;
    initialized = TRUE;

    regcomp (&metar_re[TIME_RE], RE_PREFIX TIME_RE_STR RE_SUFFIX, REG_EXTENDED);
    regcomp (&metar_re[WIND_RE], RE_PREFIX WIND_RE_STR RE_SUFFIX, REG_EXTENDED);
    regcomp (&metar_re[VIS_RE], RE_PREFIX VIS_RE_STR RE_SUFFIX, REG_EXTENDED);
    regcomp (&metar_re[COND_RE], RE_PREFIX COND_RE_STR RE_SUFFIX, REG_EXTENDED);
    regcomp (&metar_re[CLOUD_RE], RE_PREFIX CLOUD_RE_STR RE_SUFFIX, REG_EXTENDED);
    regcomp (&metar_re[TEMP_RE], RE_PREFIX TEMP_RE_STR RE_SUFFIX, REG_EXTENDED);
    regcomp (&metar_re[PRES_RE], RE_PREFIX PRES_RE_STR RE_SUFFIX, REG_EXTENDED);

    metar_f[TIME_RE] = metar_tok_time;
    metar_f[WIND_RE] = metar_tok_wind;
    metar_f[VIS_RE] = metar_tok_vis;
    metar_f[COND_RE] = metar_tok_cond;
    metar_f[CLOUD_RE] = metar_tok_cloud;
    metar_f[TEMP_RE] = metar_tok_temp;
    metar_f[PRES_RE] = metar_tok_pres;
}

gboolean
metar_parse (gchar *metar, WeatherInfo *info)
{
    gchar *p;
    //gchar *rmk;
    gint i, i2;
    regmatch_t rm, rm2;
    gchar *tokp;

    g_return_val_if_fail (info != NULL, FALSE);
    g_return_val_if_fail (metar != NULL, FALSE);

    metar_init_re ();

    /*
     * Force parsing to end at "RMK" field.  This prevents a subtle
     * problem when info within the remark happens to match an earlier state
     * and, as a result, throws off all the remaining expression
     */
    if (0 != (p = strstr (metar, " RMK "))) {
        *p = '\0';
        //rmk = p + 5;   // uncomment this if RMK data becomes useful
    }

    p = metar;
    i = TIME_RE;
    while (*p) {

        i2 = RE_NUM;
        rm2.rm_so = strlen (p);
        rm2.rm_eo = rm2.rm_so;

        for (i = 0; i < RE_NUM && rm2.rm_so > 0; i++) {
            if (0 == regexec (&metar_re[i], p, 1, &rm, 0)
                && rm.rm_so < rm2.rm_so)
            {
                i2 = i;
                /* Skip leading and trailing space characters, if present.
                   (the regular expressions include those characters to
                   only get matches limited to whole words). */
                if (p[rm.rm_so] == ' ') rm.rm_so++;
                if (p[rm.rm_eo - 1] == ' ') rm.rm_eo--;
                rm2.rm_so = rm.rm_so;
                rm2.rm_eo = rm.rm_eo;
            }
        }

        if (i2 != RE_NUM) {
            tokp = g_strndup (p + rm2.rm_so, rm2.rm_eo - rm2.rm_so);
            metar_f[i2] (tokp, info);
            g_free (tokp);
        }

        p += rm2.rm_eo;
        p += strspn (p, " ");
    }
    return TRUE;
}

static void
metar_finish (SoupSession *session, SoupMessage *msg, gpointer data)
{
    WeatherInfo *info = (WeatherInfo *)data;
    WeatherLocation *loc;
    const gchar *p, *endtag;
    gchar *searchkey, *metar;
    gboolean success = FALSE;

    g_return_if_fail (info != NULL);

    if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
        if (SOUP_STATUS_IS_TRANSPORT_ERROR (msg->status_code))
            info->network_error = TRUE;
        else {
            /* Translators: %d is an error code, and %s the error string */
            g_warning (_("Failed to get METAR data: %d %s.\n"),
                       msg->status_code, msg->reason_phrase);
        }
        request_done (info, FALSE);
        return;
    }

    loc = info->location;

    searchkey = g_strdup_printf ("<raw_text>%s", loc->code);
    p = strstr (msg->response_body->data, searchkey);
    g_free (searchkey);
    if (p) {
        p += WEATHER_LOCATION_CODE_LEN + 11;
        endtag = strstr (p, "</raw_text>");
        if (endtag)
            metar = g_strndup (p, endtag - p);
        else
            metar = g_strdup (p);
        success = metar_parse (metar, info);
        g_free (metar);
    } else if (!strstr (msg->response_body->data, "aviationweather.gov")) {
        /* The response doesn't even seem to have come from NOAA...
         * most likely it is a wifi hotspot login page. Call that a
         * network error.
         */
        info->network_error = TRUE;
    }

    info->valid = success;
    request_done (info, TRUE);
}

/* Read current conditions and fill in info structure */
void
metar_start_open (WeatherInfo *info)
{
    WeatherLocation *loc;
    SoupMessage *msg;

    g_return_if_fail (info != NULL);
    info->valid = info->network_error = FALSE;
    loc = info->location;
    if (loc == NULL) {
        g_warning (_("WeatherInfo missing location"));
        return;
    }

    msg = soup_form_request_new (
        "GET", "https://www.aviationweather.gov/adds/dataserver_current/httpparam",
        "dataSource", "metars",
        "requestType", "retrieve",
        "format", "xml",
        "hoursBeforeNow", "3",
        "mostRecent", "true",
        "fields", "raw_text",
        "stationString", loc->code,
        NULL);
    soup_session_queue_message (info->session, msg, metar_finish, info);

    info->requests_pending++;
}
