/*
 * Vzic - a program to convert Olson timezone database files into VZTIMEZONE
 * files compatible with the iCalendar specification (RFC2445).
 *
 * SPDX-FileCopyrightText: 2000-2001 Ximian, Inc.
 * SPDX-FileCopyrightText: 2003, Damon Chaplin <damon@ximian.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * Author: Damon Chaplin <damon@gnome.org>
 */

#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <libgen.h>

#include "vzic.h"
#include "vzic-output.h"
#include "vzic-parse.h"

/* This is the maximum line length we allow. */
#define MAX_LINE_LEN 1024

/* The maximum number of fields on a line. */
#define MAX_FIELDS 12

typedef enum
{
    ZONE_ID = 0, /* The 'Zone' at the start of the line. */
    ZONE_NAME = 1,
    ZONE_GMTOFF = 2,
    ZONE_RULES_SAVE = 3,
    ZONE_FORMAT = 4,
    ZONE_UNTIL_YEAR = 5,
    ZONE_UNTIL_MONTH = 6,
    ZONE_UNTIL_DAY = 7,
    ZONE_UNTIL_TIME = 8
} ZoneFieldNumber;

typedef enum
{
    RULE_ID = 0, /* The 'Rule' at the start of the line. */
    RULE_NAME = 1,
    RULE_FROM = 2,
    RULE_TO = 3,
    RULE_TYPE = 4,
    RULE_IN = 5,
    RULE_ON = 6,
    RULE_AT = 7,
    RULE_SAVE = 8,
    RULE_LETTER_S = 9
} RuleFieldNumber;

typedef enum
{
    LINK_ID = 0, /* The 'Link' at the start of the line. */
    LINK_FROM = 1,
    LINK_TO = 2
} LinkFieldNumber;

/* This struct contains information used while parsing the files, and is
   passed to most parsing functions. */
typedef struct _ParsingData ParsingData;
struct _ParsingData {
    /* This is the line being parsed. buffer is a copy that we break into fields
     and sub-fields as it is parsed. */
    char line[MAX_LINE_LEN];
    char buffer[MAX_LINE_LEN];

    /* These are pointers to the start of each field in buffer. */
    char *fields[MAX_FIELDS];
    int num_fields;

    /* These are just for producing error messages. */
    char *filename;
    int line_number;

    /* This is an array of ZoneData structs, 1 for each timezone read. */
    GArray *zone_data;

    /* This is a hash table of arrays of RuleData structs. As each Rule line is
     read in, a new RuleData struct is filled in and appended to the
     appropriate GArray in the hash table. */
    GHashTable *rule_data;

    /* A hash containing data on the Link lines. The keys are the timezones
     where the link is from (i.e. the timezone we will be outputting anyway)
     and the data is a GList of timezones to link to (where we will copy the
     timezone data to). */
    GHashTable *link_data;

    int max_until_year;
};

/*
 * Parsing functions, used when reading the Olson timezone data file.
 */
static void parse_fields(ParsingData *data);
static gboolean parse_zone_line(ParsingData *data);
static gboolean parse_zone_continuation_line(ParsingData *data);
static gboolean parse_zone_common(ParsingData *data,
                                  int offset);
static void parse_rule_line(ParsingData *data);
static void parse_link_line(ParsingData *data);

static int parse_year(const ParsingData *data,
                      char *field,
                      gboolean accept_only,
                      int only_value);
static int parse_month(const ParsingData *data,
                       char *field);
static DayCode parse_day(const ParsingData *data,
                         char *field,
                         int *day,
                         int *weekday);
static int parse_weekday(const ParsingData *data,
                         char *field);
static int parse_time(const ParsingData *data,
                      char *field,
                      TimeCode *time_code);
static int parse_number(const ParsingData *data,
                        char **num);
static int parse_rules_save(const ParsingData *data,
                            char *field,
                            char **rules);

static void parse_coord(const char *coord,
                        int len,
                        int *result);

