/**
 * Implementation of the matrix login process
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
 */

#include "matrix-connection.h"
#include "matrix-e2e.h"

#include <string.h>

/* json-glib */
#include <json-glib/json-glib.h>

/* libpurple */
#include <debug.h>
#include <libpurple/request.h>
#include <libpurple/core.h>

/* libmatrix */
#include "libmatrix.h"
#include "matrix-api.h"
#include "matrix-json.h"
#include "matrix-sync.h"

static void _start_next_sync(MatrixConnectionData *ma,
        const gchar *next_batch, gboolean full_state);


void matrix_connection_new(PurpleConnection *pc)
{
     MatrixConnectionData *conn;

     g_assert(purple_connection_get_protocol_data(pc) == NULL);
     conn = g_new0(MatrixConnectionData, 1);
     conn->pc = pc;
     purple_connection_set_protocol_data(pc, conn);
}


void matrix_connection_free(PurpleConnection *pc)
{
    MatrixConnectionData *conn = purple_connection_get_protocol_data(pc);

    g_assert(conn != NULL);

    matrix_e2e_cleanup_connection(conn);
    purple_connection_set_protocol_data(pc, NULL);

    g_free(conn->homeserver);
    conn->homeserver = NULL;

    g_free(conn->access_token);
    conn->access_token = NULL;

    g_free(conn->user_id);
    conn->user_id = NULL;

    conn->pc = NULL;

    g_free(conn);
}

void matrix_connection_cancel_sync(PurpleConnection *pc)
{
    MatrixConnectionData *conn = purple_connection_get_protocol_data(pc);
    g_assert(conn != NULL);
    if(conn->active_sync) {
        purple_debug_info("matrixprpl", "Cancelling active sync on %s\n",
                pc->account->username);
        matrix_api_cancel(conn->active_sync);
    }
    return;
}

/**
 * /sync failed
 */
void _sync_error(MatrixConnectionData *ma, gpointer user_data,
        const gchar *error_message)
{
    ma->active_sync = NULL;
    matrix_api_error(ma, user_data, error_message);
}

/**
 * /sync gave non-200 response
 */
void _sync_bad_response(MatrixConnectionData *ma, gpointer user_data,
        int http_response_code, JsonNode *json_root)
{
    ma->active_sync = NULL;
    matrix_api_bad_response(ma, user_data, http_response_code, json_root);
}


/* callback which is called when a /sync request completes */
static void _sync_complete(MatrixConnectionData *ma, gpointer user_data,
    JsonNode *body,
    const char *raw_body, size_t raw_body_len, const char *content_type)
{
    PurpleConnection *pc = ma->pc;
    const gchar *next_batch;

    ma->active_sync = NULL;

    if(body == NULL) {
        purple_connection_error_reason(pc, PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                "Couldn't parse sync response");
        return;
    }

    purple_connection_update_progress(pc, _("Connected"), 2, 3);
    purple_connection_set_state(pc, PURPLE_CONNECTED);

    matrix_sync_parse(pc, body, &next_batch);

    /* Start the next sync */
    if(next_batch == NULL) {
        purple_connection_error_reason(pc, PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                "No next_batch field");
        return;
    }
    purple_account_set_string(pc->account, PRPL_ACCOUNT_OPT_NEXT_BATCH,
            next_batch);

    _start_next_sync(ma, next_batch, FALSE);
}


static void _start_next_sync(MatrixConnectionData *ma,
        const gchar *next_batch, gboolean full_state)
{
    ma->active_sync = matrix_api_sync(ma, next_batch, 30000, full_state,
            _sync_complete, _sync_error, _sync_bad_response, NULL);
}


static gboolean _account_has_active_conversations(PurpleAccount *account)
{
    GList *ptr;

    for(ptr = purple_get_conversations(); ptr != NULL; ptr = g_list_next(ptr))
    {
        PurpleConversation *conv = ptr->data;
        if(conv -> account == account)
            return TRUE;
    }
    return FALSE;
}

