/* ctl_zoneinfo.c -- Program to perform operations on zoneinfo db
 *
 * Copyright (c) 1994-2013 Carnegie Mellon University.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The name "Carnegie Mellon University" must not be used to
 *    endorse or promote products derived from this software without
 *    prior written permission. For permission or any legal
 *    details, please contact
 *      Carnegie Mellon University
 *      Center for Technology Transfer and Enterprise Creation
 *      4615 Forbes Avenue
 *      Suite 302
 *      Pittsburgh, PA  15213
 *      (412) 268-7393, fax: (412) 268-7395
 *      innovation@andrew.cmu.edu
 *
 * 4. Redistributions of any form whatsoever must retain the following
 *    acknowledgment:
 *    "This product includes software developed by Computing Services
 *     at Carnegie Mellon University (http://www.cmu.edu/computing/)."
 *
 * CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
 * THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
 * FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
 * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

#include <config.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <errno.h>
#include <getopt.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sysexits.h>
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <libical/ical.h>
#include <libxml/tree.h>

#include "annotate.h" /* for strlist functionality */
#include "global.h"
#include "hash.h"
#include "map.h"
#include "util.h"
#include "xmalloc.h"
#include "xml_support.h"
#include "zoneinfo_db.h"

/* config.c stuff */
const int config_need_data = 0;

static int verbose = 0;

/* forward declarations */
void usage(void);
void free_zoneinfo(void *data);
void store_zoneinfo(const char *tzid, void *data, void *rock);
void do_zonedir(const char *prefix, struct hash_table *tzentries,
                struct zoneinfo *info);
void shut_down(int code);