void parse_olson_file(char *filename,
                      GArray *zone_data,
                      GHashTable *rule_data,
                      GHashTable *link_data,
                      int *max_until_year)
{
    ParsingData data;
    FILE *fp;
    int zone_continues = 0;

    fp = fopen(filename, "r");
    if (!fp) {
        fprintf(stderr, "Couldn't open file: %s\n", filename);
        exit(1);
    }

    data.filename = filename;
    data.zone_data = zone_data;
    data.rule_data = rule_data;
    data.link_data = link_data;
    data.max_until_year = 0;

    for (data.line_number = 0;; data.line_number++) {
        if (fgets(data.line, sizeof(data.line), fp) != data.line) {
            break;
        }

        memset(data.buffer, 0, MAX_LINE_LEN);
        snprintf(data.buffer, MAX_LINE_LEN, "%s", data.line);

        parse_fields(&data);
        if (data.num_fields == 0) {
            continue;
        }

        if (zone_continues) {
            zone_continues = parse_zone_continuation_line(&data);
        } else if (!strcmp(data.fields[0], "Zone")) {
            zone_continues = parse_zone_line(&data);
        } else if (!strcmp(data.fields[0], "Rule")) {
            parse_rule_line(&data);
        } else if (!strcmp(data.fields[0], "Link")) {
            parse_link_line(&data);
        } else if (!strcmp(data.fields[0], "Leap")) {
            /* We don't care about Leap lines. */
        } else {
            fprintf(stderr, "%s:%i: Invalid line.\n%s\n", filename,
                    data.line_number, data.line);
            exit(1);
        }
    }

    if (ferror(fp)) {
        fprintf(stderr, "Error reading file: %s\n", filename);
        exit(1);
    }

    if (zone_continues) {
        fprintf(stderr, "%s:%i: Zone continuation line expected.\n%s\n",
                filename, data.line_number, data.line);
        exit(1);
    }

    fclose(fp);

#ifdef VZIC_DEBUG_PRINT
    printf("Max UNTIL year: %i\n", data.max_until_year);
#endif
    *max_until_year = data.max_until_year;
}

/* Converts the line into fields. */
static void
parse_fields(ParsingData *data)
{
    int i;
    char *p, *s, ch;

    /* Reset all fields to NULL. */
    for (i = 0; i < MAX_FIELDS; i++) {
        data->fields[i] = 0;
    }

    data->num_fields = 0;
    p = data->buffer;

    for (;;) {
        /* Skip whitespace. */
        while (isspace(*p)) {
            p++;
        }

        /* See if we have reached the end of the line or a comment. */
        if (*p == '\0' || *p == '#') {
            break;
        }

        /* We must have another field, so save the start position. */
        data->fields[data->num_fields++] = p;

        /* Now find the end of the field. If the field contains '"' characters
       they are removed and we have to move the rest of the chars back. */
        s = p;
        for (;;) {
            ch = *p;
            if (ch == '\0' || ch == '#') {
                /* Don't move p on since this is the end of the line. */
                *s = '\0';
                break;
            } else if (isspace(ch)) {
                *s = '\0';
                p++;
                break;
            } else if (ch == '"') {
                p++;
                for (;;) {
                    ch = *p;
                    if (ch == '\0') {
                        fprintf(stderr,
                                "%s:%i: Closing quote character ('\"') missing.\n%s\n",
                                data->filename, data->line_number, data->line);
                        exit(1);
                    } else if (ch == '"') {
                        p++;
                        break;
                    } else {
                        *s++ = ch;
                    }
                    p++;
                }
            } else {
                *s++ = ch;
            }
            p++;
        }
    }

#ifdef VZIC_DEBUG_PRINT
    printf("%i fields: ", data->num_fields);
    for (i = 0; i < data->num_fields; i++) {
        printf("'%s' ", data->fields[i]);
    }
    printf("\n");
#endif
}