static void _start_sync(MatrixConnectionData *conn)
{
    PurpleConnection *pc = conn->pc;
    gboolean needs_full_state_sync = TRUE;
    const gchar *next_batch;
    const gchar *device_id = purple_account_get_string(pc->account,
            "device_id", NULL);

    if (device_id) {
        matrix_e2e_get_device_keys(conn, device_id);
    }

    /* start the sync loop */
    next_batch = purple_account_get_string(pc->account,
            PRPL_ACCOUNT_OPT_NEXT_BATCH, NULL);

    if(next_batch != NULL) {
        /* if we have previously done a full_state sync on this account, there's
         * no need to do another. If there are already conversations associated
         * with this account, that is a pretty good indication that we have
         * previously done a full_state sync.
         */
        if(_account_has_active_conversations(pc->account)) {
            needs_full_state_sync = FALSE;
        } else {
            /* this appears to be the first time we have connected to this account
             * on this invocation of pidgin.
             */
            gboolean skip = purple_account_get_bool(pc->account,
                    PRPL_ACCOUNT_OPT_SKIP_OLD_MESSAGES, FALSE);
            if(!skip)
                next_batch = NULL;
        }
    }

    if(needs_full_state_sync) {
        purple_connection_update_progress(pc, _("Initial Sync"), 1, 3);
    } else {
        purple_connection_update_progress(pc, _("Connected"), 2, 3);
        purple_connection_set_state(pc, PURPLE_CONNECTED);
    }

    _start_next_sync(conn, next_batch, needs_full_state_sync);
}

static void _login_completed(MatrixConnectionData *conn,
        gpointer user_data,
        JsonNode *json_root,
        const char *raw_body, size_t raw_body_len, const char *content_type)
{
    PurpleConnection *pc = conn->pc;
    JsonObject *root_obj;
    const gchar *access_token;
    const gchar *device_id;

    root_obj = matrix_json_node_get_object(json_root);
    access_token = matrix_json_object_get_string_member(root_obj,
            "access_token");
    if(access_token == NULL) {
        purple_connection_error_reason(pc,
                PURPLE_CONNECTION_ERROR_OTHER_ERROR,
                "No access_token in /login response");
        return;
    }
    conn->access_token = g_strdup(access_token);
    conn->user_id = g_strdup(matrix_json_object_get_string_member(root_obj,
            "user_id"));
    device_id = matrix_json_object_get_string_member(root_obj, "device_id");
    purple_account_set_string(pc->account, "device_id", device_id);
    purple_account_set_string(pc->account, PRPL_ACCOUNT_OPT_ACCESS_TOKEN,
            access_token);

    _start_sync(conn);
}

/*
 * Callback from _password_login when the user enters a password.
 */
static void
_password_received(PurpleConnection *gc, PurpleRequestFields *fields)
{
    PurpleAccount *acct;
    const char *entry;
    MatrixConnectionData *conn;
    gboolean remember;

    /* The password prompt dialog doesn't get disposed if the account disconnects */
    if (!PURPLE_CONNECTION_IS_VALID(gc))
      return;

    acct = purple_connection_get_account(gc);
    conn = purple_connection_get_protocol_data(gc);

    entry = purple_request_fields_get_string(fields, "password");
    remember = purple_request_fields_get_bool(fields, "remember");

    if (!entry || !*entry)
    {
        purple_notify_error(acct, NULL, _("Password is required to sign on."), NULL);
        return;
    }

    if (remember)
        purple_account_set_remember_password(acct, TRUE);

    purple_account_set_password(acct, entry);

    matrix_api_password_login(conn, acct->username,
            entry,
            purple_account_get_string(acct, "device_id", NULL),
            _login_completed, conn);
}


static void
_password_cancel(PurpleConnection *gc, PurpleRequestFields *fields)
{
    PurpleAccount *account;

    /* The password prompt dialog doesn't get disposed if the account disconnects */
    if (!PURPLE_CONNECTION_IS_VALID(gc))
        return;

    account = purple_connection_get_account(gc);

    /* Disable the account as the user has cancelled connecting */
    purple_account_set_enabled(account, purple_core_get_ui(), FALSE);
}

/*
 * Start a passworded login.
 */
static void _password_login(MatrixConnectionData *conn, PurpleAccount *acct)
{
    const char *password = purple_account_get_password(acct);

    if (password) {
        matrix_api_password_login(conn, acct->username,
                password,
                purple_account_get_string(acct, "device_id", NULL),
                _login_completed, conn);
    } else {
        purple_account_request_password(acct,G_CALLBACK( _password_received),
                G_CALLBACK(_password_cancel), conn->pc);
    }
}


/*
 * If we get an error during whoami just fall back to password
 * login.
 */
static void _whoami_error(MatrixConnectionData *conn,
        gpointer user_data, const gchar *error_message)
{
    PurpleAccount *acct = user_data;
    purple_debug_info("matrixprpl", "_whoami_error: %s\n", error_message);
    _password_login(conn, acct);
}

