/*======================================================================
 FILE: icalbdbset.c

 (C) COPYRIGHT 2001, Critical Path

 This library is free software; you can redistribute it and/or modify
 it under the terms of either:

    The LGPL as published by the Free Software Foundation, version
    2.1, available at: https://www.gnu.org/licenses/lgpl-2.1.html

 Or:

    The Mozilla Public License Version 2.0. You may obtain a copy of
    the License at https://www.mozilla.org/MPL/
======================================================================*/

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

#include "icalbdbset.h"
#include "icalbdbsetimpl.h"

#include "icalparser.h"
#include "icaltimezone.h"
#include "icalvalue.h"

#include <errno.h>
#include <stdlib.h>

#define MAX_RETRY 5

#if !defined(DB_VERSION_MAJOR)
#define DB_VERSION_MAJOR 1 //assume ancient version
#endif

static int _compare_ids(const char *compid, const char *matchid);
#if DB_VERSION_MAJOR > 5
static int _compare_keys(DB *dbp, const DBT *a, const DBT *b, size_t *locp);
#else
static int _compare_keys(DB *dbp, const DBT *a, const DBT *b);
#endif

/** Default options used when NULL is passed to icalset_new() **/
static icalbdbset_options icalbdbset_options_default =
    {
      ICALBDB_EVENTS, DB_BTREE, 0644, 0, NULL, NULL
    };

static DB_ENV *ICAL_DB_ENV = 0;

/** Initialize the db environment */

int icalbdbset_init_dbenv(char *db_env_dir,
                          void (*logDbFunc) (const DB_ENV *, const char *, const char *))
{
    int ret;
    u_int32_t flags;

    if (db_env_dir) {
        struct stat env_dir_sb;

        if (stat(db_env_dir, &env_dir_sb)) {
            fprintf(stderr, "The directory '%s' is missing, please create it.\n", db_env_dir);
            return EINVAL;
        }
    }

    ret = db_env_create(&ICAL_DB_ENV, 0);

    if (ret) {
        /* some kind of error... */
        return ret;
    }

    /* Do deadlock detection internally */
    if ((ret = ICAL_DB_ENV->set_lk_detect(ICAL_DB_ENV, DB_LOCK_DEFAULT)) != 0) {
        /*char *foo = db_strerror(ret); */
        fprintf(stderr, "Could not initialize the database locking environment\n");
        return ret;
    }

    flags = (u_int32_t) (DB_INIT_LOCK | DB_INIT_TXN | DB_CREATE | DB_THREAD |
                         DB_RECOVER | DB_INIT_LOG | DB_INIT_MPOOL);
#if defined(_WIN32) //krazy:exclude=cpp
    ret = ICAL_DB_ENV->open(ICAL_DB_ENV, db_env_dir, flags, 0 /*ignored on Windows*/);
#else
    ret = ICAL_DB_ENV->open(ICAL_DB_ENV, db_env_dir, flags, S_IRUSR | S_IWUSR);
#endif

    if (ret) {
        /*char *foo = db_strerror(ret); */
        ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "dbenv->open");
        return ret;
    }

    /* display additional error messages */
    if (logDbFunc != NULL) {
        ICAL_DB_ENV->set_errcall(ICAL_DB_ENV, logDbFunc);
    }

    return ret;
}

void icalbdbset_checkpoint(void)
{
    int ret;

    switch (ret = ICAL_DB_ENV->txn_checkpoint(ICAL_DB_ENV, 0, 0, 0)) {
    case 0:
        break;
    default:
        ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "checkpoint failed");
        abort();
    }
}

void icalbdbset_rmdbLog(void)
{
    char **listp;

    /* remove log files that are can be archived (ie. no longer needed) */
    if (ICAL_DB_ENV->log_archive(ICAL_DB_ENV, &listp, DB_ARCH_ABS) == 0) {
        if (*listp != NULL) {
            int ii = 0;

            while (listp[ii] != NULL) {
                (void)unlink(listp[ii]);
                ii++;
            }
            free(listp);
        }
    }
}

int icalbdbset_cleanup(void)
{
    int ret = 0;

    /* one last checkpoint.. */
    icalbdbset_checkpoint();

    /* remove logs that are not needed anymore */
    icalbdbset_rmdbLog();

    if (ICAL_DB_ENV) {
        ret = ICAL_DB_ENV->close(ICAL_DB_ENV, 0);
    }

    return ret;
}

DB_ENV *icalbdbset_get_env(void)
{
    return ICAL_DB_ENV;
}

/** Initialize an icalbdbset.  Also attempts to populate from the
 *  database (primary if only dbp is given, secondary if sdbp is
 *  given) and creates an empty object if retrieval is unsuccessful.
 *  pfunc is used to unpack data from the database.  If not given, we
 *  assume data is a string.
 */