static gboolean
parse_zone_line(ParsingData *data)
{
    ZoneData zone;

    /* All 5 fields up to FORMAT must be present. */
    if (data->num_fields < 5 || data->num_fields > 9) {
        fprintf(stderr, "%s:%i: Invalid Zone line - %i fields.\n%s\n",
                data->filename, data->line_number, data->num_fields,
                data->line);
        exit(1);
    }

    zone.zone_name = g_strdup(data->fields[ZONE_NAME]);
    zone.zone_line_data = g_array_new(FALSE, FALSE, sizeof(ZoneLineData));

    g_array_append_val(data->zone_data, zone);

    return parse_zone_common(data, 0);
}

static gboolean
parse_zone_continuation_line(ParsingData *data)
{
    /* All 3 fields up to FORMAT must be present. */
    if (data->num_fields < 3 || data->num_fields > 7) {
        fprintf(stderr,
                "%s:%i: Invalid Zone continuation line - %i fields.\n%s\n",
                data->filename, data->line_number, data->num_fields,
                data->line);
        exit(1);
    }

    return parse_zone_common(data, -2);
}

static gboolean
parse_zone_common(ParsingData *data,
                  int offset)
{
    ZoneData *zone;
    ZoneLineData zone_line;
    TimeCode time_code;

    zone_line.stdoff_seconds = parse_time(data,
                                          data->fields[ZONE_GMTOFF + offset],
                                          &time_code);
    zone_line.save_seconds = parse_rules_save(data,
                                              data->fields[ZONE_RULES_SAVE + offset],
                                              &zone_line.rules);

    if (!VzicPureOutput) {
        /* We round the UTC offsets to the nearest minute, to be compatible with
       Outlook. This also works with -ve numbers, I think.
       -56 % 60 = -59. -61 % 60 = -1. */
        if (zone_line.stdoff_seconds >= 0) {
            zone_line.stdoff_seconds += 30;
        } else {
            zone_line.stdoff_seconds -= 29;
        }
        zone_line.stdoff_seconds -= zone_line.stdoff_seconds % 60;

        if (zone_line.save_seconds >= 0) {
            zone_line.save_seconds += 30;
        } else {
            zone_line.save_seconds -= 29;
        }
        zone_line.save_seconds -= zone_line.save_seconds % 60;
    }

    zone_line.format = g_strdup(data->fields[ZONE_FORMAT + offset]);

    if (data->num_fields - offset >= 6) {
        zone_line.until_set = TRUE;
        zone_line.until_year = parse_year(data,
                                          data->fields[ZONE_UNTIL_YEAR + offset],
                                          FALSE, 0);
        zone_line.until_month = parse_month(data,
                                            data->fields[ZONE_UNTIL_MONTH + offset]);
        zone_line.until_day_code = parse_day(data,
                                             data->fields[ZONE_UNTIL_DAY + offset],
                                             &zone_line.until_day_number,
                                             &zone_line.until_day_weekday);
        zone_line.until_time_seconds = parse_time(data,
                                                  data->fields[ZONE_UNTIL_TIME + offset],
                                                  &zone_line.until_time_code);

        /* We also want to know the maximum year used in any UNTIL value, so we
       know where to expand all the infinite Rule data to. */
        if (zone_line.until_year != YEAR_MAXIMUM && zone_line.until_year != YEAR_MINIMUM) {
            data->max_until_year = MAX(data->max_until_year, zone_line.until_year);
        }

    } else {
        zone_line.until_set = FALSE;
    }

    /* Append it to the last Zone, since that is the one we are currently
     reading. */
    zone = &g_array_index(data->zone_data, ZoneData, data->zone_data->len - 1);
    g_array_append_val(zone->zone_line_data, zone_line);

    return zone_line.until_set;
}