/*
 * If we get a bad response just fall back to password login
 */
static void _whoami_badresp(MatrixConnectionData *conn, gpointer user_data,
        int http_response_code, struct _JsonNode *json_root)
{
    purple_debug_info("matrixprpl", "_whoami_badresp\n");
    _whoami_error(conn, user_data, "Bad response");
}

/*
 * A response from the whoami we issued to validate our access token
 * If it's succesful then we can start the connection.
 */
static void _whoami_completed(MatrixConnectionData *conn,
        gpointer user_data,
        JsonNode *json_root,
        const char *raw_body, size_t raw_body_len, const char *content_type)
{
    JsonObject *root_obj = matrix_json_node_get_object(json_root);
    const gchar *user_id = matrix_json_object_get_string_member(root_obj,
            "user_id");

    purple_debug_info("matrixprpl", "_whoami_completed got %s\n", user_id);
    if (!user_id) {
        return _whoami_error(conn, user_data, "no user_id");
    }
    // TODO: That is out user_id - right?
    conn->user_id = g_strdup(user_id);
    _start_sync(conn);
}

void matrix_connection_start_login(PurpleConnection *pc)
{
    PurpleAccount *acct = pc->account;
    MatrixConnectionData *conn = purple_connection_get_protocol_data(pc);
    const gchar *homeserver = purple_account_get_string(pc->account,
            PRPL_ACCOUNT_OPT_HOME_SERVER, DEFAULT_HOME_SERVER);
    const gchar *access_token = purple_account_get_string(pc->account,
            PRPL_ACCOUNT_OPT_ACCESS_TOKEN, NULL);

    if(!g_str_has_suffix(homeserver, "/")) {
        conn->homeserver = g_strconcat(homeserver, "/", NULL);
    } else {
        conn->homeserver = g_strdup(homeserver);
    }

    purple_connection_set_state(pc, PURPLE_CONNECTING);
    purple_connection_update_progress(pc, _("Logging in"), 0, 3);

    if (access_token) {
        conn->access_token = g_strdup(access_token);
        matrix_api_whoami(conn, _whoami_completed, _whoami_error,
                _whoami_badresp, conn);
    } else {
        _password_login(conn, acct);
    }
}


static void _join_completed(MatrixConnectionData *conn,
        gpointer user_data,
        JsonNode *json_root,
        const char *raw_body, size_t raw_body_len, const char *content_type)
{
    GHashTable *components = user_data;
    JsonObject *root_obj;
    const gchar *room_id;

    root_obj = matrix_json_node_get_object(json_root);
    room_id = matrix_json_object_get_string_member(root_obj, "room_id");
    purple_debug_info("matrixprpl", "join %s completed", room_id);

    g_hash_table_destroy(components);
}


static void _join_error(MatrixConnectionData *conn,
        gpointer user_data, const gchar *error_message)
{
    GHashTable *components = user_data;
    g_hash_table_destroy(components);
    matrix_api_error(conn, user_data, error_message);
}


static void _join_failed(MatrixConnectionData *conn,
        gpointer user_data, int http_response_code,
        struct _JsonNode *json_root)
{
    GHashTable *components = user_data;
    JsonObject *json_obj;
    const gchar *error = NULL;
    const gchar *title = "Error joining chat";

    if (json_root != NULL) {
        json_obj = matrix_json_node_get_object(json_root);
        error = matrix_json_object_get_string_member(json_obj, "error");
    }

    purple_notify_error(conn->pc, title, title, error);
    purple_serv_got_join_chat_failed(conn->pc, components);
    g_hash_table_destroy(components);
}



void matrix_connection_join_room(struct _PurpleConnection *pc,
        const gchar *room, GHashTable *components)
{
    GHashTable *copy;
    GHashTableIter iter;
    gpointer key, value;

    MatrixConnectionData *conn = purple_connection_get_protocol_data(pc);

    /* we have to copy the components table, so that we can pass it back
     * later on :/
     */
    copy = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    g_hash_table_iter_init (&iter, components);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        g_hash_table_insert(copy, g_strdup(key), g_strdup(value));
    }

    matrix_api_join_room(conn, room, _join_completed, _join_error, _join_failed,
            copy);
}


void matrix_connection_reject_invite(struct _PurpleConnection *pc,
        const gchar *room_id)
{
    MatrixConnectionData *conn = purple_connection_get_protocol_data(pc);

    matrix_api_leave_room(conn, room_id, NULL, NULL, NULL, NULL);
}