/* This populates a cluster with the entire contents of a database */
static icalerrorenum icalbdbset_read_database(icalbdbset *bset, char *(*pfunc) (const DBT *dbt))
{
    DB *dbp;
    DBC *dbcp;
    DBT key, data;
    char *str;
    int ret = EINVAL;
    char keystore[256];
    char datastore[1024];
    char *more_mem = NULL;
    DB_TXN *tid;

    _unused(pfunc);

    memset(&key, 0, sizeof(DBT));
    memset(&data, 0, sizeof(DBT));

    if (bset->sdbp) {
        dbp = bset->sdbp;
    } else {
        dbp = bset->dbp;
    }

    if (!dbp) {
        return ICAL_FILE_ERROR;
    }

    bset->cluster = icalcomponent_new(ICAL_XROOT_COMPONENT);

    if (ICAL_DB_ENV->txn_begin(ICAL_DB_ENV, NULL, &tid, 0) != 0) {
        abort();
    }

    /* acquire a cursor for the database */
    if ((ret = dbp->cursor(dbp, tid, &dbcp, 0)) != 0) {
        dbp->err(dbp, ret, "primary index");
        goto err1;
    }

    key.flags = DB_DBT_USERMEM;
    key.data = keystore;
    key.ulen = (u_int32_t) sizeof(keystore);

    data.flags = DB_DBT_USERMEM;
    data.data = datastore;
    data.ulen = (u_int32_t) sizeof(datastore);

    /* fetch the key/data pair */
    while (1) {
        ret = dbcp->c_get(dbcp, &key, &data, DB_NEXT);
        if (ret == DB_NOTFOUND) {
            break;
        } else if (ret == ENOMEM) {
            if (more_mem) {
                free(more_mem);
            }
            more_mem = malloc(data.ulen + 1024);
            data.data = more_mem;
            data.ulen = data.ulen + 1024;
        } else if (ret == DB_LOCK_DEADLOCK) {
            /*char *foo = db_strerror(ret); */
            abort();    /* should retry in case of DB_LOCK_DEADLOCK */
        } else if (ret) {
            /*char *foo = db_strerror(ret); */
            /* some other weird-ass error  */
            dbp->err(dbp, ret, "cursor");
            abort();
        } else {
            icalcomponent *cl;

            /* this prevents an array read bounds error */
            if ((str = (char *)calloc(data.size + 1, sizeof(char))) == NULL) {
                goto err2;
            }
            memcpy(str, (char *)data.data, data.size);

            cl = icalparser_parse_string(str);

            icalcomponent_add_component(bset->cluster, cl);
            free(str);
        }
    }
    if (ret != DB_NOTFOUND) {
        goto err2;
    }

    if (more_mem) {
        free(more_mem);
        more_mem = NULL;
    }

    if (dbcp->c_close(dbcp) != 0) {
        abort();        /* should retry in case of DB_LOCK_DEADLOCK */
    }

    if (tid->commit(tid, 0) != 0) {
        abort();
    }

    return ICAL_NO_ERROR;

  err2:
    if (more_mem) {
        free(more_mem);
    }
    dbcp->c_close(dbcp);
    abort();    /* should retry in case of DB_LOCK_DEADLOCK */
    return ICAL_INTERNAL_ERROR;

  err1:
    dbp->err(dbp, ret, "cursor index");
    abort();
    return ICAL_FILE_ERROR;
}

icalset *icalbdbset_init(icalset *set, const char *dsn, void *options_in)
{
    icalbdbset *bset = (icalbdbset *) set;
    icalbdbset_options *options = (icalbdbset_options *) options_in;
    DB *cal_db;
    const char *subdb_name = NULL;

    _unused(dsn);

    if (options == (icalbdbset_options *) NULL) {
        options = &icalbdbset_options_default;
    }

    switch (options->subdb) {
    case ICALBDB_CALENDARS:
        subdb_name = "calendars";
        break;
    case ICALBDB_EVENTS:
        subdb_name = "events";
        break;
    case ICALBDB_TODOS:
        subdb_name = "todos";
        break;
    case ICALBDB_REMINDERS:
        subdb_name = "reminders";
        break;
    }

    cal_db = icalbdbset_bdb_open(set->dsn,
                                 subdb_name, options->dbtype, options->mode, options->flag);
    if (cal_db == NULL) {
        return NULL;
    }

    bset->dbp = cal_db;
    bset->sdbp = NULL;
    bset->gauge = 0;
    bset->cluster = 0;

    if (icalbdbset_read_database(bset, options->pfunc) != ICAL_NO_ERROR) {
        return NULL;
    }

    return (icalset *) bset;
}

/** open a database and return a reference to it.  Used only for
    opening the primary index.
    flag = set_flag() DUP | DUP_SORT
 */

icalset *icalbdbset_new(const char *database_filename, icalbdbset_subdb_type subdb_type, int dbtype,
                        u_int32_t flag)
{
    icalbdbset_options options = icalbdbset_options_default;

    options.subdb = subdb_type;
    options.dbtype = dbtype;
    options.flag = flag;

    /* this will in turn call icalbdbset_init */
    return icalset_new(ICAL_BDB_SET, database_filename, &options);
}

/**
 *  Open a secondary database, used for accessing secondary indices.
 *  The callback function tells icalbdbset how to associate secondary
 *  key information with primary data.  See the BerkeleyDB reference
 *  guide for more information.
 */

DB *icalbdbset_bdb_open_secondary(DB *dbp, const char *database, const char *sub_database,
                                  int (*callback) (DB *db, const DBT *dbt1, const DBT *dbt2,
                                                   DBT *dbt3), int type)
{
    int ret;
    u_int32_t flags;
    DB *sdbp = NULL;

    if (!sub_database) {
        return NULL;
    }

    if (!ICAL_DB_ENV) {
        if (icalbdbset_init_dbenv(NULL, NULL) != 0) {
            return NULL;
        }
    }

    /* Open/create secondary */
    if ((ret = db_create(&sdbp, ICAL_DB_ENV, 0)) != 0) {
        ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "secondary index: %s", sub_database);
        return NULL;
    }

    if ((ret = sdbp->set_flags(sdbp, (u_int32_t) (DB_DUP | DB_DUPSORT))) != 0) {
        ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "set_flags error for secondary index: %s", sub_database);
        return NULL;
    }

    flags = (u_int32_t) (DB_CREATE | DB_THREAD);
    ret = sdbp->open(sdbp, NULL, database, sub_database, type, (u_int32_t) flags, 0644);
    if (ret != 0) {
        ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "failed to open secondary index: %s", sub_database);
        if (ret == DB_RUNRECOVERY) {
            abort();
        } else {
            return NULL;
        }
    }

    /* Associate the primary index with a secondary */
    if ((ret = dbp->associate(dbp, NULL, sdbp, callback, 0)) != 0) {
        ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "failed to associate secondary index: %s", sub_database);
        return NULL;
    }

    return sdbp;
}