static void
parse_rule_line(ParsingData *data)
{
    GArray *rule_array;
    RuleData rule;
    char *name;
    TimeCode time_code;

    /* All 10 fields must be present. */
    if (data->num_fields != 10) {
        fprintf(stderr, "%s:%i: Invalid Rule line - %i fields.\n%s\n",
                data->filename, data->line_number, data->num_fields,
                data->line);
        exit(1);
    }

    name = data->fields[RULE_NAME];

    /* Create the GArray and add it to the hash table if it doesn't already
     exist. */
    rule_array = g_hash_table_lookup(data->rule_data, name);
    if (!rule_array) {
        rule_array = g_array_new(FALSE, FALSE, sizeof(RuleData));
        g_hash_table_insert(data->rule_data, g_strdup(name), rule_array);
    }

    rule.from_year = parse_year(data, data->fields[RULE_FROM], FALSE, 0);
    if (rule.from_year == YEAR_MAXIMUM) {
        fprintf(stderr, "%s:%i: Invalid Rule FROM value: '%s'\n",
                data->filename, data->line_number, data->fields[RULE_FROM]);
        exit(1);
    }

    rule.to_year = parse_year(data, data->fields[RULE_TO], TRUE,
                              rule.from_year);
    if (rule.to_year == YEAR_MINIMUM) {
        fprintf(stderr, "%s:%i: Invalid Rule TO value: %s\n",
                data->filename, data->line_number, data->fields[RULE_TO]);
        exit(1);
    }

    /* We also want to know the maximum year used in any TO/FROM value, so we
     know where to expand all the infinite Rule data to. */
    if (rule.to_year != YEAR_MAXIMUM) {
        data->max_until_year = MAX(data->max_until_year, rule.to_year);
    } else if (rule.from_year != YEAR_MINIMUM) {
        data->max_until_year = MAX(data->max_until_year, rule.from_year);
    }

    if (!strcmp(data->fields[RULE_TYPE], "-")) {
        rule.type = NULL;
    } else {
        printf("Type: %s\n", data->fields[RULE_TYPE]);
        rule.type = g_strdup(data->fields[RULE_TYPE]);
    }

    rule.in_month = parse_month(data, data->fields[RULE_IN]);
    rule.on_day_code = parse_day(data, data->fields[RULE_ON],
                                 &rule.on_day_number, &rule.on_day_weekday);
    rule.at_time_seconds = parse_time(data, data->fields[RULE_AT],
                                      &rule.at_time_code);
    rule.save_seconds = parse_time(data, data->fields[RULE_SAVE], &time_code);

    if (!strcmp(data->fields[RULE_LETTER_S], "-")) {
        rule.letter_s = NULL;
    } else {
        rule.letter_s = g_strdup(data->fields[RULE_LETTER_S]);
    }

    rule.is_shallow_copy = FALSE;

    g_array_append_val(rule_array, rule);
}

static void
parse_link_line(ParsingData *data)
{
    const char *from;
    char *to;

    /* We must have 3 fields for a Link. */
    if (data->num_fields != 3) {
        fprintf(stderr, "%s:%i: Invalid Rule line - %i fields.\n%s\n",
                data->filename, data->line_number, data->num_fields,
                data->line);
        exit(1);
    }

    from = data->fields[LINK_FROM];
    to = data->fields[LINK_TO];

#ifdef VZIC_DEBUG_PRINT
    printf("LINK FROM: %s\tTO: %s\n", from, to);
#endif

#if CREATE_SYMLINK
    int len = strnlen(to, 254);
    int dirs = 0;
    int i;
    for (i = 0; i < len; i++) {
        dirs += to[i] == '/' ? 1 : 0;
    }
    if (dirs >= 0) {
        char rel_from[255];
        char to_dir[255];
        char to_path[255];
        if (dirs == 0) {
            snprintf(rel_from, sizeof(rel_from), "%s.ics", from);
        } else if (dirs == 1) {
            snprintf(rel_from, sizeof(rel_from), "../%s.ics", from);
        } else if (dirs == 2) {
            snprintf(rel_from, sizeof(rel_from), "../../%s.ics", from);
        } else {
            return;
        }
        snprintf(to_path, sizeof(to_path), "%s/%s.ics", VzicOutputDir, to);
        snprintf(to_dir, sizeof(to_dir), "%s", to_path);
        ensure_directory_exists(dirname(to_dir));
        //printf("Creating symlink from %s to %s\n", rel_from, to_path);
        if (symlink(rel_from, to_path) != 0) {
            fprintf(stderr, "Unable to create symbolic link from %s to %s: %s\n", rel_from, to_path, strerror(errno));
            exit(1);
        }
    }
#else
    GList *zone_list;
    char *old_from;
    if (g_hash_table_lookup_extended(data->link_data, from,
                                     (gpointer *)&old_from,
                                     (gpointer *)&zone_list)) {
        from = old_from;
    } else {
        from = g_strdup(from);
        zone_list = NULL;
    }

    zone_list = g_list_prepend(zone_list, g_strdup(to));

    g_hash_table_insert(data->link_data, (gpointer)from, zone_list);
#endif
}

