/* Copyright (c) 2015-2016 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "mail-namespace.h"
#include "mail-storage.h"
#include "mail-storage-private.h"
#include "notify-plugin.h"
#include "str.h"

#include "push-notification-drivers.h"
#include "push-notification-events.h"
#include "push-notification-events-rfc5423.h"
#include "push-notification-plugin.h"
#include "push-notification-triggers.h"
#include "push-notification-txn-mbox.h"
#include "push-notification-txn-msg.h"


#define PUSH_NOTIFICATION_CONFIG "push_notification_driver"
#define PUSH_NOTIFICATION_CONFIG_OLD "push_notification_backend"

#define PUSH_NOTIFICATION_USER_CONTEXT(obj) \
        MODULE_CONTEXT(obj, push_notification_user_module)
static MODULE_CONTEXT_DEFINE_INIT(push_notification_user_module,
                                  &mail_user_module_register);


static void
push_notification_transaction_init(struct push_notification_txn *ptxn)
{
    struct push_notification_driver_txn *dtxn;
    struct push_notification_driver_user **duser;
    struct mail_storage *storage;

    if (ptxn->initialized) {
        return;
    }

    ptxn->initialized = TRUE;

    storage = mailbox_get_storage(ptxn->mbox);
    if (storage->user->autocreated &&
        (strcmp(storage->name, "raw") == 0)) {
        /* no notifications for autocreated raw users */
        return;
    }

    array_foreach_modifiable(&ptxn->puser->driverlist->drivers, duser) {
        dtxn = p_new(ptxn->pool, struct push_notification_driver_txn, 1);
        dtxn->duser = *duser;
        dtxn->ptxn = ptxn;

        if ((dtxn->duser->driver->v.begin_txn == NULL) ||
            dtxn->duser->driver->v.begin_txn(dtxn)) {
            array_append(&ptxn->drivers, &dtxn, 1);
        }
    }
}

static struct push_notification_txn *
push_notification_transaction_create(struct mailbox *box,
                                     struct mailbox_transaction_context *t)
{
    pool_t pool;
    struct push_notification_txn *ptxn;
    struct mail_storage *storage;

    pool = pool_alloconly_create("push notification transaction", 2048);

    ptxn = p_new(pool, struct push_notification_txn, 1);
    ptxn->mbox = box;
    storage = mailbox_get_storage(box);
    ptxn->muser = mail_storage_get_user(storage);
    ptxn->pool = pool;
    ptxn->puser = PUSH_NOTIFICATION_USER_CONTEXT(ptxn->muser);
    ptxn->t = t;
    ptxn->trigger = PUSH_NOTIFICATION_EVENT_TRIGGER_NONE;

    p_array_init(&ptxn->drivers, pool, 4);

    return ptxn;
}

static void push_notification_transaction_end
(struct push_notification_txn *ptxn, bool success)
{
    struct push_notification_driver_txn **dtxn;

    if (ptxn->initialized) {
        array_foreach_modifiable(&ptxn->drivers, dtxn) {
            if ((*dtxn)->duser->driver->v.end_txn != NULL) {
                (*dtxn)->duser->driver->v.end_txn(*dtxn, success);
            }
        }
    }

    pool_unref(&ptxn->pool);
}

static void push_notification_transaction_commit
(void *txn, struct mail_transaction_commit_changes *changes)
{
    struct push_notification_txn *ptxn = (struct push_notification_txn *)txn;

    if (changes == NULL) {
        push_notification_txn_mbox_end(ptxn);
    } else {
        push_notification_txn_msg_end(ptxn, changes);
    }

    push_notification_transaction_end(ptxn, TRUE);
}

static void push_notification_mailbox_create(struct mailbox *box)
{
    struct push_notification_txn *ptxn;

    ptxn = push_notification_transaction_create(box, NULL);
    push_notification_trigger_mbox_create(ptxn, box, NULL);
    push_notification_transaction_commit(ptxn, NULL);
}

static void push_notification_mailbox_delete(void *txn ATTR_UNUSED,
                                             struct mailbox *box)
{
    struct push_notification_txn *ptxn;

    ptxn = push_notification_transaction_create(box, NULL);
    push_notification_trigger_mbox_delete(ptxn, box, NULL);
    push_notification_transaction_commit(ptxn, NULL);
}

static void push_notification_mailbox_rename(struct mailbox *src,
                                             struct mailbox *dest)
{
    struct push_notification_txn *ptxn;

    ptxn = push_notification_transaction_create(dest, NULL);
    push_notification_trigger_mbox_rename(ptxn, src, dest, NULL);
    push_notification_transaction_commit(ptxn, NULL);
}

static void push_notification_mailbox_subscribe(struct mailbox *box,
                                                bool subscribed)
{
    struct push_notification_txn *ptxn;

    ptxn = push_notification_transaction_create(box, NULL);
    push_notification_trigger_mbox_subscribe(ptxn, box, subscribed, NULL);
    push_notification_transaction_commit(ptxn, NULL);
}

static void push_notification_mail_save(void *txn, struct mail *mail)
{
    struct push_notification_txn *ptxn = txn;

    push_notification_transaction_init(ptxn);

    /* POST_SESSION means MTA delivery. */
    if (mail->box->flags & MAILBOX_FLAG_POST_SESSION) {
        push_notification_trigger_msg_save_new(ptxn, mail, NULL);
    } else {
        push_notification_trigger_msg_save_append(ptxn, mail, NULL);
    }
}

static void push_notification_mail_copy(void *txn,
                                        struct mail *src ATTR_UNUSED,
                                        struct mail *dest)
{
    push_notification_mail_save(txn, dest);
}