DB *icalbdbset_bdb_open(const char *path, const char *subdb, int dbtype, int mode, u_int32_t flag)
{
    DB *dbp = NULL;
    int ret;
    u_int32_t flags;

    /* Initialize the correct set of db subsystems (see capdb.c) */
    flags = (u_int32_t) (DB_CREATE | DB_THREAD);

    /* should just abort here instead of opening an env in the current dir..  */
    if (!ICAL_DB_ENV) {
        if (icalbdbset_init_dbenv(NULL, NULL) != 0) {
            return NULL;
        }
    }

    /* Create and initialize database object, open the database. */
    if (db_create(&dbp, ICAL_DB_ENV, 0) != 0) {
        return NULL;
    }

    /* set comparison function, if BTREE */
    if (dbtype == DB_BTREE) {
        dbp->set_bt_compare(dbp, _compare_keys);
    }

    /* set DUP, DUPSORT */
    if (flag != 0) {
        dbp->set_flags(dbp, flag);
    }

    if ((ret = dbp->open(dbp, NULL, path, subdb, dbtype, flags, mode)) != 0) {
        ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "%s (database: %s): open failed.", path, subdb);
        if (ret == DB_RUNRECOVERY) {
            abort();
        } else {
            return NULL;
        }
    }

    return dbp;
}

/* icalbdbset_parse_data -- parses using pfunc to unpack data. */
char *icalbdbset_parse_data(DBT *dbt, char *(*pfunc) (const DBT *dbt))
{
    char *ret;

    if (pfunc) {
        ret = (char *)pfunc(dbt);
    } else {
        ret = (char *)dbt->data;
    }

    return ret;
}

/* XXX add more to this */
void icalbdbset_free(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rv((bset != 0), "bset");

    if (bset->cluster != 0) {
        (void)icalbdbset_commit(set);
        icalcomponent_free(bset->cluster);
        bset->cluster = 0;
    }

    if (bset->gauge != 0) {
        icalgauge_free(bset->gauge);
    }

    if (bset->path != 0) {
        free((char *)bset->path);
        bset->path = 0;
    }

    if (bset->sindex != 0) {
        free((char *)bset->sindex);
        bset->sindex = 0;
    }

    if (bset->dbp && (bset->dbp->close(bset->dbp, 0) != 0)) {
        /* ?? */
    }
    bset->dbp = NULL;
}

/* return cursor is in rdbcp */
int icalbdbset_acquire_cursor(DB *dbp, DB_TXN *tid, DBC ** rdbcp)
{
    int ret = 0;

    if ((ret = dbp->cursor(dbp, tid, rdbcp, 0)) != 0) {
        dbp->err(dbp, ret, "couldn't open cursor");
        goto err1;
    }

    return ICAL_NO_ERROR;

  err1:
    return ICAL_FILE_ERROR;
}

/* returns key/data in arguments */
int icalbdbset_get_first(DBC *dbcp, DBT *key, DBT *data)
{
    return icalbdbset_cget(dbcp, key, data, DB_FIRST);
}

int icalbdbset_get_next(DBC *dbcp, DBT *key, DBT *data)
{
    return icalbdbset_cget(dbcp, key, data, DB_NEXT);
}

int icalbdbset_get_last(DBC *dbcp, DBT *key, DBT *data)
{
    return icalbdbset_cget(dbcp, key, data, DB_LAST);
}

int icalbdbset_get_key(DBC *dbcp, DBT *key, DBT *data)
{
    return icalbdbset_cget(dbcp, key, data, DB_SET);
}

int icalbdbset_delete(DB *dbp, DBT *key)
{
    DB_TXN *tid;
    int ret = 0;
    int done = 0;
    int retry = 0;

    while ((retry < MAX_RETRY) && !done) {

        if ((ret = ICAL_DB_ENV->txn_begin(ICAL_DB_ENV, NULL, &tid, 0)) != 0) {
            if (ret == DB_LOCK_DEADLOCK) {
                retry++;
                continue;
            } else {
                /*char *foo = db_strerror(ret); */
                abort();
            }
        }

        if ((ret = dbp->del(dbp, tid, key, 0)) != 0) {
            if (ret == DB_NOTFOUND) {
                /* do nothing - not an error condition */
            } else if (ret == DB_LOCK_DEADLOCK) {
                tid->abort(tid);
                retry++;
                continue;
            } else {
                char *strError = db_strerror(ret);

                icalerror_warn("icalbdbset_delete failed: ");
                icalerror_warn(strError);
                tid->abort(tid);
                return ICAL_FILE_ERROR;
            }
        }

        if ((ret = tid->commit(tid, 0)) != 0) {
            if (ret == DB_LOCK_DEADLOCK) {
                tid->abort(tid);
                retry++;
                continue;
            } else {
                /*char *foo = db_strerror(ret); */
                abort();
            }
        }

        done = 1;       /* all is well */
    }

    if (!done) {
        if (tid != NULL) {
            tid->abort(tid);
        }
    }

    return ret;
}

int icalbdbset_cget(DBC *dbcp, DBT *key, DBT *data, u_int32_t access_method)
{
    key->flags |= DB_DBT_MALLOC;        /* change these to DB_DBT_USERMEM */
    data->flags |= DB_DBT_MALLOC;

    /* fetch the key/data pair */
    if (dbcp->c_get(dbcp, key, data, access_method) != 0) {
        goto err1;
    }

    return ICAL_NO_ERROR;

  err1:
    return ICAL_FILE_ERROR;
}

int icalbdbset_cput(DBC *dbcp, DBT *key, DBT *data, u_int32_t access_method)
{
    _unused(access_method);

    key->flags |= DB_DBT_MALLOC;        /* change these to DB_DBT_USERMEM */
    data->flags |= DB_DBT_MALLOC;

    /* fetch the key/data pair */
    if (dbcp->c_put(dbcp, key, data, 0) != 0) {
        goto err1;
    }

    return ICAL_NO_ERROR;

  err1:
    return ICAL_FILE_ERROR;
}