int main(int argc, char **argv)
{
    int opt, r = 0;
    char *alt_config = NULL, *pub = NULL, *ver = NULL, *winfile = NULL;
    const char *zoneinfo_dir = NULL;
    enum { REBUILD, WINZONES, NONE } op = NONE;

    /* keep this in alphabetical order */
    static const char short_options[] = "C:r:vw:";

    static const struct option long_options[] = {
        /* n.b. no long option for -C */
        { "rebuild", required_argument, NULL, 'r' },
        { "verbose", no_argument, NULL, 'v' },
        { "windows-zone-xml", required_argument, NULL, 'w' },
        { 0, 0, 0, 0 },
    };

    while (-1 != (opt = getopt_long(argc, argv,
                                    short_options, long_options, NULL)))
    {
        switch (opt) {
        case 'C': /* alt config file */
            alt_config = optarg;
            break;

        case 'r':
            if (op == NONE) {
                op = REBUILD;
                pub = optarg;
                ver = strchr(optarg, ':');
                if (ver) *ver++ = '\0';
                else usage();
            }
            else usage();
            break;

        case 'v':
            verbose = 1;
            break;

        case 'w':
            if (op == NONE) {
                op = WINZONES;
                winfile = optarg;
            }
            else usage();
            break;

        default:
            usage();
        }
    }

    cyrus_init(alt_config, "ctl_zoneinfo", 0, 0);

    signals_set_shutdown(&shut_down);
    signals_add_handlers(0);

    zoneinfo_dir = config_getstring(IMAPOPT_ZONEINFO_DIR);
    if (!zoneinfo_dir) {
        fprintf(stderr, "zoneinfo_dir must be set for tzdist service\n");
        cyrus_done();
        return EX_CONFIG;
    }

    switch (op) {
    case REBUILD: {
        struct hash_table tzentries;
        struct zoneinfo *info;
        struct txn *tid = NULL;
        char buf[3000];
        FILE *fp;

        construct_hash_table(&tzentries, 500, 1);

        /* Add INFO record (overall lastmod and TZ DB source version) */
        info = xzmalloc(sizeof(struct zoneinfo));
        info->type = ZI_INFO;
        appendstrlist(&info->data, pub);
        appendstrlist(&info->data, ver);
        hash_insert(INFO_TZID, info, &tzentries);

        /* Add LEAP record (last updated and hash) */
        snprintf(buf, sizeof(buf), "%s%s", zoneinfo_dir, FNAME_LEAPSECFILE);
        if (verbose) printf("Processing leap seconds file %s\n", buf);
        if (!(fp = fopen(buf, "r"))) {
            fprintf(stderr, "Could not open leap seconds file %s\n", buf);
        }
        else {
            struct zoneinfo *leap = xzmalloc(sizeof(struct zoneinfo));
            leap->type = ZI_INFO;

            while(fgets(buf, sizeof(buf), fp)) {
                if (buf[0] == '#') {
                    /* comment line */

                    if (buf[1] == '$') {
                        /* last updated */
                        unsigned long last;

                        sscanf(buf+2, "\t%lu", &last);
                        leap->dtstamp = last - NIST_EPOCH_OFFSET;
                    }
                    else if (buf[1] == 'h') {
                        /* hash */
                        char *p, *hash = buf+3 /* skip "#h\t" */;

                        /* trim trailing whitespace */
                        for (p = hash + strlen(hash); isspace(*--p); *p = '\0');
                        appendstrlist(&leap->data, hash);
                    }
                }
            }
            fclose(fp);

            hash_insert(LEAP_TZID, leap, &tzentries);
            info->dtstamp = leap->dtstamp;
        }

        /* Add ZONE/LINK records */
        do_zonedir(zoneinfo_dir, &tzentries, info);

        zoneinfo_open(NULL);

        /* Store records */
        hash_enumerate(&tzentries, &store_zoneinfo, &tid);

        zoneinfo_close(tid);

        free_hash_table(&tzentries, &free_zoneinfo);
        break;
    }

    case WINZONES: {
        xmlParserCtxtPtr ctxt;
        xmlDocPtr doc;
        xmlNodePtr node;
        struct buf tzidbuf = BUF_INITIALIZER;
        struct buf aliasbuf = BUF_INITIALIZER;

        if (verbose) printf("Processing Windows Zone file %s\n", winfile);

        /* Parse the XML file */
        ctxt = xmlNewParserCtxt();
        if (!ctxt) {
            fprintf(stderr, "Failed to create XML parser context\n");
            break;
        }

        doc = xmlCtxtReadFile(ctxt, winfile, NULL, 0);
        xmlFreeParserCtxt(ctxt);
        if (!doc) {
            fprintf(stderr, "Failed to parse XML document\n");
            break;
        }

        node = xmlDocGetRootElement(doc);
        if (!node || xmlStrcmp(node->name, BAD_CAST "supplementalData")) {
            fprintf(stderr, "Incorrect root node\n");
            goto done;
        }

        for (node = xmlFirstElementChild(node);
             node && xmlStrcmp(node->name, BAD_CAST "windowsZones");
             node = xmlNextElementSibling(node));
        if (!node) {
            fprintf(stderr, "Missing windowsZones node\n");
            goto done;
        }

        node = xmlFirstElementChild(node);
        if (!node || xmlStrcmp(node->name, BAD_CAST "mapTimezones")) {
            fprintf(stderr, "Missing mapTimezones node\n");
            goto done;
        }

        if (chdir(zoneinfo_dir)) {
            fprintf(stderr, "chdir(%s) failed\n", zoneinfo_dir);
            goto done;
        }

        for (node = xmlFirstElementChild(node);
             node;
             node = xmlNextElementSibling(node)) {
            if (!xmlStrcmp(node->name, BAD_CAST "mapZone") &&
                !xmlStrcmp(xmlGetProp(node, BAD_CAST "territory"),
                           BAD_CAST "001")) {
                const char *tzid, *alias;

                buf_setcstr(&tzidbuf,
                            (const char *) xmlGetProp(node, BAD_CAST "type"));
                buf_appendcstr(&tzidbuf, ".ics");
                tzid = buf_cstring(&tzidbuf);
                buf_setcstr(&aliasbuf,
                            (const char *) xmlGetProp(node, BAD_CAST "other"));
                buf_appendcstr(&aliasbuf, ".ics");
                alias = buf_cstring(&aliasbuf);

                if (verbose) printf("\tLINK: %s -> %s\n", alias, tzid);

                if (symlink(tzid, alias)) {
                    if (errno == EEXIST) {
                        struct stat sbuf;

                        if (stat(alias, &sbuf)) {
                            fprintf(stderr, "stat(%s) failed: %s\n",
                                    alias, strerror(errno));
                            errno = EEXIST;
                        }
                        else if (sbuf.st_mode & S_IFLNK) {
                            char link[MAX_MAILBOX_PATH+1];
                            int n = readlink(alias, link, MAX_MAILBOX_PATH);

                            if (n == -1) {
                                fprintf(stderr, "readlink(%s) failed: %s\n",
                                        alias, strerror(errno));
                                errno = EEXIST;
                            }
                            else if (n == (int) strlen(tzid) &&
                                     !strncmp(tzid, link, n)) {
                                errno = 0;
                            }
                        }
                    }

                    if (errno) {
                        fprintf(stderr, "symlink(%s, %s) failed: %s\n",
                                tzid, alias, strerror(errno));
                    }
                }
            }
        }

  done:
        buf_free(&aliasbuf);
        buf_free(&tzidbuf);
        xmlFreeDoc(doc);
        break;
    }

    case NONE:
        r = 2;
        usage();
        break;
    }

    cyrus_done();

    return r;
}


void usage(void)
{
    fprintf(stderr,
            "usage: ctl_zoneinfo [-C <alt_config>] [-v]"
            " -r <publisher>:<version> | -w <file>\n");
    exit(EX_USAGE);
}


/* Add all ZONEs and LINKs in the given directory to the hash table */
void do_zonedir(const char *dir, struct hash_table *tzentries,
                struct zoneinfo *info)
{
    DIR *dirp;
    struct dirent *dirent;

    signals_poll();

    if (verbose) printf("Rebuilding %s\n", dir);

    dirp = opendir(dir);
    if (!dirp) {
        fprintf(stderr, "can't open zoneinfo directory %s\n", dir);
        return;
    }