static int
parse_year(const ParsingData *data,
           char *field,
           gboolean accept_only,
           int only_value)
{
    size_t len;
    int year = 0;
    char *p;

    if (!field) {
        fprintf(stderr, "%s:%i: Missing year.\n%s\n", data->filename,
                data->line_number, data->line);
        exit(1);
    }

    len = strlen(field);
    if (accept_only && !strncmp(field, "only", len)) {
        return only_value;
    }
    if (len >= 2) {
        if (!strncmp(field, "maximum", len)) {
            return YEAR_MAXIMUM;
        } else if (!strncmp(field, "minimum", len)) {
            return YEAR_MINIMUM;
        }
    }

    for (p = field; *p; p++) {
        if (*p < '0' || *p > '9') {
            fprintf(stderr, "%s:%i: Invalid year: %s\n%s\n", data->filename,
                    data->line_number, field, data->line);
            exit(1);
        }

        year = year * 10 + *p - '0';
    }

    if (year < 1000 || year > 2100) {
        fprintf(stderr, "%s:%i: Strange year: %s\n%s\n", data->filename,
                data->line_number, field, data->line);
        exit(1);
    }

    return year;
}

/* Parses a month name, returning 0 (Jan) to 11 (Dec). */
static int
parse_month(const ParsingData *data,
            char *field)
{
    static const char *months[] = {"january", "february", "march", "april", "may",
                                   "june", "july", "august", "september", "october",
                                   "november", "december"};
    char *p;
    size_t len;
    int i;

    /* If the field is missing, it must be the optional UNTIL month, so we return
     0 for January. */
    if (!field) {
        return 0;
    }

    for (p = field, len = 0; *p; p++, len++) {
        *p = tolower(*p);
    }

    for (i = 0; i < 12; i++) {
        if (!strncmp(field, months[i], len)) {
            return i;
        }
    }

    fprintf(stderr, "%s:%i: Invalid month: %s\n%s\n", data->filename,
            data->line_number, field, data->line);
    exit(1);
}

/* Parses a day specifier, returning a code representing the type of match
   together with a day of the month and a weekday number (0=Sun). */