int icalbdbset_put(DB *dbp, DBT *key, DBT *data, u_int32_t access_method)
{
    int ret = 0;
    DB_TXN *tid = NULL;
    int retry = 0;
    int done = 0;

    while ((retry < MAX_RETRY) && !done) {

        if ((ret = ICAL_DB_ENV->txn_begin(ICAL_DB_ENV, NULL, &tid, 0)) != 0) {
            if (ret == DB_LOCK_DEADLOCK) {
                retry++;
                continue;
            } else {
                /*char *foo = db_strerror(ret); */
                abort();
            }
        }

        if ((ret = dbp->put(dbp, tid, key, data, access_method)) != 0) {
            if (ret == DB_LOCK_DEADLOCK) {
                tid->abort(tid);
                retry++;
                continue;
            } else {
                char *strError = db_strerror(ret);

                icalerror_warn("icalbdbset_put failed: ");
                icalerror_warn(strError);
                tid->abort(tid);
                return ICAL_FILE_ERROR;
            }
        }

        if ((ret = tid->commit(tid, 0)) != 0) {
            if (ret == DB_LOCK_DEADLOCK) {
                tid->abort(tid);
                retry++;
                continue;
            } else {
                /*char *foo = db_strerror(ret); */
                abort();
            }
        }

        done = 1;       /* all is well */
    }

    if (!done) {
        if (tid != NULL) {
            tid->abort(tid);
        }
        return ICAL_FILE_ERROR;
    } else {
        return ICAL_NO_ERROR;
    }
}

int icalbdbset_get(DB *dbp, DB_TXN *tid, DBT *key, DBT *data, u_int32_t flags)
{
    return dbp->get(dbp, tid, key, data, flags);
}

/** Returns the path of the database file **/

const char *icalbdbset_path(icalset *set)
{
    icalerror_check_arg_rz((set != 0), "set");

    return set->dsn;
}

const char *icalbdbset_subdb(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rz((bset != 0), "bset");

    return bset->subdb;
}

/** Write changes out to the database file.
 */

icalerrorenum icalbdbset_commit(icalset *set)
{
    DB *dbp;
    DBC *dbcp;
    DBT key, data;
    icalcomponent *c;
    char *str = NULL;
    int ret = 0;
    int reterr = ICAL_NO_ERROR;
    char keystore[256];
    char uidbuf[256];
    char datastore[1024];
    char *more_mem = NULL;
    DB_TXN *tid = NULL;
    icalbdbset *bset = (icalbdbset *) set;
    int bad_uid_counter = 0;
    int retry = 0, done = 0, completed = 0, deadlocked = 0;

    icalerror_check_arg_re((bset != 0), "bset", ICAL_BADARG_ERROR);

    dbp = bset->dbp;
    icalerror_check_arg_re((dbp != 0), "dbp is invalid", ICAL_BADARG_ERROR);

    if (bset->changed == 0) {
        return ICAL_NO_ERROR;
    }

    memset(&key, 0, sizeof(key));
    memset(&data, 0, sizeof(data));

    key.flags = DB_DBT_USERMEM;
    key.data = keystore;
    key.ulen = (u_int32_t) sizeof(keystore);

    data.flags = DB_DBT_USERMEM;
    data.data = datastore;
    data.ulen = (u_int32_t) sizeof(datastore);

    if (!ICAL_DB_ENV) {
        if (icalbdbset_init_dbenv(NULL, NULL) != 0) {
            return ICAL_INTERNAL_ERROR;
        }
    }

    while ((retry < MAX_RETRY) && !done) {

        if ((ret = ICAL_DB_ENV->txn_begin(ICAL_DB_ENV, NULL, &tid, 0)) != 0) {
            if (ret == DB_LOCK_DEADLOCK) {
                retry++;
                continue;
            } else if (ret == DB_RUNRECOVERY) {
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "icalbdbset_commit: txn_begin failed");
                abort();
            } else {
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "icalbdbset_commit");
                return ICAL_INTERNAL_ERROR;
            }
        }

        /* first delete everything in the database, because there could be removed components */
        if ((ret = dbp->cursor(dbp, tid, &dbcp, DB_DIRTY_READ)) != 0) {
            tid->abort(tid);
            if (ret == DB_LOCK_DEADLOCK) {
                retry++;
                continue;
            } else if (ret == DB_RUNRECOVERY) {
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "curor failed");
                abort();
            } else {
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "curor failed");
                /* leave bset->changed set to true */
                return ICAL_INTERNAL_ERROR;
            }
        }

        /* fetch the key/data pair, then delete it */
        completed = 0;
        while (!completed && !deadlocked) {
            ret = dbcp->c_get(dbcp, &key, &data, DB_NEXT);
            if (ret == DB_NOTFOUND) {
                completed = 1;
            } else if (ret == ENOMEM) {
                if (more_mem) {
                    free(more_mem);
                }
                more_mem = malloc(data.ulen + 1024);
                data.data = more_mem;
                data.ulen = data.ulen + 1024;
            } else if (ret == DB_LOCK_DEADLOCK) {
                deadlocked = 1;
            } else if (ret == DB_RUNRECOVERY) {
                tid->abort(tid);
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "c_get failed.");
                abort();
            } else if (ret == 0) {
                if ((ret = dbcp->c_del(dbcp, 0)) != 0) {
                    dbp->err(dbp, ret, "cursor");
                    if (ret == DB_KEYEMPTY) {
                        /* never actually created, continue onward.. */
                        /* do nothing - break; */
                    } else if (ret == DB_LOCK_DEADLOCK) {
                        deadlocked = 1;
                    } else {
                        /*char *foo = db_strerror(ret); */
                        abort();
                    }
                }
            } else {    /* some other non-fatal error */
                dbcp->c_close(dbcp);
                tid->abort(tid);
                if (more_mem) {
                    free(more_mem);
                    more_mem = NULL;
                }
                return ICAL_INTERNAL_ERROR;
            }
        }

        if (more_mem) {
            free(more_mem);
            more_mem = NULL;
        }

        if (deadlocked) {
            dbcp->c_close(dbcp);
            tid->abort(tid);
            retry++;
            continue;   /* next retry */
        }

        deadlocked = 0;
        for (c = icalcomponent_get_first_component(bset->cluster, ICAL_ANY_COMPONENT);
             c != 0 && !deadlocked;
             c = icalcomponent_get_next_component(bset->cluster, ICAL_ANY_COMPONENT)) {

            memset(&key, 0, sizeof(key));
            memset(&data, 0, sizeof(data));

            /* Note that we're always inserting into a primary index. */
            if (icalcomponent_isa(c) != ICAL_VAGENDA_COMPONENT) {
                char *uidstr = (char *)icalcomponent_get_uid(c);

                if (!uidstr) {  /* this shouldn't happen */
                    /* no uid string, we need to add one */
                    snprintf(uidbuf, 256, "baduid%d-%d", getpid(), bad_uid_counter++);
                    key.data = uidbuf;
                } else {
                    key.data = uidstr;
                }
            } else {
                char *relcalid = NULL;

                relcalid = (char *)icalcomponent_get_relcalid(c);
                if (relcalid == NULL) {
                    snprintf(uidbuf, 256, "baduid%d-%d", getpid(), bad_uid_counter++);
                    key.data = uidbuf;
                } else {
                    key.data = relcalid;
                }
            }
            key.size = (u_int32_t) strlen(key.data);

            str = icalcomponent_as_ical_string_r(c);
            data.data = str;
            data.size = (u_int32_t) strlen(str);

            if ((ret = dbcp->c_put(dbcp, &key, &data, DB_KEYLAST)) != 0) {
                if (ret == DB_LOCK_DEADLOCK) {
                    deadlocked = 1;
                } else if (ret == DB_RUNRECOVERY) {
                    ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "c_put failed.");
                    abort();
                } else {
                    ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "c_put failed %s.", str);
                    /* continue to try to put as many icalcomponent as possible */
                    reterr = ICAL_INTERNAL_ERROR;
                }
            }
        }

        if (deadlocked) {
            dbcp->c_close(dbcp);
            tid->abort(tid);
            retry++;
            continue;
        }

        if ((ret = dbcp->c_close(dbcp)) != 0) {
            tid->abort(tid);
            if (ret == DB_LOCK_DEADLOCK) {
                retry++;
                continue;
            } else if (ret == DB_RUNRECOVERY) {
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "c_closed failed.");
                abort();
            } else {
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "c_closed failed.");
                reterr = ICAL_INTERNAL_ERROR;
            }
        }

        if ((ret = tid->commit(tid, 0)) != 0) {
            tid->abort(tid);
            if (ret == DB_LOCK_DEADLOCK) {
                retry++;
                continue;
            } else if (ret == DB_RUNRECOVERY) {
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "commit failed.");
                abort();
            } else {
                ICAL_DB_ENV->err(ICAL_DB_ENV, ret, "commit failed.");
                reterr = ICAL_INTERNAL_ERROR;
            }
        }

        done = 1;
    }

    bset->changed = 0;
    return reterr;
}