    while ((dirent = readdir(dirp))) {
        char path[2048], *tzid;
        int plen;
        struct stat sbuf;
        struct zoneinfo *zi;

        if (*dirent->d_name == '.') continue;

        plen = snprintf(path, sizeof(path), "%s/%s", dir, dirent->d_name);
        lstat(path, &sbuf);

        if (S_ISDIR(sbuf.st_mode)) {
            /* Path is a directory (region) */
          do_zonedir(path, tzentries, info);
        }
        else if (S_ISLNK(sbuf.st_mode)) {
            /* Path is a symlink (alias) */
            char link[1024], *alias;
            ssize_t llen;

            /* Isolate tzid in path */
            if ((llen = readlink(path, link, sizeof(link))) < 0) continue;
            link[llen-4] = '\0';  /* Trim ".ics" */
            for (tzid = link; !strncmp(tzid, "../", 3); tzid += 3);

            /* Isolate alias in path */
            path[plen-4] = '\0';  /* Trim ".ics" */
            alias = path + strlen(dir) + 1;

            if (verbose) printf("\tLINK: %s -> %s\n", alias, tzid);

            /* Create hash entry for alias */
            if (!(zi = hash_lookup(alias, tzentries))) {
                zi = xzmalloc(sizeof(struct zoneinfo));
                hash_insert(alias, zi, tzentries);
            }
            zi->type = ZI_LINK;
            appendstrlist(&zi->data, tzid);

            /* Create/update hash entry for tzid */
            if (!(zi = hash_lookup(tzid, tzentries))) {
                zi = xzmalloc(sizeof(struct zoneinfo));
                hash_insert(tzid, zi, tzentries);
            }
            zi->type = ZI_ZONE;
            appendstrlist(&zi->data, alias);
        }
        else if (S_ISREG(sbuf.st_mode)) {
            /* Path is a regular file (zone) */
            int fd;
            const char *base = NULL;
            size_t len = 0;
            icalcomponent *ical, *comp;
            icalproperty *prop;
            char *alias = NULL;

            /* Parse the iCalendar file for important properties */
            if ((fd = open(path, O_RDONLY)) == -1) continue;
            map_refresh(fd, 1, &base, &len, MAP_UNKNOWN_LEN, path, NULL);
            close(fd);

            ical = icalparser_parse_string(base);
            map_free(&base, &len);

            if (!ical) continue;  /* skip non-iCalendar files */

            comp = icalcomponent_get_first_component(ical,
                                                     ICAL_VTIMEZONE_COMPONENT);
            prop = icalcomponent_get_first_property(comp, ICAL_TZID_PROPERTY);
            tzid = (char *) icalproperty_get_value_as_string(prop);

            prop = icalcomponent_get_first_property(comp,
                                                    ICAL_TZIDALIASOF_PROPERTY);
            if (prop) {
                alias = tzid;
                tzid = (char *) icalproperty_get_value_as_string(prop);

                if (verbose) printf("\tLINK: %s -> %s\n", alias, tzid);
            }
            else if (verbose) printf("\tZONE: %s\n", tzid);

            /* Create/update hash entry for tzid */
            if (!(zi = hash_lookup(tzid, tzentries))) {
                zi = xzmalloc(sizeof(struct zoneinfo));
                hash_insert(tzid, zi, tzentries);
            }
            zi->type = ZI_ZONE;
            prop = icalcomponent_get_first_property(comp,
                                                    ICAL_LASTMODIFIED_PROPERTY);
            zi->dtstamp = icaltime_as_timet(icalproperty_get_lastmodified(prop));

            icalcomponent_free(ical);

            /* Check overall lastmod */
            if (zi->dtstamp > info->dtstamp) info->dtstamp = zi->dtstamp;

            if (alias) {
                /* Add alias to the list for this tzid */
                appendstrlist(&zi->data, alias);

                /* Create hash entry for alias */
                if (!(zi = hash_lookup(alias, tzentries))) {
                    zi = xzmalloc(sizeof(struct zoneinfo));
                    hash_insert(alias, zi, tzentries);
                }
                zi->type = ZI_LINK;
                appendstrlist(&zi->data, tzid);
            }
        }
        else {
            fprintf(stderr, "unknown path type %s\n", path);
        }
    }

    closedir(dirp);
}


/* Free a malloc'd struct zoneinfo */
void free_zoneinfo(void *data)
{
    struct zoneinfo *zi = (struct zoneinfo *) data;

    freestrlist(zi->data);
    free(zi);
}


/* Store a struct zoneinfo into zoneinfo.db using the given txn */
void store_zoneinfo(const char *tzid, void *data, void *rock)
{
    struct zoneinfo *zi = (struct zoneinfo *) data;
    struct txn **tid = (struct txn **) rock;

    zoneinfo_store(tzid, zi, tid);
}


/*
 * Cleanly shut down and exit
 */
void shut_down(int code) __attribute__((noreturn));
void shut_down(int code)
{
    in_shutdown = 1;

    exit(code);
}