static void push_notification_mail_expunge(void *txn, struct mail *mail)
{
    struct push_notification_txn *ptxn = txn;

    push_notification_transaction_init(ptxn);
    push_notification_trigger_msg_save_expunge(txn, mail, NULL);
}

static void
push_notification_mail_update_flags(void *txn, struct mail *mail,
                                    enum mail_flags old_flags)
{
    struct push_notification_txn *ptxn = txn;

    push_notification_transaction_init(ptxn);
    push_notification_trigger_msg_flag_change(txn, mail, NULL, old_flags);
}

static void
push_notification_mail_update_keywords(void *txn, struct mail *mail,
                                       const char *const *old_keywords)
{
    struct push_notification_txn *ptxn = txn;

    push_notification_transaction_init(ptxn);
    push_notification_trigger_msg_keyword_change(txn, mail, NULL, old_keywords);
}

static void *
push_notification_transaction_begin(struct mailbox_transaction_context *t)
{
    return push_notification_transaction_create(mailbox_transaction_get_mailbox(t), t);
}

static void push_notification_transaction_rollback(void *txn)
{
    struct push_notification_txn *ptxn = txn;

    push_notification_transaction_end(ptxn, FALSE);
}

static void
push_notification_config_init(const char *config_name,
                              struct mail_user *user,
                              struct push_notification_driver_list *dlist)
{
    struct push_notification_driver_user *duser;
    const char *env;
    unsigned int i;
    string_t *root_name;

    root_name = t_str_new(32);
    str_append(root_name, config_name);

    for (i = 2;; i++) {
        env = mail_user_plugin_getenv(user, str_c(root_name));
        if ((env == NULL) || (*env == '\0')) {
            break;
        }

        if (push_notification_driver_init(user, env, user->pool, &duser) < 0) {
            break;
        }

        // Add driver.
        array_append(&dlist->drivers, &duser, 1);

        str_truncate(root_name, strlen(config_name));
        str_printfa(root_name, "%d", i);
    }
}

static struct push_notification_driver_list *
push_notification_driver_list_init(struct mail_user *user)
{
    struct push_notification_driver_list *dlist;

    dlist = p_new(user->pool, struct push_notification_driver_list, 1);
    p_array_init(&dlist->drivers, user->pool, 4);

    push_notification_config_init(PUSH_NOTIFICATION_CONFIG, user, dlist);

    if (array_is_empty(&dlist->drivers)) {
        /* Support old configuration (it was available at time initial OX
         * driver was first released). */
        push_notification_config_init(PUSH_NOTIFICATION_CONFIG_OLD, user,
                                      dlist);
    }
    return dlist;
}

static void push_notification_user_deinit(struct mail_user *user)
{
    struct push_notification_user *puser = PUSH_NOTIFICATION_USER_CONTEXT(user);
    struct push_notification_driver_list *dlist = puser->driverlist;
    struct push_notification_driver_user **duser;

    array_foreach_modifiable(&dlist->drivers, duser) {
        if ((*duser)->driver->v.deinit != NULL) {
            (*duser)->driver->v.deinit(*duser);
        }

        if ((*duser)->driver->v.cleanup != NULL) {
            (*duser)->driver->v.cleanup();
        }
    }
    puser->module_ctx.super.deinit(user);
}

static void push_notification_user_created(struct mail_user *user)
{
    struct mail_user_vfuncs *v = user->vlast;
    struct push_notification_user *puser;

    puser = p_new(user->pool, struct push_notification_user, 1);
    puser->module_ctx.super = *v;
    user->vlast = &puser->module_ctx.super;
    v->deinit = push_notification_user_deinit;
    puser->driverlist = push_notification_driver_list_init(user);

    MODULE_CONTEXT_SET(user, push_notification_user_module, puser);
}


/* Plugin interface. */

const char *push_notification_plugin_version = DOVECOT_ABI_VERSION;
const char *push_notification_plugin_dependencies[] = { "notify", NULL };

extern struct push_notification_driver push_notification_driver_dlog;
extern struct push_notification_driver push_notification_driver_ox;

static struct notify_context *push_notification_ctx;

static const struct notify_vfuncs push_notification_vfuncs = {
    /* Mailbox Events */
    .mailbox_create = push_notification_mailbox_create,
    .mailbox_delete_commit = push_notification_mailbox_delete,
    .mailbox_rename = push_notification_mailbox_rename,
    .mailbox_set_subscribed = push_notification_mailbox_subscribe,

    /* Mail Events */
    .mail_copy = push_notification_mail_copy,
    .mail_save = push_notification_mail_save,
    .mail_expunge = push_notification_mail_expunge,
    .mail_update_flags = push_notification_mail_update_flags,
    .mail_update_keywords = push_notification_mail_update_keywords,
    .mail_transaction_begin = push_notification_transaction_begin,
    .mail_transaction_commit = push_notification_transaction_commit,
    .mail_transaction_rollback = push_notification_transaction_rollback
};

static struct mail_storage_hooks push_notification_storage_hooks = {
    .mail_user_created = push_notification_user_created
};

void push_notification_plugin_init(struct module *module)
{
    push_notification_ctx = notify_register(&push_notification_vfuncs);
    mail_storage_hooks_add(module, &push_notification_storage_hooks);

    push_notification_driver_register(&push_notification_driver_dlog);
    push_notification_driver_register(&push_notification_driver_ox);

    push_notification_event_register_rfc5423_events();
}

void push_notification_plugin_deinit(void)
{
    push_notification_driver_unregister(&push_notification_driver_dlog);
    push_notification_driver_unregister(&push_notification_driver_ox);

    push_notification_event_unregister_rfc5423_events();
    mail_storage_hooks_remove(&push_notification_storage_hooks);
    notify_unregister(push_notification_ctx);
}