void icalbdbset_mark(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rv((bset != 0), "bset");

    bset->changed = 1;
}

icalcomponent *icalbdbset_get_component(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rz((bset != 0), "bset");

    return bset->cluster;
}

/* manipulate the components in the cluster */

icalerrorenum icalbdbset_add_component(icalset *set, icalcomponent *child)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_re((bset != 0), "bset", ICAL_BADARG_ERROR);
    icalerror_check_arg_re((child != 0), "child", ICAL_BADARG_ERROR);

    icalcomponent_add_component(bset->cluster, child);

    icalbdbset_mark(set);

    return ICAL_NO_ERROR;
}

icalerrorenum icalbdbset_remove_component(icalset *set, icalcomponent *child)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_re((bset != 0), "bset", ICAL_BADARG_ERROR);
    icalerror_check_arg_re((child != 0), "child", ICAL_BADARG_ERROR);

    icalcomponent_remove_component(bset->cluster, child);

    icalbdbset_mark(set);

    return ICAL_NO_ERROR;
}

int icalbdbset_count_components(icalset *set, icalcomponent_kind kind)
{
    icalbdbset *bset;

    if (set == 0) {
        icalerror_set_errno(ICAL_BADARG_ERROR);
        return -1;
    }

    bset = (icalbdbset *) set;
    return icalcomponent_count_components(bset->cluster, kind);
}

/** Sets the gauge **/

icalerrorenum icalbdbset_select(icalset *set, icalgauge *gauge)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_re((bset != 0), "bset", ICAL_BADARG_ERROR);
    icalerror_check_arg_re(gauge != 0, "gauge", ICAL_BADARG_ERROR);

    bset->gauge = gauge;

    return ICAL_NO_ERROR;
}

/** Clear the gauge **/

void icalbdbset_clear(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rv((bset != 0), "bset");

    bset->gauge = 0;
}

icalcomponent *icalbdbset_fetch(icalset *set, icalcomponent_kind kind, const char *uid)
{
    icalcompiter i;
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rz((bset != 0), "bset");

    for (i = icalcomponent_begin_component(bset->cluster, kind);
         icalcompiter_deref(&i) != 0; icalcompiter_next(&i)) {

        icalcomponent *this = icalcompiter_deref(&i);
        icalproperty *p = NULL;
        const char *this_uid = NULL;

        if (this != 0) {
            if (kind == ICAL_VAGENDA_COMPONENT) {
                p = icalcomponent_get_first_property(this, ICAL_RELCALID_PROPERTY);
                if (p != NULL) {
                    this_uid = icalproperty_get_relcalid(p);
                }
            } else {
                p = icalcomponent_get_first_property(this, ICAL_UID_PROPERTY);
                if (p != NULL) {
                    this_uid = icalproperty_get_uid(p);
                }
            }

            if (this_uid == NULL) {
                icalerror_warn("icalbdbset_fetch found a component with no UID");
                continue;
            }

            if (strcmp(uid, this_uid) == 0) {
                return this;
            }
        }
    }

    return 0;
}