static DayCode
parse_day(const ParsingData *data,
          char *field,
          int *day,
          int *weekday)
{
    char *day_part, *p;
    DayCode day_code;

    *day = *weekday = 0;

    if (!field) {
        *day = 1;
        return DAY_SIMPLE;
    }

    if (!strncmp(field, "last", 4)) {
        *weekday = parse_weekday(data, field + 4);
        /* We set the day to the end of the month to make sorting Rules easy. */
        *day = 31;
        return DAY_LAST_WEEKDAY;
    }

    day_part = field;
    day_code = DAY_SIMPLE;

    for (p = field; *p; p++) {
        if (*p == '<' || *p == '>') {
            if (*(p + 1) == '=') {
                day_code = (*p == '<') ? DAY_WEEKDAY_ON_OR_BEFORE
                                       : DAY_WEEKDAY_ON_OR_AFTER;
                *p = '\0';
                *weekday = parse_weekday(data, field);
                day_part = p + 2;
                break;
            }

            fprintf(stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename,
                    data->line_number, field, data->line);
            exit(1);
        }
    }

    for (p = day_part; *p; p++) {
        if (*p < '0' || *p > '9') {
            fprintf(stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename,
                    data->line_number, field, data->line);
            exit(1);
        }

        *day = *day * 10 + *p - '0';
    }

    if (*day < 1 || *day > 31) {
        fprintf(stderr, "%s:%i: Invalid day: %s\n%s\n", data->filename,
                data->line_number, field, data->line);
        exit(1);
    }

    return day_code;
}

/* Parses a weekday name, returning 0 (Sun) to 6 (Sat). */
static int
parse_weekday(const ParsingData *data,
              char *field)
{
    static const char *weekdays[] = {"sunday", "monday", "tuesday", "wednesday",
                                     "thursday", "friday", "saturday"};
    char *p;
    size_t len;
    int i;

    for (p = field, len = 0; *p; p++, len++) {
        *p = tolower(*p);
    }

    for (i = 0; i < 7; i++) {
        if (!strncmp(field, weekdays[i], len)) {
            return i;
        }
    }

    fprintf(stderr, "%s:%i: Invalid weekday: %s\n%s\n", data->filename,
            data->line_number, field, data->line);
    exit(1);
}

/* Parses a time (hour + minute + second) and returns the result in seconds,
   together with a time code specifying whether it is Wall clock time,
   local standard time, or universal time.
   The time can start with a '-' in which case it will be negative. */
static int
parse_time(const ParsingData *data,
           char *field,
           TimeCode *time_code)
{
    char *p;
    int hours = 0, minutes = 0, seconds = 0, result, negative = 0;

    if (!field) {
        *time_code = TIME_WALL;
        return 0;
    }

    p = field;
    if (*p == '-') {
        p++;
        negative = 1;
    }

    hours = parse_number(data, &p);

    if (*p == ':') {
        p++;
        minutes = parse_number(data, &p);

        if (*p == ':') {
            p++;
            seconds = parse_number(data, &p);
        }
    }

    /* Japan can use a 30-hour clock, per
     https://en.wikipedia.org/wiki/Date_and_time_notation_in_Japan */
    if (hours < 0 || hours > 30 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59 || (hours >= 24 && (minutes != 0 || seconds != 0))) {
        fprintf(stderr, "%s:%i: Invalid time: %s\n%s\n", data->filename,
                data->line_number, field, data->line);
        exit(1);
    }

#ifdef VZIC_DEBUG_PRINT
    printf("Time: %s -> %i:%02i:%02i\n", field, hours, minutes, seconds);
#endif

    result = hours * 3600 + minutes * 60 + seconds;
    if (negative) {
        result = -result;
    }

    if (*p == '\0') {
        *time_code = TIME_WALL;
        return result;
    }

    if (*(p + 1) == '\0') {
        if (*p == 'w') {
            *time_code = TIME_WALL;
            return result;
        } else if (*p == 's') {
            *time_code = TIME_STANDARD;
            return result;
        } else if (*p == 'u' || *p == 'g' || *p == 'z') {
            *time_code = TIME_UNIVERSAL;
            return result;
        }
    }

    fprintf(stderr, "%s:%i: Invalid time: %s\n%s\n", data->filename,
            data->line_number, field, data->line);
    exit(1);
}

/* Parses a simple number and returns the result. The pointer argument
   is moved to the first character after the number. */