int icalbdbset_has_uid(icalset *store, const char *uid)
{
    _unused(store);
    _unused(uid);
    assert(0);  /* HACK, not implemented */
    return 0;
}

/******* support routines for icalbdbset_fetch_match *********/

struct icalbdbset_id
{
    char *uid;
    char *recurrence_id;
    int sequence;
};

static void icalbdbset_id_free(struct icalbdbset_id *id)
{
    if (id->recurrence_id != 0) {
        free(id->recurrence_id);
    }

    if (id->uid != 0) {
        free(id->uid);
    }
}

struct icalbdbset_id icalbdbset_get_id(icalcomponent *comp)
{
    icalcomponent *inner;
    struct icalbdbset_id id;
    icalproperty *p;

    inner = icalcomponent_get_first_real_component(comp);

    p = icalcomponent_get_first_property(inner, ICAL_UID_PROPERTY);

    assert(p != 0);

    id.uid = strdup(icalproperty_get_uid(p));

    p = icalcomponent_get_first_property(inner, ICAL_SEQUENCE_PROPERTY);

    if (p == 0) {
        id.sequence = 0;
    } else {
        id.sequence = icalproperty_get_sequence(p);
    }

    p = icalcomponent_get_first_property(inner, ICAL_RECURRENCEID_PROPERTY);

    if (p == 0) {
        id.recurrence_id = NULL;
    } else {
        icalvalue *v;

        v = icalproperty_get_value(p);
        id.recurrence_id = icalvalue_as_ical_string_r(v);

        assert(id.recurrence_id != 0);
    }

    return id;
}

/* Find the component that is related to the given
   component. Currently, it just matches based on UID and
   RECURRENCE-ID */

static int _compare_ids(const char *compid, const char *matchid)
{
    if (compid != NULL && matchid != NULL) {
        if (strcmp(compid, matchid) == 0) {
            return 1;
        }
    }

    if (compid == NULL && matchid == NULL) {
        return 1;
    }

    return 0;
}

icalcomponent *icalbdbset_fetch_match(icalset *set, icalcomponent *comp)
{
    icalbdbset *bset = (icalbdbset *) set;
    icalcompiter i;
    struct icalbdbset_id comp_id, match_id;

    icalerror_check_arg_rz((bset != 0), "bset");
    comp_id = icalbdbset_get_id(comp);

    for (i = icalcomponent_begin_component(bset->cluster, ICAL_ANY_COMPONENT);
         icalcompiter_deref(&i) != 0; icalcompiter_next(&i)) {

        icalcomponent *match = icalcompiter_deref(&i);

        match_id = icalbdbset_get_id(match);

        if (_compare_ids(comp_id.uid, match_id.uid) &&
            _compare_ids(comp_id.recurrence_id, match_id.recurrence_id)) {

            /* HACK. What to do with SEQUENCE? */

            icalbdbset_id_free(&match_id);
            icalbdbset_id_free(&comp_id);
            return match;
        }

        icalbdbset_id_free(&match_id);
    }

    icalbdbset_id_free(&comp_id);
    return 0;
}

icalerrorenum icalbdbset_modify(icalset *set, icalcomponent *old, icalcomponent *newc)
{
    _unused(set);
    _unused(old);
    _unused(newc);
    assert(0);  /* HACK, not implemented */
    return ICAL_NO_ERROR;
}

/* caller is responsible to cal icalbdbset_free_cluster first */
icalerrorenum icalbdbset_set_cluster(icalset *set, icalcomponent *cluster)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rz((bset != 0), "bset");

    bset->cluster = cluster;
    return ICAL_NO_ERROR;
}

icalerrorenum icalbdbset_free_cluster(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rz((bset != 0), "bset");

    if (bset->cluster != NULL) {
        icalcomponent_free(bset->cluster);
    }
    return ICAL_NO_ERROR;
}

icalcomponent *icalbdbset_get_cluster(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rz((bset != 0), "bset");

    return bset->cluster;
}

/** Iterate through components. */
icalcomponent *icalbdbset_get_current_component(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;

    icalerror_check_arg_rz((bset != 0), "bset");

    return icalcomponent_get_current_component(bset->cluster);
}

icalcomponent *icalbdbset_get_first_component(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;
    icalcomponent *c = 0;

    icalerror_check_arg_rz((bset != 0), "bset");

    do {
        if (c == 0) {
            c = icalcomponent_get_first_component(bset->cluster, ICAL_ANY_COMPONENT);
        } else {
            c = icalcomponent_get_next_component(bset->cluster, ICAL_ANY_COMPONENT);
        }

        if (c != 0 && (bset->gauge == 0 || icalgauge_compare(bset->gauge, c) == 1)) {
            return c;
        }

    } while (c != 0);

    return 0;
}

icalsetiter icalbdbset_begin_component(icalset *set, icalcomponent_kind kind,
                                       icalgauge *gauge, const char *tzid)
{
    icalsetiter itr = icalsetiter_null;
    icalcomponent *comp = NULL;
    icalcompiter citr;
    icalbdbset *bset;
    struct icaltimetype start, next;
    icalproperty *dtstart, *rrule, *prop, *due;
    struct icalrecurrencetype recur;
    icaltimezone *u_zone;
    int g = 0;
    int orig_time_was_utc = 0;

    icalerror_check_arg_re((set != 0), "set", icalsetiter_null);
    bset = (icalbdbset *) set;

    itr.gauge = gauge;
    itr.tzid = tzid;

    citr = icalcomponent_begin_component(bset->cluster, kind);
    comp = icalcompiter_deref(&citr);

    if (gauge == 0) {
        itr.iter = citr;
        return itr;
    }

    /* if there is a gauge, the first matched component is returned */
    while (comp != 0) {

        /* check if it is a recurring component and with gauge expand, if so
         * we need to add recurrence-id property to the given component */
        rrule = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
        g = icalgauge_get_expand(gauge);

        if (rrule != 0 && g == 1) {

            /* it is a recurring event */

            u_zone = icaltimezone_get_builtin_timezone(itr.tzid);

            /* use UTC, if that's all we have. */
            if (!u_zone) {
                u_zone = icaltimezone_get_utc_timezone();
            }

            recur = icalproperty_get_rrule(rrule);
            start = icaltime_from_timet_with_zone(time(0), 0, NULL);

            if (icalcomponent_isa(comp) == ICAL_VEVENT_COMPONENT) {
                dtstart = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
                if (dtstart) {
                    start = icalproperty_get_dtstart(dtstart);
                }
            } else if (icalcomponent_isa(comp) == ICAL_VTODO_COMPONENT) {
                due = icalcomponent_get_first_property(comp, ICAL_DUE_PROPERTY);
                if (due) {
                    start = icalproperty_get_due(due);
                }
            }

            /* Convert to the user's timezone in order to be able to compare
             * the results from the rrule iterator. */
            if (icaltime_is_utc(start)) {
                start = icaltime_convert_to_zone(start, u_zone);
                orig_time_was_utc = 1;
            }

            if (itr.last_component == NULL) {
                itr.ritr = icalrecur_iterator_new(recur, start);
                next = icalrecur_iterator_next(itr.ritr);
                itr.last_component = comp;
            } else {
                next = icalrecur_iterator_next(itr.ritr);
                if (icaltime_is_null_time(next)) {
                    itr.last_component = NULL;
                    icalrecur_iterator_free(itr.ritr);
                    itr.ritr = NULL;
                    /* no matched occurrence */
                    goto getNextComp;
                } else {
                    itr.last_component = comp;
                }
            }

            /* if it is excluded, do next one */
            if (icalproperty_recurrence_is_excluded(comp, &start, &next)) {
                next = icalrecur_iterator_next(itr.ritr);
                continue;
            }

            /* add recurrence-id value to the property if the property already exist;
             * add the recurrence id property and the value if the property does not exist */
            prop = icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
            if (prop == 0) {
                icalcomponent_add_property(comp, icalproperty_new_recurrenceid(next));
            } else {
                icalproperty_set_recurrenceid(prop, next);
            }

            /* convert the next recurrence time into the user's timezone */
            if (orig_time_was_utc) {
                next = icaltime_convert_to_zone(next, icaltimezone_get_utc_timezone());
            }
        }
        /* end of a recurring event */
        if (gauge == 0 || icalgauge_compare(itr.gauge, comp) == 1) {
            /* find a matched and return it */
            itr.iter = citr;
            return itr;
        }

        /* if it is a recurring but no matched occurrence has been found OR
         * it is not a recurring and no matched component has been found,
         * read the next component to find out */
      getNextComp:
        if ((rrule != NULL && itr.last_component == NULL) || (rrule == NULL)) {
            (void)icalcompiter_next(&citr);
            comp = icalcompiter_deref(&citr);
        }
    }   /* while */

    /* no matched component has found */
    return icalsetiter_null;
}

icalcomponent *icalbdbset_form_a_matched_recurrence_component(icalsetiter *itr)
{
    icalcomponent *comp = NULL;
    struct icaltimetype start, next;
    icalproperty *dtstart, *rrule, *prop, *due;
    struct icalrecurrencetype recur;
    icaltimezone *u_zone;
    int orig_time_was_utc = 0;

    comp = itr->last_component;

    if (comp == NULL || itr->gauge == NULL) {
        return NULL;
    }

    rrule = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
    /* if there is no RRULE, simply return to the caller */
    if (rrule == NULL) {
        return NULL;
    }

    u_zone = icaltimezone_get_builtin_timezone(itr->tzid);

    /* use UTC, if that's all we have. */
    if (!u_zone) {
        u_zone = icaltimezone_get_utc_timezone();
    }

    recur = icalproperty_get_rrule(rrule);
    start = icaltime_from_timet_with_zone(time(0), 0, NULL);

    if (icalcomponent_isa(comp) == ICAL_VEVENT_COMPONENT) {
        dtstart = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
        if (dtstart) {
            start = icalproperty_get_dtstart(dtstart);
        }
    } else if (icalcomponent_isa(comp) == ICAL_VTODO_COMPONENT) {
        due = icalcomponent_get_first_property(comp, ICAL_DUE_PROPERTY);
        if (due) {
            start = icalproperty_get_due(due);
        }
    }

    /* Convert to the user's timezone in order to be able to compare the results
     * from the rrule iterator. */
    if (icaltime_is_utc(start)) {
        start = icaltime_convert_to_zone(start, u_zone);
        orig_time_was_utc = 1;
    }

    if (itr->ritr == NULL) {
        itr->ritr = icalrecur_iterator_new(recur, start);
        next = icalrecur_iterator_next(itr->ritr);
        itr->last_component = comp;
    } else {
        next = icalrecur_iterator_next(itr->ritr);
        if (icaltime_is_null_time(next)) {
            /* no more recurrence, returns */
            itr->last_component = NULL;
            icalrecur_iterator_free(itr->ritr);
            itr->ritr = NULL;
            /* no more pending matched occurrence,
             * all the pending matched occurrences have been returned */
            return NULL;
        } else {
            itr->last_component = comp;
        }
    }

    /* if it is excluded, return NULL to the caller */
    if (icalproperty_recurrence_is_excluded(comp, &start, &next)) {
        (void)icalrecur_iterator_next(itr->ritr);
        return NULL;
    }

    /* set recurrence-id value to the property if the property already exist;
     * add the recurrence id property and the value if the property does not exist */
    prop = icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
    if (prop == 0) {
        icalcomponent_add_property(comp, icalproperty_new_recurrenceid(next));
    } else {
        icalproperty_set_recurrenceid(prop, next);
    }

    if (orig_time_was_utc) {
        next = icaltime_convert_to_zone(next, icaltimezone_get_utc_timezone());
    }

    if (itr->gauge == 0 || icalgauge_compare(itr->gauge, comp) == 1) {
        /* find a matched and return it */
        return comp;
    }

    /* not matched */
    return NULL;
}