static int
parse_number(const ParsingData *data,
             char **num)
{
    char *p;
    int result;

    p = *num;

    // potential null value where '-' is specified. assume zero.
    if (!p || !*p) {
        return 0;
    }

#ifdef VZIC_DEBUG_PRINT
    printf("In parse_number p:%s\n", p);
#endif

    if (*p < '0' || *p > '9') {
        fprintf(stderr, "%s:%i: Invalid number: %s\n%s\n", data->filename,
                data->line_number, *num, data->line);
        exit(1);
    }

    result = *p++ - '0';

    while (*p >= '0' && *p <= '9') {
        result = result * 10 + *p++ - '0';
    }

    *num = p;
    return result;
}

static int
parse_rules_save(const ParsingData *data,
                 char *field,
                 char **rules)
{
    TimeCode time_code;

    *rules = NULL;

    /* Check for just "-". */
    if (field[0] == '-' && field[1] == '\0') {
        return 0;
    }

    /* Check for a time to add to local standard time. We don't care about a
     time code here, since it is just an offset. */
    if (*field == '-' || (*field >= '0' && *field <= '9')) {
        return parse_time(data, field, &time_code);
    }

    /* It must be a rules name. */
    *rules = g_strdup(field);
    return 0;
}

GHashTable *
parse_zone_tab(const char *filename)
{
    GHashTable *zones_hash;
    ZoneDescription *zone_desc;
    FILE *fp;
    char buf[4096];
    gchar **fields, *zone_name, *latitude;
    const gchar *longitude;

    fp = fopen(filename, "r");
    if (!fp) {
        fprintf(stderr, "Couldn't open file: %s\n", filename);
        exit(1);
    }

    zones_hash = g_hash_table_new(g_str_hash, g_str_equal);

    while (fgets(buf, sizeof(buf), fp)) {
        if (*buf == '#') {
            continue;
        }

        g_strchomp(buf);
        fields = g_strsplit(buf, "\t", 4);

        if (strlen(fields[0]) != 2) {
            fprintf(stderr, "Invalid zone description line: %s\n", buf);
            exit(1);
        }

        zone_name = g_strdup(fields[2]);

        zone_desc = g_new(ZoneDescription, 1);
        zone_desc->country_code[0] = fields[0][0];
        zone_desc->country_code[1] = fields[0][1];
        zone_desc->comment = (fields[3] && fields[3][0]) ? g_strdup(fields[3])
                                                         : NULL;

        /* Now parse the latitude and longitude. */
        latitude = fields[1];
        longitude = latitude + 1;
        while (*longitude != '+' && *longitude != '-') {
            longitude++;
        }

        parse_coord(latitude, longitude - latitude, zone_desc->latitude);
        parse_coord(longitude, strlen(longitude), zone_desc->longitude);

        g_hash_table_insert(zones_hash, zone_name, zone_desc);

#ifdef VZIC_DEBUG_PRINT
        g_print("Found zone: %s %i %02i %02i,%i %02i %02i\n", zone_name,
                zone_desc->latitude[0], zone_desc->latitude[1],
                zone_desc->latitude[2],
                zone_desc->longitude[0], zone_desc->longitude[1],
                zone_desc->longitude[2]);
#endif
    }

    fclose(fp);

    return zones_hash;
}

static void
parse_coord(const char *coord,
            int len,
            int *result)
{
    int degrees = 0, minutes = 0, seconds = 0;

    if (len == 5) {
        sscanf(coord + 1, "%2d%2d", &degrees, &minutes);
    } else if (len == 6) {
        sscanf(coord + 1, "%3d%2d", &degrees, &minutes);
    } else if (len == 7) {
        sscanf(coord + 1, "%2d%2d%2d", &degrees, &minutes, &seconds);
    } else if (len == 8) {
        sscanf(coord + 1, "%3d%2d%2d", &degrees, &minutes, &seconds);
    } else {
        fprintf(stderr, "Invalid coordinate: %s\n", coord);
        exit(1);
    }

    if (coord[0] == '-') {
        degrees = -degrees;
    }

    result[0] = degrees;
    result[1] = minutes;
    result[2] = seconds;
}