icalcomponent *icalbdbsetiter_to_next(icalset *set, icalsetiter *i)
{
    icalcomponent *comp = NULL;
    struct icaltimetype start, next;
    icalproperty *dtstart, *rrule, *prop, *due;
    struct icalrecurrencetype recur;
    icaltimezone *u_zone;
    int g = 0;
    int orig_time_was_utc = 0;

    _unused(set);

    do {

        /* no pending occurrence, read the next component */
        if (i->last_component == NULL) {
            comp = icalcompiter_next(&(i->iter));
        } else {
            comp = i->last_component;
        }

        /* no next component, simply return */
        if (comp == 0) {
            return NULL;
        }

        if (i->gauge == 0) {
            return comp;
        }

        /* finding the next matched component and return it to the caller */

        rrule = icalcomponent_get_first_property(comp, ICAL_RRULE_PROPERTY);
        g = icalgauge_get_expand(i->gauge);

        /* a recurring component with expand query */
        if (rrule != 0 && g == 1) {

            u_zone = icaltimezone_get_builtin_timezone(i->tzid);

            /* use UTC, if that's all we have. */
            if (!u_zone) {
                u_zone = icaltimezone_get_utc_timezone();
            }

            recur = icalproperty_get_rrule(rrule);
            start = icaltime_from_timet_with_zone(time(0), 0, NULL);

            if (icalcomponent_isa(comp) == ICAL_VEVENT_COMPONENT) {
                dtstart = icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
                if (dtstart) {
                    start = icalproperty_get_dtstart(dtstart);
                }
            } else if (icalcomponent_isa(comp) == ICAL_VTODO_COMPONENT) {
                due = icalcomponent_get_first_property(comp, ICAL_DUE_PROPERTY);
                if (due) {
                    start = icalproperty_get_due(due);
                }
            }

            /* Convert to the user's timezone in order to be able to compare
             * the results from the rrule iterator. */
            if (icaltime_is_utc(start)) {
                start = icaltime_convert_to_zone(start, u_zone);
                orig_time_was_utc = 1;
            }

            if (i->ritr == NULL) {
                i->ritr = icalrecur_iterator_new(recur, start);
                next = icalrecur_iterator_next(i->ritr);
                i->last_component = comp;
            } else {
                next = icalrecur_iterator_next(i->ritr);
                if (icaltime_is_null_time(next)) {
                    i->last_component = NULL;
                    icalrecur_iterator_free(i->ritr);
                    i->ritr = NULL;
                    /* no more occurrence, should go to get next component */
                    continue;
                } else {
                    i->last_component = comp;
                }
            }

            /* if it is excluded, do next one */
            if (icalproperty_recurrence_is_excluded(comp, &start, &next)) {
                next = icalrecur_iterator_next(i->ritr);
                continue;
            }

            /* set recurrence-id value to the property if the property already exist;
             * add the recurrence id property and the value if the property does not exist */
            prop = icalcomponent_get_first_property(comp, ICAL_RECURRENCEID_PROPERTY);
            if (prop == 0) {
                icalcomponent_add_property(comp, icalproperty_new_recurrenceid(next));
            } else {
                icalproperty_set_recurrenceid(prop, next);
            }

            if (orig_time_was_utc) {
                next = icaltime_convert_to_zone(next, icaltimezone_get_utc_timezone());
            }
        }
        /* end of recurring event with expand query */
        if (comp != 0 && (i->gauge == 0 || icalgauge_compare(i->gauge, comp) == 1)) {
            /* found a matched, return it */
            return comp;
        }
    } while (comp != 0);

    /* coverity[dead_error_line] */
    return NULL;        /*unreachable */
}

icalcomponent *icalbdbset_get_next_component(icalset *set)
{
    icalbdbset *bset = (icalbdbset *) set;
    icalcomponent *c = 0;

    icalerror_check_arg_rz((bset != 0), "bset");

    do {
        c = icalcomponent_get_next_component(bset->cluster, ICAL_ANY_COMPONENT);
        if (c != 0 && (bset->gauge == 0 || icalgauge_compare(bset->gauge, c) == 1)) {
            return c;
        }

    } while (c != 0);

    return 0;
}

int icalbdbset_begin_transaction(DB_TXN *parent_tid, DB_TXN ** tid)
{
    return ICAL_DB_ENV->txn_begin(ICAL_DB_ENV, parent_tid, tid, 0);
}

int icalbdbset_commit_transaction(DB_TXN *txnid)
{
    return txnid->commit(txnid, 0);
}

#if DB_VERSION_MAJOR > 5
static int _compare_keys(DB *dbp, const DBT *a, const DBT *b, size_t *locp)
#else
static int _compare_keys(DB *dbp, const DBT *a, const DBT *b)
#endif
{
    /*
     * Returns:
     * < 0 if a < b
     * = 0 if a = b
     * > 0 if a > b
     */

    char *ac = (char *)a->data;
    char *bc = (char *)b->data;

    _unused(dbp);
#if DB_VERSION_MAJOR > 5
    locp = NULL;
#endif
    return strncmp(ac, bc, a->size);
}
