/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */


#include "common.h"

#include <pthread.h>

#include <ccnet.h>

#include "db.h"
#include "seafile-session.h"
#include "seafile-config.h"
#include "sync-mgr.h"
#include "transfer-mgr.h"
#include "processors/sync-repo-proc.h"
#include "processors/getca-proc.h"
#include "processors/check-protocol-proc.h"
#include "vc-common.h"
#include "seafile-error.h"
#include "status.h"
#include "mq-mgr.h"
#include "utils.h"

#include "sync-status-tree.h"

#ifdef WIN32
#include <shlobj.h>
#endif

#define DEBUG_FLAG SEAFILE_DEBUG_SYNC
#include "log.h"

#define DEFAULT_SYNC_INTERVAL 30 /* 30s */
#define CHECK_SYNC_INTERVAL  1000 /* 1s */
#define UPDATE_TX_STATE_INTERVAL 1000 /* 1s */
#define MAX_RUNNING_SYNC_TASKS 5
#define CHECK_LOCKED_FILES_INTERVAL 10 /* 10s */
#define CHECK_FOLDER_PERMS_INTERVAL 30 /* 30s */

enum {
    SERVER_SIDE_MERGE_UNKNOWN = 0,
    SERVER_SIDE_MERGE_SUPPORTED,
    SERVER_SIDE_MERGE_UNSUPPORTED,
};

struct _ServerState {
    int server_side_merge;
    gboolean checking;
};
typedef struct _ServerState ServerState;

struct _HttpServerState {
    int http_version;
    gboolean checking;
    gint64 last_http_check_time;
    char *testing_host;
    /* Can be server_url or server_url:8082, depends on which one works. */
    char *effective_host;
    gboolean use_fileserver_port;

    gboolean folder_perms_not_supported;
    gint64 last_check_perms_time;
    gboolean checking_folder_perms;

    gboolean locked_files_not_supported;
    gint64 last_check_locked_files_time;
    gboolean checking_locked_files;
};
typedef struct _HttpServerState HttpServerState;

struct _SeafSyncManagerPriv {
    struct CcnetTimer *check_sync_timer;
    struct CcnetTimer *update_tx_state_timer;
    int    pulse_count;

    /* When FALSE, auto sync is globally disabled */
    gboolean   auto_sync_enabled;

    GHashTable *active_paths;
    pthread_mutex_t paths_lock;

#ifdef WIN32
    GAsyncQueue *refresh_paths;
    struct CcnetTimer *refresh_windows_timer;
#endif
};

struct _ActivePathsInfo {
    GHashTable *paths;
    struct SyncStatusTree *syncing_tree;
    struct SyncStatusTree *synced_tree;
};
typedef struct _ActivePathsInfo ActivePathsInfo;

static void
start_sync (SeafSyncManager *manager, SeafRepo *repo,
            gboolean need_commit, gboolean is_manual_sync,
            gboolean is_initial_commit);

static int auto_sync_pulse (void *vmanager);

static void on_repo_fetched (SeafileSession *seaf,
                             TransferTask *tx_task,
                             SeafSyncManager *manager);
static void on_repo_uploaded (SeafileSession *seaf,
                              TransferTask *tx_task,
                              SeafSyncManager *manager);
static void on_repo_http_fetched (SeafileSession *seaf,
                                  HttpTxTask *tx_task,
                                  SeafSyncManager *manager);
static void on_repo_http_uploaded (SeafileSession *seaf,
                                   HttpTxTask *tx_task,
                                   SeafSyncManager *manager);

static inline void
transition_sync_state (SyncTask *task, int new_state);

static void sync_task_free (SyncTask *task);

static gboolean
check_relay_status (SeafSyncManager *mgr, SeafRepo *repo);

static gboolean
has_old_commits_to_upload (SeafRepo *repo);

static int
sync_repo_v2 (SeafSyncManager *manager, SeafRepo *repo, gboolean is_manual_sync);

static gboolean
check_http_protocol (SeafSyncManager *mgr, SeafRepo *repo);

static void
active_paths_info_free (ActivePathsInfo *info);

SeafSyncManager*
seaf_sync_manager_new (SeafileSession *seaf)
{
    SeafSyncManager *mgr = g_new0 (SeafSyncManager, 1);
    mgr->priv = g_new0 (SeafSyncManagerPriv, 1);    
    mgr->priv->auto_sync_enabled = TRUE;
    mgr->seaf = seaf;

    mgr->sync_interval = DEFAULT_SYNC_INTERVAL;
    mgr->sync_infos = g_hash_table_new (g_str_hash, g_str_equal);

    mgr->server_states = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                g_free, g_free);

    mgr->http_server_states = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                     g_free, g_free);

    gboolean exists;
    int download_limit = seafile_session_config_get_int (seaf,
                                                         KEY_DOWNLOAD_LIMIT,
                                                         &exists);
    if (exists)
        mgr->download_limit = download_limit;

    int upload_limit = seafile_session_config_get_int (seaf,
                                                       KEY_UPLOAD_LIMIT,
                                                       &exists);
    if (exists)
        mgr->upload_limit = upload_limit;

    mgr->priv->active_paths = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                     g_free,
                                                     (GDestroyNotify)active_paths_info_free);
    pthread_mutex_init (&mgr->priv->paths_lock, NULL);

#ifdef WIN32
    mgr->priv->refresh_paths = g_async_queue_new ();
#endif

    return mgr;
}

static SyncInfo*
get_sync_info (SeafSyncManager *manager, const char *repo_id)
{
    SyncInfo *info = g_hash_table_lookup (manager->sync_infos, repo_id);
    if (info) return info;

    info = g_new0 (SyncInfo, 1);
    memcpy (info->repo_id, repo_id, 36);
    g_hash_table_insert (manager->sync_infos, info->repo_id, info);
    return info;
}

SyncInfo *
seaf_sync_manager_get_sync_info (SeafSyncManager *mgr,
                                 const char *repo_id)
{
    return g_hash_table_lookup (mgr->sync_infos, repo_id);
}

int
seaf_sync_manager_init (SeafSyncManager *mgr)
{
    return 0;
}

/* In case ccnet relay info is lost(e.g. ~/ccnet is removed), we need to
 * re-add the relay by supplying addr:port
 */
static void
add_relay_if_needed (SeafRepo *repo)
{
    CcnetPeer *relay = NULL;
    char *relay_port = NULL, *relay_addr = NULL;
    GString *buf = NULL; 

    seaf_repo_manager_get_repo_relay_info (seaf->repo_mgr, repo->id,
                                           &relay_addr, &relay_port);

    relay = ccnet_get_peer (seaf->ccnetrpc_client, repo->relay_id);
    if (relay) {
        /* no relay addr/port info in seafile db. This means we are
         * updating from an old version. */
        if (!relay_addr || !relay_port) {
            if (relay->public_addr && relay->public_port) {
                char port[16];
                snprintf (port, sizeof(port), "%d", relay->public_port);
                seaf_repo_manager_set_repo_relay_info (seaf->repo_mgr, repo->id,
                                                       relay->public_addr, port);
            }
        }
        goto out;
    }

    /* relay info is lost in ccnet, but we have its addr:port in seafile.db */
    if (relay_addr && relay_port) {
        buf = g_string_new(NULL);
        g_string_append_printf (buf, "add-relay --id %s --addr %s:%s",
                                repo->relay_id, relay_addr, relay_port);
                                               
    } else {
        seaf_warning ("[sync mgr] relay addr/port info"
                      " of repo %.10s is unknown\n", repo->id);
    }

    if (buf) {
        ccnet_send_command (seaf->session, buf->str, NULL, NULL);
    }

out:
    g_free (relay_addr);
    g_free (relay_port);
    if (relay)
        g_object_unref (relay);
    if (buf)
        g_string_free (buf, TRUE);
}

static void
add_repo_relays ()
{
    GList *ptr, *repo_list;

    repo_list = seaf_repo_manager_get_repo_list (seaf->repo_mgr, 0, -1);

    for (ptr = repo_list; ptr; ptr = ptr->next) {
        SeafRepo *repo = ptr->data;
        /* Only use non-http sync protocol for old repos.
         * If no old repos exist, we don't need to connect to 10001 port.
         */
        if (repo->version == 0 && repo->relay_id) {
            add_relay_if_needed (repo);
        }
    }

    g_list_free (repo_list);
}

static void 
format_transfer_task_detail (TransferTask *task, GString *buf)
{
    if (task->state != TASK_STATE_NORMAL ||
        task->runtime_state == TASK_RT_STATE_INIT ||
        task->runtime_state == TASK_RT_STATE_FINISHED ||
        task->runtime_state == TASK_RT_STATE_NETDOWN)
        return;

    SeafRepo *repo = seaf_repo_manager_get_repo (seaf->repo_mgr,
                                                 task->repo_id);
    char *repo_name;
    char *type;
    
    if (repo) {
        repo_name = repo->name;
        type = (task->type == TASK_TYPE_UPLOAD) ? "upload" : "download";
        
    } else if (task->is_clone) {
        CloneTask *ctask;
        ctask = seaf_clone_manager_get_task (seaf->clone_mgr, task->repo_id);
        repo_name = ctask->repo_name;
        type = "download";
        
    } else {
        return;
    }
    int rate = transfer_task_get_rate(task);

    g_string_append_printf (buf, "%s\t%d %s\n", type, rate, repo_name);
}

static void 
format_http_task_detail (HttpTxTask *task, GString *buf)
{
    if (task->state != HTTP_TASK_STATE_NORMAL ||
        task->runtime_state == HTTP_TASK_RT_STATE_INIT ||
        task->runtime_state == HTTP_TASK_RT_STATE_FINISHED)
        return;

    SeafRepo *repo = seaf_repo_manager_get_repo (seaf->repo_mgr,
                                                 task->repo_id);
    char *repo_name;
    char *type;
    
    if (repo) {
        repo_name = repo->name;
        type = (task->type == HTTP_TASK_TYPE_UPLOAD) ? "upload" : "download";
        
    } else if (task->is_clone) {
        CloneTask *ctask;
        ctask = seaf_clone_manager_get_task (seaf->clone_mgr, task->repo_id);
        repo_name = ctask->repo_name;
        type = "download";
        
    } else {
        return;
    }
    int rate = http_tx_task_get_rate(task);

    g_string_append_printf (buf, "%s\t%d %s\n", type, rate, repo_name);
}

/*
 * Publish a notification message to report :
 *
 *      [uploading/downloading]\t[transfer-rate] [repo-name]\n
 */
static int
update_tx_state (void *vmanager)
{
    SeafSyncManager *mgr = vmanager;
    GString *buf = g_string_new (NULL);
    GList *tasks, *ptr;
    TransferTask *task;
    HttpTxTask *http_task;

    mgr->last_sent_bytes = g_atomic_int_get (&mgr->sent_bytes);
    g_atomic_int_set (&mgr->sent_bytes, 0);
    mgr->last_recv_bytes = g_atomic_int_get (&mgr->recv_bytes);
    g_atomic_int_set (&mgr->recv_bytes, 0);

    tasks = seaf_transfer_manager_get_upload_tasks (seaf->transfer_mgr);
    for (ptr = tasks; ptr; ptr = ptr->next) {
        task = ptr->data;
        format_transfer_task_detail (task, buf);
    }
    g_list_free (tasks);

    tasks = seaf_transfer_manager_get_download_tasks (seaf->transfer_mgr);
    for (ptr = tasks; ptr; ptr = ptr->next) {
        task = ptr->data;
        format_transfer_task_detail (task, buf);
    }
    g_list_free (tasks);

    tasks = http_tx_manager_get_upload_tasks (seaf->http_tx_mgr);
    for (ptr = tasks; ptr; ptr = ptr->next) {
        http_task = ptr->data;
        format_http_task_detail (http_task, buf);
    }
    g_list_free (tasks);

    tasks = http_tx_manager_get_download_tasks (seaf->http_tx_mgr);
    for (ptr = tasks; ptr; ptr = ptr->next) {
        http_task = ptr->data;
        format_http_task_detail (http_task, buf);
    }
    g_list_free (tasks);

    if (buf->len != 0)
        seaf_mq_manager_publish_notification (seaf->mq_mgr, "transfer",
                                              buf->str);

    g_string_free (buf, TRUE);

    return TRUE;
}

#ifdef WIN32
static void *
refresh_windows_explorer_thread (void *vdata);

#define STARTUP_REFRESH_WINDOWS_DELAY 10000

static int
refresh_all_windows_on_startup (void *vdata)
{
    /* This is a hack to tell Windows Explorer to refresh all open windows.
     * On startup, if there is one big library, its events may dominate the
     * explorer refresh queue. Other libraries don't get refreshed until
     * the big library's events are consumed. So we refresh the open windows
     * to reduce the delay.
     */
    SHChangeNotify (SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);

    /* One time */
    return 0;
}
#endif

int
seaf_sync_manager_start (SeafSyncManager *mgr)
{
    add_repo_relays ();

    mgr->priv->check_sync_timer = ccnet_timer_new (
        auto_sync_pulse, mgr, CHECK_SYNC_INTERVAL);

    mgr->priv->update_tx_state_timer = ccnet_timer_new (
        update_tx_state, mgr, UPDATE_TX_STATE_INTERVAL);

    ccnet_proc_factory_register_processor (mgr->seaf->session->proc_factory,
                                           "seafile-sync-repo",
                                           SEAFILE_TYPE_SYNC_REPO_PROC);
    ccnet_proc_factory_register_processor (mgr->seaf->session->proc_factory,
                                           "seafile-getca",
                                           SEAFILE_TYPE_GETCA_PROC);
    ccnet_proc_factory_register_processor (mgr->seaf->session->proc_factory,
                                           "seafile-check-protocol",
                                           SEAFILE_TYPE_CHECK_PROTOCOL_PROC);
    g_signal_connect (seaf, "repo-fetched",
                      (GCallback)on_repo_fetched, mgr);
    g_signal_connect (seaf, "repo-uploaded",
                      (GCallback)on_repo_uploaded, mgr);
    g_signal_connect (seaf, "repo-http-fetched",
                      (GCallback)on_repo_http_fetched, mgr);
    g_signal_connect (seaf, "repo-http-uploaded",
                      (GCallback)on_repo_http_uploaded, mgr);

#ifdef WIN32
    ccnet_job_manager_schedule_job (seaf->job_mgr,
                                    refresh_windows_explorer_thread,
                                    NULL,
                                    mgr->priv->refresh_paths);

    mgr->priv->refresh_windows_timer = ccnet_timer_new (
        refresh_all_windows_on_startup, mgr, STARTUP_REFRESH_WINDOWS_DELAY);
#endif

    return 0;
}

int
seaf_sync_manager_add_sync_task (SeafSyncManager *mgr,
                                 const char *repo_id,
                                 GError **error)
{
    if (!seaf->started) {
        seaf_message ("sync manager is not started, skip sync request.\n");
        return -1;
    }

    SeafRepo *repo;

    repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id);
    if (!repo) {
        seaf_warning ("[sync mgr] cannot find repo %s.\n", repo_id);
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_REPO, "Invalid repo");
        return -1;
    }

    if (seaf_repo_check_worktree (repo) < 0) {
        g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_NO_WORKTREE,
                     "Worktree doesn't exist");
        return -1;
    }

    SyncInfo *info = get_sync_info (mgr, repo->id);

    if (info->in_sync)
        return 0;

    if (repo->version > 0) {
        if (check_http_protocol (mgr, repo)) {
            sync_repo_v2 (mgr, repo, TRUE);
            return 0;
        }
    } else {
        /* If relay is not ready or protocol version is not determined,
         * need to wait.
         */
        if (!check_relay_status (mgr, repo)) {
            seaf_warning ("Relay for repo %s(%.8s) is not ready or protocol version"
                          "is not detected.\n", repo->name, repo->id);
            return 0;
        }
        start_sync (mgr, repo, TRUE, TRUE, FALSE);
    }

    return 0;
}

void
seaf_sync_manager_cancel_sync_task (SeafSyncManager *mgr,
                                    const char *repo_id)
{
    SyncInfo *info;
    SyncTask *task;

    if (!seaf->started) {
        seaf_message ("sync manager is not started, skip cancel request.\n");
        return;
    }

    /* Cancel running task. */
    info = g_hash_table_lookup (mgr->sync_infos, repo_id);
    if (!info || !info->in_sync)
        return;

    g_return_if_fail (info->current_task != NULL);
    task = info->current_task;

    switch (task->state) {
    case SYNC_STATE_FETCH:
        if (!task->http_sync)
            seaf_transfer_manager_cancel_task (seaf->transfer_mgr,
                                               task->tx_id,
                                               TASK_TYPE_DOWNLOAD);
        else
            http_tx_manager_cancel_task (seaf->http_tx_mgr,
                                         repo_id,
                                         HTTP_TASK_TYPE_DOWNLOAD);
        transition_sync_state (task, SYNC_STATE_CANCEL_PENDING);
        break;
    case SYNC_STATE_UPLOAD:
        if (!task->http_sync)
            seaf_transfer_manager_cancel_task (seaf->transfer_mgr,
                                               task->tx_id,
                                               TASK_TYPE_UPLOAD);
        else
            http_tx_manager_cancel_task (seaf->http_tx_mgr,
                                         repo_id,
                                         HTTP_TASK_TYPE_UPLOAD);
        transition_sync_state (task, SYNC_STATE_CANCEL_PENDING);
        break;
    case SYNC_STATE_COMMIT:
    case SYNC_STATE_INIT:
    case SYNC_STATE_MERGE:
        transition_sync_state (task, SYNC_STATE_CANCEL_PENDING);
        break;
    case SYNC_STATE_CANCEL_PENDING:
        break;
    default:
        g_return_if_reached ();
    }
}

/* Check the notify setting by user.  */
static gboolean
need_notify_sync (SeafRepo *repo)
{
    char *notify_setting = seafile_session_config_get_string(seaf, "notify_sync");
    if (notify_setting == NULL) {
        seafile_session_config_set_string(seaf, "notify_sync", "on");
        return TRUE;
    }

    gboolean result = (g_strcmp0(notify_setting, "on") == 0);
    g_free (notify_setting);
    return result;
}

static const char *sync_state_str[] = {
    "synchronized",
    "committing",
    "initializing",
    "downloading",
    "merging",
    "uploading",
    "error",
    "canceled",
    "cancel pending"
};

static gboolean
find_meaningful_commit (SeafCommit *commit, void *data, gboolean *stop)
{
    SeafCommit **p_head = data;

    if (commit->second_parent_id && commit->new_merge && !commit->conflict)
        return TRUE;

    *stop = TRUE;
    seaf_commit_ref (commit);
    *p_head = commit;
    return TRUE;
}

static void
notify_sync (SeafRepo *repo)
{
    SeafCommit *head = NULL;

    if (!seaf_commit_manager_traverse_commit_tree_truncated (seaf->commit_mgr,
                                                   repo->id, repo->version,
                                                   repo->head->commit_id,
                                                   find_meaningful_commit,
                                                   &head, FALSE)) {
        seaf_warning ("Failed to traverse commit tree of %.8s.\n", repo->id);
        return;
    }
    if (!head)
        return;

    GString *buf = g_string_new (NULL);
    g_string_append_printf (buf, "%s\t%s\t%s",
                            repo->name,
                            repo->id,
                            head->desc);
    seaf_mq_manager_publish_notification (seaf->mq_mgr,
                                          "sync.done",
                                          buf->str);
    g_string_free (buf, TRUE);
    seaf_commit_unref (head);
}

#define IN_ERROR_THRESHOLD 3

static void
update_sync_info_error_state (SyncTask *task, int new_state)
{
    SyncInfo *info = task->info;

    if (new_state == SYNC_STATE_ERROR &&
        task->error != SYNC_ERROR_FILES_LOCKED) {
        info->err_cnt++;
        if (info->err_cnt == IN_ERROR_THRESHOLD)
            info->in_error = TRUE;
    } else if (info->err_cnt > 0) {
        info->err_cnt = 0;
        info->in_error = FALSE;
    }
}

static inline void
transition_sync_state (SyncTask *task, int new_state)
{
    g_return_if_fail (new_state >= 0 && new_state < SYNC_STATE_NUM);

    if (task->state != new_state) {
        if (!(task->state == SYNC_STATE_DONE && new_state == SYNC_STATE_INIT) &&
            !(task->state == SYNC_STATE_INIT && new_state == SYNC_STATE_DONE)) {
            seaf_message ("Repo '%s' sync state transition from '%s' to '%s'.\n",
                          task->repo->name,
                          sync_state_str[task->state],
                          sync_state_str[new_state]);
        }

        if (!task->server_side_merge) {
            if ((task->state == SYNC_STATE_MERGE ||
                 task->state == SYNC_STATE_UPLOAD) &&
                new_state == SYNC_STATE_DONE &&
                need_notify_sync(task->repo))
                notify_sync (task->repo);
        } else {
            if (((task->state == SYNC_STATE_INIT && task->uploaded) ||
                 task->state == SYNC_STATE_FETCH) &&
                new_state == SYNC_STATE_DONE &&
                need_notify_sync(task->repo))
                notify_sync (task->repo);
        }

        task->state = new_state;
        if (new_state == SYNC_STATE_DONE || 
            new_state == SYNC_STATE_CANCELED ||
            new_state == SYNC_STATE_ERROR) {
            task->info->in_sync = FALSE;
            --(task->mgr->n_running_tasks);
            update_sync_info_error_state (task, new_state);
        }

#ifdef WIN32
        seaf_sync_manager_add_refresh_path (seaf->sync_mgr, task->repo->worktree);
#endif
    }
}

static const char *sync_error_str[] = {
    "Success",
    "relay not connected",
    "failed to upgrade old repo",
    "Server has been removed",
    "You have not login to the server",
    "Remote service is not available",
    "You do not have permission to access this library",
    "The storage space of the repo owner has been used up",
    "Access denied to service. Please check your registration on relay.",
    "Internal data corrupted.",
    "Failed to start upload.",
    "Error occured in upload.",
    "Failed to start download.",
    "Error occured in download.",
    "No such repo on relay.",
    "Repo is damaged on relay.",
    "Failed to index files.",
    "Conflict in merge.",
    "Files changed in local folder, skip merge.",
    "Server version is too old.",
    "Failed to get sync info from server.",
    "Files are locked by other application",
    "Unknown error.",
};

void
seaf_sync_manager_set_task_error (SyncTask *task, int error)
{
    g_return_if_fail (error >= 0 && error < SYNC_ERROR_NUM);

    if (task->state != SYNC_STATE_ERROR) {
        seaf_message ("Repo '%s' sync state transition from %s to '%s': '%s'.\n",
                      task->repo->name,
                      sync_state_str[task->state],
                      sync_state_str[SYNC_STATE_ERROR],
                      sync_error_str[error]);
        task->state = SYNC_STATE_ERROR;
        task->error = error;
        task->info->in_sync = FALSE;
        --(task->mgr->n_running_tasks);
        update_sync_info_error_state (task, SYNC_STATE_ERROR);

#ifdef WIN32
        seaf_sync_manager_add_refresh_path (seaf->sync_mgr, task->repo->worktree);
#endif

#if 0
        if (task->repo && error != SYNC_ERROR_RELAY_OFFLINE
            && error != SYNC_ERROR_NOREPO) {
            GString *buf = g_string_new (NULL);
            g_string_append_printf (buf, "%s\t%s", task->repo->name,
                                    task->repo->id);
            seaf_mq_manager_publish_notification (seaf->mq_mgr,
                                                  "sync.error",
                                                  buf->str);
            g_string_free (buf, TRUE);
        }
#endif                                         
    }
}

static void
sync_task_free (SyncTask *task)
{
    g_free (task->tx_id);
    g_free (task->dest_id);
    g_free (task->token);
    g_free (task);
}

static void
start_upload_if_necessary (SyncTask *task)
{
    GError *error = NULL;
    SeafRepo *repo = task->repo;
    const char *repo_id = task->repo->id;

    if (!task->http_sync) {
        char *tx_id = seaf_transfer_manager_add_upload (seaf->transfer_mgr,
                                                        repo_id,
                                                        task->repo->version,
                                                        task->dest_id,
                                                        "local",
                                                        "master",
                                                        task->token,
                                                        task->server_side_merge,
                                                        &error);
        if (error != NULL) {
            seaf_warning ("Failed to start upload: %s\n", error->message);
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_START_UPLOAD);
            return;
        }
        task->tx_id = tx_id;
    } else {
        if (http_tx_manager_add_upload (seaf->http_tx_mgr,
                                        repo->id,
                                        repo->version,
                                        repo->effective_host,
                                        repo->token,
                                        task->http_version,
                                        repo->use_fileserver_port,
                                        &error) < 0) {
            seaf_warning ("Failed to start http upload: %s\n", error->message);
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_START_UPLOAD);
            return;
        }
        task->tx_id = g_strdup(repo->id);
    }

    transition_sync_state (task, SYNC_STATE_UPLOAD);
}

static void
start_fetch_if_necessary (SyncTask *task, const char *remote_head)
{
    GError *error = NULL;
    char *tx_id;
    SeafRepo *repo = task->repo;
    const char *repo_id = task->repo->id;

    if (!task->http_sync) {
        tx_id = seaf_transfer_manager_add_download (seaf->transfer_mgr,
                                                    repo_id,
                                                    task->repo->version,
                                                    task->dest_id,
                                                    "fetch_head",
                                                    "master",
                                                    task->token,
                                                    task->server_side_merge,
                                                    NULL,
                                                    NULL,
                                                    repo->email,
                                                    &error);

        if (error != NULL) {
            seaf_warning ("[sync-mgr] Failed to start download: %s\n",
                          error->message);
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_START_FETCH);
            return;
        }
        task->tx_id = tx_id;
    } else {
        if (http_tx_manager_add_download (seaf->http_tx_mgr,
                                          repo->id,
                                          repo->version,
                                          repo->effective_host,
                                          repo->token,
                                          remote_head,
                                          FALSE,
                                          NULL, NULL,
                                          task->http_version,
                                          repo->email,
                                          repo->use_fileserver_port,
                                          repo->name,
                                          &error) < 0) {
            seaf_warning ("Failed to start http download: %s.\n", error->message);
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_START_FETCH);
            return;
        }
        task->tx_id = g_strdup(repo->id);
    }

    transition_sync_state (task, SYNC_STATE_FETCH);
}

struct MergeResult {
    SyncTask *task;
    gboolean success;
    int merge_status;
    gboolean worktree_dirty;
};

static void *
merge_job (void *vtask)
{
    SyncTask *task = vtask;
    SeafRepo *repo = task->repo;
    char *err_msg = NULL;
    struct MergeResult *res = g_new0 (struct MergeResult, 1);

    res->task = task;

    if (repo->delete_pending) {
        seaf_message ("Repo %s was deleted, don't need to merge.\n", repo->id);
        return res;
    }

    /*
     * 4 types of errors may occur:
     * 1. merge conflicts;
     * 2. fail to checkout a file because the worktree file has been changed;
     * 3. Files are locked on Windows;
     * 4. other I/O errors.
     *
     * For 1, the next commit operation will make worktree clean.
     * For 2 and 4, the errors are ignored by the merge routine (return 0).
     * For 3, just wait another merge retry.
     * */
    if (seaf_repo_merge (repo, "master", &err_msg, &res->merge_status) < 0) {
        seaf_message ("[Sync mgr] Merge of repo %s(%.8s) is not clean.\n",
                   repo->name, repo->id);
        res->success = FALSE;
        g_free (err_msg);
        return res;
    }

    res->success = TRUE;
    g_free (err_msg);
    seaf_message ("[Sync mgr] Merged repo %s(%.8s).\n", repo->name, repo->id);
    return res;
}

static void
merge_job_done (void *vresult)
{
    struct MergeResult *res = vresult;
    SeafRepo *repo = res->task->repo;

    if (repo->delete_pending) {
        transition_sync_state (res->task, SYNC_STATE_CANCELED);
        seaf_repo_manager_del_repo (seaf->repo_mgr, repo);
        g_free (res);
        return;
    }

    if (res->task->state == SYNC_STATE_CANCEL_PENDING) {
        transition_sync_state (res->task, SYNC_STATE_CANCELED);
        g_free (res);
        return;
    }

    if (res->success) {
        SeafBranch *local;
        SeafBranch *master = seaf_branch_manager_get_branch (seaf->branch_mgr,
                                                             repo->id,
                                                             "master");
        if (!master) {
            seaf_warning ("[sync mgr] master branch doesn't exist.\n");
            seaf_sync_manager_set_task_error (res->task, SYNC_ERROR_DATA_CORRUPT);
            goto out;
        }
        /* Save head commit id of master branch for GC, since we've
         * checked out the blocks on the master branch.
         */
        seaf_repo_manager_set_repo_property (seaf->repo_mgr,
                                             repo->id,
                                             REPO_REMOTE_HEAD,
                                             master->commit_id);
        seaf_branch_unref (master);

        /* If it's a ff merge, also update REPO_LOCAL_HEAD. */
        switch (res->merge_status) {
        case MERGE_STATUS_FAST_FORWARD:
            local = seaf_branch_manager_get_branch (seaf->branch_mgr,
                                                    repo->id,
                                                    "local");
            if (!local) {
                seaf_warning ("[sync mgr] local branch doesn't exist.\n");
                seaf_sync_manager_set_task_error (res->task, SYNC_ERROR_DATA_CORRUPT);
                goto out;
            }

            seaf_repo_manager_set_repo_property (seaf->repo_mgr,
                                                 repo->id,
                                                 REPO_LOCAL_HEAD,
                                                 local->commit_id);
            seaf_branch_unref (local);

            transition_sync_state (res->task, SYNC_STATE_DONE);
            break;
        case MERGE_STATUS_REAL_MERGE:
            start_upload_if_necessary (res->task);
            break;
        case MERGE_STATUS_UPTODATE:
            transition_sync_state (res->task, SYNC_STATE_DONE);
            break;
        }
    } else if (res->worktree_dirty)
        seaf_sync_manager_set_task_error (res->task, SYNC_ERROR_WORKTREE_DIRTY);
    else
        seaf_sync_manager_set_task_error (res->task, SYNC_ERROR_MERGE);

out:
    g_free (res);
}

static void
merge_branches_if_necessary (SyncTask *task)
{
    SeafRepo *repo = task->repo;

    /* Repo is not checked out yet. */
    if (!repo->head) {
        transition_sync_state (task, SYNC_STATE_DONE);
        return;
    }
    transition_sync_state (task, SYNC_STATE_MERGE);

    ccnet_job_manager_schedule_job (seaf->job_mgr, 
                                    merge_job, 
                                    merge_job_done,
                                    task);
}

typedef struct {
    char remote_id[41];
    char last_uploaded[41];
    char last_checkout[41];
    gboolean result;
} CheckFFData;

static gboolean
check_fast_forward (SeafCommit *commit, void *vdata, gboolean *stop)
{
    CheckFFData *data = vdata;

    if (strcmp (commit->commit_id, data->remote_id) == 0) {
        *stop = TRUE;
        data->result = TRUE;
        return TRUE;
    }

    if (strcmp (commit->commit_id, data->last_uploaded) == 0 ||
        strcmp (commit->commit_id, data->last_checkout) == 0) {
        *stop = TRUE;
        return TRUE;
    }

    return TRUE;
}

static gboolean
check_fast_forward_with_limit (SeafRepo *repo,
                               const char *local_id,
                               const char *remote_id,
                               const char *last_uploaded,
                               const char *last_checkout,
                               gboolean *error)
{
    CheckFFData data;

    memset (&data, 0, sizeof(data));
    memcpy (data.remote_id, remote_id, 40);
    memcpy (data.last_uploaded, last_uploaded, 40);
    memcpy (data.last_checkout, last_checkout, 40);
    *error = FALSE;

    if (!seaf_commit_manager_traverse_commit_tree_truncated (seaf->commit_mgr,
                                                             repo->id,
                                                             repo->version,
                                                             local_id,
                                                             check_fast_forward,
                                                             &data, FALSE)) {
        seaf_warning ("Failed to traverse commit tree from %s.\n", local_id);
        *error = TRUE;
        return FALSE;
    }

    return data.result;
}

static void
getca_done_cb (CcnetProcessor *processor, gboolean success, void *data)
{
    SyncTask *task = data;
    SyncInfo *info = task->info;
    SeafRepo *repo = task->repo;
    SeafileGetcaProc *proc = (SeafileGetcaProc *)processor;
    SeafBranch *master;

    if (repo->delete_pending) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
        seaf_repo_manager_del_repo (seaf->repo_mgr, repo);
        return;
    }

    if (task->state == SYNC_STATE_CANCEL_PENDING) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
        return;
    }

    if (!success) {
        switch (processor->failure) {
        case PROC_NO_SERVICE:
            seaf_warning ("Server doesn't support putca-proc.\n");
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_DEPRECATED_SERVER);
            break;
        case GETCA_PROC_ACCESS_DENIED:
            seaf_warning ("No permission to access repo %.8s.\n", repo->id);
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_ACCESS_DENIED);
            break;
        case GETCA_PROC_NO_CA:
            seaf_warning ("Compute common ancestor failed for %.8s.\n", repo->id);
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_UNKNOWN);
            break;
        case PROC_REMOTE_DEAD:
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_SERVICE_DOWN);
            break;
        case PROC_PERM_ERR:
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_PROC_PERM_ERR);
            break;
        case PROC_DONE:
            /* It can never happen */
            g_return_if_reached ();
        case PROC_BAD_RESP:
        case PROC_NOTSET:
        default:
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_UNKNOWN);
        }
        return;
    }

    seaf_repo_manager_set_common_ancestor (seaf->repo_mgr,
                                           repo->id,
                                           proc->ca_id,
                                           repo->head->commit_id);

    master = seaf_branch_manager_get_branch (seaf->branch_mgr,
                                             info->repo_id,
                                             "master");

    if (!master || strcmp (info->head_commit, master->commit_id) != 0) {
        start_fetch_if_necessary (task, NULL);
    } else if (strcmp (repo->head->commit_id, master->commit_id) != 0) {
        /* Try to merge even if we don't need to fetch. */
        merge_branches_if_necessary (task);
    }

    seaf_branch_unref (master);
}

static int
start_get_ca_proc (SyncTask *task, const char *repo_id)
{
    CcnetProcessor *processor;

    processor = ccnet_proc_factory_create_remote_master_processor (
        seaf->session->proc_factory, "seafile-getca", task->dest_id);
    if (!processor) {
        seaf_warning ("[sync-mgr] failed to create getca proc.\n");
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_UNKNOWN);
        return -1;
    }

    if (ccnet_processor_startl (processor, repo_id, task->token, NULL) < 0) {
        seaf_warning ("[sync-mgr] failed to start getca proc.\n");
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_UNKNOWN);
        return -1;
    }

    g_signal_connect (processor, "done", (GCallback)getca_done_cb, task);
    return 0;
}

/* Return TURE if we started a processor, otherwise return FALSE. */
static gboolean
update_common_ancestor (SyncTask *task,
                        const char *last_uploaded,
                        const char *last_checkout)
{
    SeafRepo *repo = task->repo;
    char *local_head = repo->head->commit_id;
    char ca_id[41], cached_head_id[41];

    /* If common ancestor result is not cached, we need to compute it. */
    if (seaf_repo_manager_get_common_ancestor (seaf->repo_mgr, repo->id,
                                               ca_id, cached_head_id) < 0)
        goto update_common_ancestor;

    /* If the head id is unchanged, use the cached common ancestor id directly.
     * Common ancestor won't change if the local head is not updated.
     */
    if (strcmp (cached_head_id, local_head) == 0) {
        seaf_debug ("Use cached common ancestor.\n");
        return FALSE;
    }

update_common_ancestor:
    if (strcmp (last_uploaded, local_head) == 0 ||
        strcmp (last_checkout, local_head) == 0) {
        seaf_debug ("Use local head as common ancestor.\n");
        seaf_repo_manager_set_common_ancestor (seaf->repo_mgr, repo->id,
                                               local_head, local_head);
        return FALSE;
    }

    start_get_ca_proc (task, repo->id);
    return TRUE;
}

static gboolean
repo_block_store_exists (SeafRepo *repo)
{
    gboolean ret;
    char *store_path = g_build_filename (seaf->seaf_dir, "storage", "blocks",
                                         repo->id, NULL);
    if (g_file_test (store_path, G_FILE_TEST_IS_DIR))
        ret = TRUE;
    else
        ret = FALSE;
    g_free (store_path);
    return ret;
}

#ifdef WIN32

static GHashTable *
load_locked_files_blocks (const char *repo_id)
{
    LockedFileSet *fset;
    GHashTable *block_id_hash;
    GHashTableIter iter;
    gpointer key, value;
    LockedFile *locked;
    Seafile *file;
    int i;
    char *blk_id;

    fset = seaf_repo_manager_get_locked_file_set (seaf->repo_mgr, repo_id);

    block_id_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

    g_hash_table_iter_init (&iter, fset->locked_files);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        locked = value;

        if (strcmp (locked->operation, LOCKED_OP_UPDATE) == 0) {
            file = seaf_fs_manager_get_seafile (seaf->fs_mgr,
                                                fset->repo_id, 1,
                                                locked->file_id);
            if (!file) {
                seaf_warning ("Failed to find file %s in repo %.8s.\n",
                              locked->file_id, fset->repo_id);
                continue;
            }

            for (i = 0; i < file->n_blocks; ++i) {
                blk_id = g_strdup (file->blk_sha1s[i]);
                g_hash_table_replace (block_id_hash, blk_id, blk_id);
            }

            seafile_unref (file);
        }
    }

    locked_file_set_free (fset);

    return block_id_hash;
}

static gboolean
remove_block_cb (const char *store_id,
                 int version,
                 const char *block_id,
                 void *user_data)
{
    GHashTable *block_hash = user_data;

    if (!g_hash_table_lookup (block_hash, block_id))
        seaf_block_manager_remove_block (seaf->block_mgr, store_id, version, block_id);

    return TRUE;
}

#endif

static void *
remove_repo_blocks (void *vtask)
{
    SyncTask *task = vtask;

#ifndef WIN32
    seaf_block_manager_remove_store (seaf->block_mgr, task->repo->id);
#else
    GHashTable *block_hash;

    block_hash = load_locked_files_blocks (task->repo->id);
    if (g_hash_table_size (block_hash) == 0) {
        g_hash_table_destroy (block_hash);
        seaf_block_manager_remove_store (seaf->block_mgr, task->repo->id);
        return vtask;
    }

    seaf_block_manager_foreach_block (seaf->block_mgr,
                                      task->repo->id,
                                      task->repo->version,
                                      remove_block_cb,
                                      block_hash);

    g_hash_table_destroy (block_hash);
#endif

    return vtask;
}

static void
remove_blocks_done (void *vtask)
{
    SyncTask *task = vtask;

    transition_sync_state (task, SYNC_STATE_DONE);
}

static void
on_repo_deleted_on_server (SyncTask *task, SeafRepo *repo)
{
    seaf_sync_manager_set_task_error (task, SYNC_ERROR_NOREPO);

    seaf_warning ("repo %s(%.8s) not found on server\n",
                  repo->name, repo->id);

    if (!seafile_session_config_get_allow_repo_not_found_on_server(seaf)) {
        seaf_message ("remove repo %s(%.8s) since it's deleted on relay\n",
                      repo->name, repo->id);
        seaf_mq_manager_publish_notification (seaf->mq_mgr,
                                              "repo.deleted_on_relay",
                                              repo->name);
        seaf_repo_manager_del_repo (seaf->repo_mgr, repo);
    }
}

static void
update_sync_status (SyncTask *task)
{
    SyncInfo *info = task->info;
    SeafRepo *repo = task->repo;
    SeafBranch *master, *local;
    char *last_uploaded = NULL, *last_checkout = NULL;

    local = seaf_branch_manager_get_branch (
        seaf->branch_mgr, info->repo_id, "local");
    if (!local) {
        seaf_warning ("[sync-mgr] Branch local not found for repo %s(%.8s).\n",
                   repo->name, repo->id);
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_DATA_CORRUPT);
        return;
    }
    master = seaf_branch_manager_get_branch (
        seaf->branch_mgr, info->repo_id, "master");

    last_uploaded = seaf_repo_manager_get_repo_property (seaf->repo_mgr,
                                                         repo->id,
                                                         REPO_LOCAL_HEAD);
    if (!last_uploaded) {
        seaf_warning ("Last uploaded commit id is not found in db.\n");
        seaf_branch_unref (local);
        seaf_branch_unref (master);
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_DATA_CORRUPT);
        return;
    }

    last_checkout = seaf_repo_manager_get_repo_property (seaf->repo_mgr,
                                                         repo->id,
                                                         REPO_REMOTE_HEAD);
    if (!last_checkout) {
        seaf_warning ("Last checked out commit id is not found in db.\n");
        seaf_branch_unref (local);
        seaf_branch_unref (master);
        g_free (last_uploaded);
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_DATA_CORRUPT);
        return;
    }

    if (info->repo_corrupted) {
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_REPO_CORRUPT);
    } else if (info->deleted_on_relay) {
        /* First upload. */
        if (!master)
            start_upload_if_necessary (task);
        /* If repo doesn't exist on relay and we have "master",
         * it was deleted on relay. In this case we remove this repo.
         */
        else
            on_repo_deleted_on_server (task, repo);
    } else {
        /* branch deleted on relay */
        if (info->branch_deleted_on_relay) {
            start_upload_if_necessary (task);
            goto out;
        }

        /* If local head is the same as remote head, already in sync. */
        if (strcmp (local->commit_id, info->head_commit) == 0) {
            /* As long as the repo is synced with the server. All the local
             * blocks are not useful any more.
             */
            if (repo_block_store_exists (repo)) {
                /* seaf_message ("Removing blocks for repo %s(%.8s).\n", */
                /*               repo->name, repo->id); */
                ccnet_job_manager_schedule_job (seaf->job_mgr,
                                                remove_repo_blocks,
                                                remove_blocks_done,
                                                task);
            } else
                transition_sync_state (task, SYNC_STATE_DONE);
            goto out;
        }

        /* This checking is done in the main thread. But it usually doesn't take
         * much time, because the traversing is limited by last_uploaded and
         * last_checkout commits.
         */
        gboolean error = FALSE;
        gboolean is_ff = check_fast_forward_with_limit (repo,
                                                        local->commit_id,
                                                        info->head_commit,
                                                        last_uploaded,
                                                        last_checkout,
                                                        &error);
        if (error) {
            seaf_warning ("Failed to check fast forward.\n");
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_DATA_CORRUPT);
            goto out;
        }

        /* fast-forward upload */
        if (is_ff) {
            start_upload_if_necessary (task);
            goto out;
        }

        /*
         * We have to compute the common ancestor before doing merge.
         * The last result of computation is cached in local db.
         * Check if we need to re-compute the common ancestor.
         * If so we'll start a processor to do that on the server.
         * For repo version == 0, we download all commits so there is no
         * need to check.
         */
        if (repo->version > 0 &&
            update_common_ancestor (task, last_uploaded, last_checkout))
            goto out;

        if (!master || strcmp (info->head_commit, master->commit_id) != 0) {
            start_fetch_if_necessary (task, NULL);
        } else if (strcmp (local->commit_id, master->commit_id) != 0) {
            /* Try to merge even if we don't need to fetch. */
            merge_branches_if_necessary (task);
        }
    }

out:
    seaf_branch_unref (local);
    if (master)
        seaf_branch_unref (master);
    g_free (last_uploaded);
    g_free (last_checkout);
}

static void
update_sync_status_v2 (SyncTask *task)
{
    SyncInfo *info = task->info;
    SeafRepo *repo = task->repo;
    SeafBranch *master = NULL, *local = NULL;

    local = seaf_branch_manager_get_branch (
        seaf->branch_mgr, info->repo_id, "local");
    if (!local) {
        seaf_warning ("[sync-mgr] Branch local not found for repo %s(%.8s).\n",
                   repo->name, repo->id);
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_DATA_CORRUPT);
        return;
    }

    master = seaf_branch_manager_get_branch (
        seaf->branch_mgr, info->repo_id, "master");
    if (!master) {
        seaf_warning ("[sync-mgr] Branch master not found for repo %s(%.8s).\n",
                   repo->name, repo->id);
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_DATA_CORRUPT);
        return;
    }

    if (info->repo_corrupted) {
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_REPO_CORRUPT);
    } else if (info->deleted_on_relay) {
        on_repo_deleted_on_server (task, repo);
    } else {
        /* If local head is the same as remote head, already in sync. */
        if (strcmp (local->commit_id, info->head_commit) == 0) {
            /* As long as the repo is synced with the server. All the local
             * blocks are not useful any more.
             */
            if (repo_block_store_exists (repo)) {
                seaf_message ("Removing blocks for repo %s(%.8s).\n",
                              repo->name, repo->id);
                ccnet_job_manager_schedule_job (seaf->job_mgr,
                                                remove_repo_blocks,
                                                remove_blocks_done,
                                                task);
            } else
                transition_sync_state (task, SYNC_STATE_DONE);
        } else
            start_fetch_if_necessary (task, task->info->head_commit);
    }

    seaf_branch_unref (local);
    seaf_branch_unref (master);
}

static void
sync_done_cb (CcnetProcessor *processor, gboolean success, void *data)
{
    SyncTask *task = data;
    SeafRepo *repo = task->repo;

    if (repo->delete_pending) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
        seaf_repo_manager_del_repo (seaf->repo_mgr, repo);
        return;
    }

    if (task->state == SYNC_STATE_CANCEL_PENDING) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
        return;
    }

    if (!success) {
        switch (processor->failure) {
        case PROC_DONE:
            /* It can never happen */
            g_return_if_reached ();
        case PROC_REMOTE_DEAD:
        case PROC_NO_SERVICE:
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_SERVICE_DOWN);
            break;
        case PROC_PERM_ERR:
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_PROC_PERM_ERR);
            break;
        case PROC_BAD_RESP:
        case PROC_NOTSET:
        default:
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_UNKNOWN);
        }
        return;
    }

    if (!task->server_side_merge)
        update_sync_status (task);
    else
        update_sync_status_v2 (task);
}

/*
  The sync-repo processor is used to check the head commit at the server side.
*/
static int
start_sync_repo_proc (SeafSyncManager *manager, SyncTask *task)
{
    CcnetProcessor *processor;

    processor = ccnet_proc_factory_create_remote_master_processor (
        seaf->session->proc_factory, "seafile-sync-repo", task->dest_id);
    if (!processor) {
        seaf_warning ("[sync-mgr] failed to create get seafile-sync-repo proc.\n");
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_UNKNOWN);
        return -1;
    }
    ((SeafileSyncRepoProc *)processor)->task = task;

    if (ccnet_processor_startl (processor, NULL) < 0) {
        seaf_warning ("[sync-mgr] failed to start get seafile-sync-repo proc.\n");
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_UNKNOWN);
        return -1;
    }

    g_signal_connect (processor, "done", (GCallback)sync_done_cb, task);

    transition_sync_state (task, SYNC_STATE_INIT);

    return 0;
}

static void
check_head_commit_done (HttpHeadCommit *result, void *user_data)
{
    SyncTask *task = user_data;
    SyncInfo *info = task->info;

    if (!result->check_success) {
        seaf_sync_manager_set_task_error (task, SYNC_ERROR_GET_SYNC_INFO);
        return;
    }

    info->deleted_on_relay = result->is_deleted;
    info->repo_corrupted = result->is_corrupt;
    memcpy (info->head_commit, result->head_commit, 40);

    update_sync_status_v2 (task);
}

static int
check_head_commit_http (SyncTask *task)
{
    SeafRepo *repo = task->repo;

    int ret = http_tx_manager_check_head_commit (seaf->http_tx_mgr,
                                                 repo->id, repo->version,
                                                 repo->effective_host,
                                                 repo->token,
                                                 repo->use_fileserver_port,
                                                 check_head_commit_done,
                                                 task);
    if (ret == 0)
        transition_sync_state (task, SYNC_STATE_INIT);
    return ret;
}

struct CommitResult {
    SyncTask *task;
    gboolean changed;
    gboolean success;
};

static void *
commit_job (void *vtask)
{
    SyncTask *task = vtask;
    SeafRepo *repo = task->repo;
    struct CommitResult *res = g_new0 (struct CommitResult, 1);
    GError *error = NULL;

    res->task = task;

    if (repo->delete_pending)
        return res;

    res->changed = TRUE;
    res->success = TRUE;

    char *commit_id = seaf_repo_index_commit (repo, "",
                                              task->is_manual_sync,
                                              task->is_initial_commit,
                                              &error);
    if (commit_id == NULL && error != NULL) {
        seaf_warning ("[Sync mgr] Failed to commit to repo %s(%.8s).\n",
                      repo->name, repo->id);
        res->success = FALSE;
    } else if (commit_id == NULL) {
        res->changed = FALSE;
    }
    g_free (commit_id);

    return res;
}

static void
commit_job_done (void *vres)
{
    struct CommitResult *res = vres;
    SeafRepo *repo = res->task->repo;
    SyncTask *task = res->task;

    res->task->mgr->commit_job_running = FALSE;

    if (repo->delete_pending) {
        transition_sync_state (res->task, SYNC_STATE_CANCELED);
        seaf_repo_manager_del_repo (seaf->repo_mgr, repo);
        g_free (res);
        return;
    }

    if (res->task->state == SYNC_STATE_CANCEL_PENDING) {
        transition_sync_state (res->task, SYNC_STATE_CANCELED);
        g_free (res);
        return;
    }

    if (!res->success) {
        seaf_sync_manager_set_task_error (res->task, SYNC_ERROR_COMMIT);
        g_free (res);
        return;
    }

    if (!res->task->server_side_merge) {
        /* If nothing committed and is not manual sync, no need to sync. */
        if (!res->changed &&
            !res->task->is_manual_sync && !res->task->is_initial_commit) {
            transition_sync_state (res->task, SYNC_STATE_DONE);
            g_free (res);
            return;
        }
        start_sync_repo_proc (res->task->mgr, res->task);
    } else {
        if (res->changed)
            start_upload_if_necessary (res->task);
        else if (task->is_manual_sync || task->is_initial_commit) {
            if (task->http_sync)
                check_head_commit_http (task);
            else
                start_sync_repo_proc (task->mgr, task);
        } else
            transition_sync_state (task, SYNC_STATE_DONE);
    }

    g_free (res);
}

static int check_commit_state (void *data);

static void
commit_repo (SyncTask *task)
{
    /* In order not to eat too much CPU power, only one commit job can be run
     * at the same time. Other sync tasks have to check every 1 second.
     */
    if (task->mgr->commit_job_running) {
        task->commit_timer = ccnet_timer_new (check_commit_state, task, 1000);
        return;
    }

    task->mgr->commit_job_running = TRUE;

    transition_sync_state (task, SYNC_STATE_COMMIT);

    ccnet_job_manager_schedule_job (seaf->job_mgr, 
                                    commit_job, 
                                    commit_job_done,
                                    task);
}

static int
check_commit_state (void *data)
{
    SyncTask *task = data;

    if (!task->mgr->commit_job_running) {
        ccnet_timer_free (&task->commit_timer);
        commit_repo (task);
        return 0;
    }

    return 1;
}

static void
start_sync (SeafSyncManager *manager, SeafRepo *repo,
            gboolean need_commit, gboolean is_manual_sync,
            gboolean is_initial_commit)
{
    SyncTask *task = g_new0 (SyncTask, 1);
    SyncInfo *info;

    info = get_sync_info (manager, repo->id);

    task->info = info;
    task->mgr = manager;

    task->dest_id = g_strdup(repo->relay_id);
    task->token = g_strdup(repo->token);
    task->is_manual_sync = is_manual_sync;
    task->is_initial_commit = is_initial_commit;

    repo->last_sync_time = time(NULL);
    ++(manager->n_running_tasks);

    /* Free the last task when a new task is started.
     * This way we can always get the state of the last task even
     * after it's done.
     */
    if (task->info->current_task)
        sync_task_free (task->info->current_task);
    task->info->current_task = task;
    task->info->in_sync = TRUE;
    task->repo = repo;

    if (need_commit) {
        repo->create_partial_commit = FALSE;
        commit_repo (task);
    } else
        start_sync_repo_proc (manager, task);
}

static int
sync_repo (SeafSyncManager *manager, SeafRepo *repo)
{
    WTStatus *status;
    gint now = (gint)time(NULL);
    gint last_changed;

    status = seaf_wt_monitor_get_worktree_status (manager->seaf->wt_monitor,
                                                  repo->id);
    if (status) {
        last_changed = g_atomic_int_get (&status->last_changed);
        if (status->last_check == 0) {
            /* Force commit and sync after a new repo is added. */
            start_sync (manager, repo, TRUE, FALSE, TRUE);
            status->last_check = now;
            wt_status_unref (status);
            return 0;
        } else if (last_changed != 0 && status->last_check <= last_changed) {
            /* Commit and sync if the repo has been updated after the
             * last check and is not updated for the last 2 seconds.
             */
            if (now - last_changed >= 2) {
                start_sync (manager, repo, TRUE, FALSE, FALSE);
                status->last_check = now;
                wt_status_unref (status);
                return 0;
            }
        }
        wt_status_unref (status);
    }

    if (manager->n_running_tasks >= MAX_RUNNING_SYNC_TASKS)
        return -1;

    if (repo->last_sync_time > now - manager->sync_interval)
        return -1;

    start_sync (manager, repo, FALSE, FALSE, FALSE);

    return 0;
}

static SyncTask *
create_sync_task_v2 (SeafSyncManager *manager, SeafRepo *repo,
                     gboolean is_manual_sync, gboolean is_initial_commit)
{
    SyncTask *task = g_new0 (SyncTask, 1);
    SyncInfo *info;

    info = get_sync_info (manager, repo->id);

    task->info = info;
    task->mgr = manager;

    task->dest_id = g_strdup (repo->relay_id);
    task->token = g_strdup(repo->token);
    task->is_manual_sync = is_manual_sync;
    task->is_initial_commit = is_initial_commit;
    task->server_side_merge = TRUE;

    repo->last_sync_time = time(NULL);
    ++(manager->n_running_tasks);

    /* Free the last task when a new task is started.
     * This way we can always get the state of the last task even
     * after it's done.
     */
    if (task->info->current_task)
        sync_task_free (task->info->current_task);
    task->info->current_task = task;
    task->info->in_sync = TRUE;
    task->repo = repo;

    if (repo->server_url) {
        HttpServerState *state = g_hash_table_lookup (manager->http_server_states,
                                                      repo->server_url);
        if (state) {
            task->http_sync = TRUE;
            task->http_version = state->http_version;
        }
    }

    return task;
}

static gboolean
create_commit_from_event_queue (SeafSyncManager *manager, SeafRepo *repo,
                                gboolean is_manual_sync)
{
    WTStatus *status;
    SyncTask *task;
    gboolean ret = FALSE;
    gint now = (gint)time(NULL);
    gint last_changed;

    status = seaf_wt_monitor_get_worktree_status (manager->seaf->wt_monitor,
                                                  repo->id);
    if (status) {
        last_changed = g_atomic_int_get (&status->last_changed);
        if (status->last_check == 0) {
            /* Force commit and sync after a new repo is added. */
            task = create_sync_task_v2 (manager, repo, is_manual_sync, TRUE);
            repo->create_partial_commit = TRUE;
            commit_repo (task);
            status->last_check = now;
            ret = TRUE;
        } else if (status->partial_commit) {
            task = create_sync_task_v2 (manager, repo, is_manual_sync, FALSE);
            repo->create_partial_commit = TRUE;
            commit_repo (task);
            ret = TRUE;
        } else if (last_changed != 0 && status->last_check <= last_changed) {
            /* Commit and sync if the repo has been updated after the
             * last check and is not updated for the last 2 seconds.
             */
            if (now - last_changed >= 2) {
                task = create_sync_task_v2 (manager, repo, is_manual_sync, FALSE);
                repo->create_partial_commit = TRUE;
                commit_repo (task);
                status->last_check = now;
                ret = TRUE;
            }
        }
        wt_status_unref (status);
    }

    return ret;
}

static gboolean
can_schedule_repo (SeafSyncManager *manager, SeafRepo *repo)
{
    int now = (int)time(NULL);

    return ((repo->last_sync_time == 0 ||
             repo->last_sync_time < now - manager->sync_interval) &&
            manager->n_running_tasks < MAX_RUNNING_SYNC_TASKS);
}

static int
sync_repo_v2 (SeafSyncManager *manager, SeafRepo *repo, gboolean is_manual_sync)
{
    SeafBranch *master, *local;
    SyncTask *task;
    int ret = 0;
    char *last_download = NULL;

    master = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "master");
    if (!master) {
        seaf_warning ("No master branch found for repo %s(%.8s).\n",
                      repo->name, repo->id);
        return -1;
    }
    local = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "local");
    if (!local) {
        seaf_warning ("No local branch found for repo %s(%.8s).\n",
                      repo->name, repo->id);
        return -1;
    }

    /* If last download was interrupted in the fetch and download stage,
     * need to resume it at exactly the same remote commit.
     */
    last_download = seaf_repo_manager_get_repo_property (seaf->repo_mgr,
                                                         repo->id,
                                                         REPO_PROP_DOWNLOAD_HEAD);
    if (last_download && strcmp (last_download, EMPTY_SHA1) != 0) {
        if (is_manual_sync || can_schedule_repo (manager, repo)) {
            task = create_sync_task_v2 (manager, repo, is_manual_sync, FALSE);
            start_fetch_if_necessary (task, last_download);
        }
        goto out;
    }

    if (strcmp (master->commit_id, local->commit_id) != 0) {
        if (is_manual_sync || can_schedule_repo (manager, repo)) {
            task = create_sync_task_v2 (manager, repo, is_manual_sync, FALSE);
            start_upload_if_necessary (task);
        }
        /* Do nothing if the client still has something to upload
         * but it's before 30-second schedule.
         */
        goto out;
    } else if (is_manual_sync) {
        task = create_sync_task_v2 (manager, repo, is_manual_sync, FALSE);
        commit_repo (task);
        goto out;
    } else if (create_commit_from_event_queue (manager, repo, is_manual_sync))
        goto out;

    if (is_manual_sync || can_schedule_repo (manager, repo)) {
        task = create_sync_task_v2 (manager, repo, is_manual_sync, FALSE);
        if (task->http_sync)
            check_head_commit_http (task);
        else
            start_sync_repo_proc (manager, task);
    }

out:
    g_free (last_download);
    seaf_branch_unref (master);
    seaf_branch_unref (local);
    return ret;
}

static void
auto_delete_repo (SeafSyncManager *manager, SeafRepo *repo)
{
    SyncInfo *info = seaf_sync_manager_get_sync_info (manager, repo->id);
    char *name = g_strdup (repo->name);

    seaf_message ("Auto deleted repo '%s'.\n", repo->name);

    seaf_sync_manager_cancel_sync_task (seaf->sync_mgr, repo->id);

    if (info != NULL && info->in_sync) {
        seaf_repo_manager_mark_repo_deleted (seaf->repo_mgr, repo);
    } else {
        seaf_repo_manager_del_repo (seaf->repo_mgr, repo);
    }

    /* Publish a message, for applet to notify in the system tray */
    seaf_mq_manager_publish_notification (seaf->mq_mgr,
                                          "repo.removed",
                                          name);
    g_free (name);
}

static void
check_protocol_done_cb (CcnetProcessor *processor, gboolean success, void *data)
{
    ServerState *state = data;

    state->checking = FALSE;
    if (success)
        state->server_side_merge = SERVER_SIDE_MERGE_SUPPORTED;
    else if (processor->failure == PROC_NO_SERVICE)
        /* Talking to an old server. */
        state->server_side_merge = SERVER_SIDE_MERGE_UNSUPPORTED;
}

static int
start_check_protocol_proc (SeafSyncManager *manager,
                           const char *peer_id, ServerState *state)
{
    CcnetProcessor *processor;

    processor = ccnet_proc_factory_create_remote_master_processor (
        seaf->session->proc_factory, "seafile-check-protocol", peer_id);
    if (!processor) {
        seaf_warning ("[sync-mgr] failed to create get seafile-check-protocol proc.\n");
        return -1;
    }

    if (ccnet_processor_startl (processor, NULL) < 0) {
        seaf_warning ("[sync-mgr] failed to start seafile-check-protocol proc.\n");
        return -1;
    }

    g_signal_connect (processor, "done", (GCallback)check_protocol_done_cb, state);

    return 0;
}

static gboolean
check_relay_status (SeafSyncManager *mgr, SeafRepo *repo)
{
    gboolean is_ready = ccnet_peer_is_ready (seaf->ccnetrpc_client, repo->relay_id);

    ServerState *state = g_hash_table_lookup (mgr->server_states, repo->relay_id);
    if (!state) {
        state = g_new0 (ServerState, 1);
        g_hash_table_insert (mgr->server_states, g_strdup(repo->relay_id), state);
    }

    if (is_ready) {
        if (state->server_side_merge == SERVER_SIDE_MERGE_UNKNOWN) {
            if (!state->checking) {
                start_check_protocol_proc (mgr, repo->relay_id, state);
                state->checking = TRUE;
            }
            return FALSE;
        } else
            return TRUE;
    } else {
        if (state->server_side_merge == SERVER_SIDE_MERGE_UNKNOWN)
            return FALSE;
        else {
            /* Reset protocol_version to unknown so that we'll check it
             * after the server is up again. */
            state->server_side_merge = SERVER_SIDE_MERGE_UNKNOWN;
            return FALSE;
        }
    }
}

static char *
http_fileserver_url (const char *url)
{
    const char *host;
    char *colon;
    char *url_no_port;
    char *ret = NULL;

    /* Just return the url itself if it's invalid. */
    if (strlen(url) <= strlen("http://"))
        return g_strdup(url);

    /* Skip protocol schem. */
    host = url + strlen("http://");

    colon = strrchr (host, ':');
    if (colon) {
        url_no_port = g_strndup(url, colon - url);
        ret = g_strconcat(url_no_port, ":8082", NULL);
        g_free (url_no_port);
    } else {
        ret = g_strconcat(url, ":8082", NULL);
    }

    return ret;
}

static void
check_http_fileserver_protocol_done (HttpProtocolVersion *result, void *user_data)
{
    HttpServerState *state = user_data;

    state->checking = FALSE;

    if (result->check_success && !result->not_supported) {
        state->http_version = result->version;
        state->effective_host = http_fileserver_url(state->testing_host);
        state->use_fileserver_port = TRUE;
    }
}

static void
check_http_protocol_done (HttpProtocolVersion *result, void *user_data)
{
    HttpServerState *state = user_data;

    if (result->check_success && !result->not_supported) {
        state->http_version = result->version;
        state->effective_host = g_strdup(state->testing_host);
        state->checking = FALSE;
    } else if (strncmp(state->testing_host, "https", 5) != 0) {
        char *host_fileserver = http_fileserver_url(state->testing_host);
        http_tx_manager_check_protocol_version (seaf->http_tx_mgr,
                                                host_fileserver,
                                                TRUE,
                                                check_http_fileserver_protocol_done,
                                                state);
        g_free (host_fileserver);
    } else {
        state->checking = FALSE;
    }
}

#define CHECK_HTTP_INTERVAL 10

/*
 * Returns TRUE if we're ready to use http-sync; otherwise FALSE.
 */
static gboolean
check_http_protocol (SeafSyncManager *mgr, SeafRepo *repo)
{
    /* If a repo was cloned before 4.0, server-url is not set. */
    if (!repo->server_url)
        return FALSE;

    HttpServerState *state = g_hash_table_lookup (mgr->http_server_states,
                                                  repo->server_url);
    if (!state) {
        state = g_new0 (HttpServerState, 1);
        g_hash_table_insert (mgr->http_server_states,
                             g_strdup(repo->server_url), state);
    }

    if (state->checking) {
        return FALSE;
    }

    if (state->http_version > 0) {
        if (!repo->effective_host) {
            repo->effective_host = g_strdup(state->effective_host);
            repo->use_fileserver_port = state->use_fileserver_port;
        }
        return TRUE;
    }

    /* If we haven't detected the server url successfully, retry every 10 seconds. */
    gint64 now = time(NULL);
    if (now - state->last_http_check_time < CHECK_HTTP_INTERVAL)
        return FALSE;

    /* First try repo->server_url.
     * If it fails and https is not used, try server_url:8082 instead.
     */
    g_free (state->testing_host);
    state->testing_host = g_strdup(repo->server_url);

    state->last_http_check_time = (gint64)time(NULL);

    http_tx_manager_check_protocol_version (seaf->http_tx_mgr,
                                            repo->server_url,
                                            FALSE,
                                            check_http_protocol_done,
                                            state);
    state->checking = TRUE;

    return FALSE;
}

/*
 * If the user upgarde from 3.0.x, there may be more than one commit to upload
 * on the local branch. The new syncing protocol can't handle more than one
 * commit. So if we detect this case, fall back to old protocol.
 * After the repo is synced this time, we can use new protocol in the future.
 */
static gboolean
has_old_commits_to_upload (SeafRepo *repo)
{
    SeafBranch *master = NULL, *local = NULL;
    SeafCommit *head = NULL;
    gboolean ret = TRUE;

    master = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "master");
    if (!master) {
        seaf_warning ("No master branch found for repo %s(%.8s).\n",
                      repo->name, repo->id);
        goto out;
    }
    local = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "local");
    if (!local) {
        seaf_warning ("No local branch found for repo %s(%.8s).\n",
                      repo->name, repo->id);
        goto out;
    }

    if (strcmp (local->commit_id, master->commit_id) == 0) {
        ret = FALSE;
        goto out;
    }

    head = seaf_commit_manager_get_commit (seaf->commit_mgr,
                                           repo->id, repo->version,
                                           local->commit_id);
    if (!head) {
        seaf_warning ("Failed to get head commit of repo %s(%.8s).\n",
                      repo->name, repo->id);
        goto out;
    }

    if (head->second_parent_id == NULL &&
        g_strcmp0 (head->parent_id, master->commit_id) == 0)
        ret = FALSE;

out:
    seaf_branch_unref (master);
    seaf_branch_unref (local);
    seaf_commit_unref (head);
    return ret;
}

gint
cmp_repos_by_sync_time (gconstpointer a, gconstpointer b, gpointer user_data)
{
    const SeafRepo *repo_a = a;
    const SeafRepo *repo_b = b;

    return (repo_a->last_sync_time - repo_b->last_sync_time);
}

#ifdef WIN32

static void
cleanup_file_blocks (const char *repo_id, int version, const char *file_id)
{
    Seafile *file;
    int i;

    file = seaf_fs_manager_get_seafile (seaf->fs_mgr,
                                        repo_id, version,
                                        file_id);
    for (i = 0; i < file->n_blocks; ++i)
        seaf_block_manager_remove_block (seaf->block_mgr,
                                         repo_id, version,
                                         file->blk_sha1s[i]);

    seafile_unref (file);
}

static gboolean
handle_locked_file_update (SeafRepo *repo, struct index_state *istate,
                           LockedFileSet *fset, const char *path, LockedFile *locked)
{
    struct cache_entry *ce;
    char file_id[41];
    char *fullpath = NULL;
    SeafStat st;
    gboolean file_exists = TRUE;
    SeafileCrypt *crypt = NULL;
    SeafBranch *master = NULL;
    gboolean ret = TRUE;

    /* File is still locked, do nothing. */
    if (do_check_file_locked (path, repo->worktree))
        return FALSE;

    seaf_debug ("Update previously locked file %s in repo %.8s.\n",
                path, repo->id);

    /* If the file was locked on the last checkout, the worktree file was not
     * updated, but the index has been updated. So the ce in the index should
     * contain the information for the file to be updated.
     */
    ce = index_name_exists (istate, path, strlen(path), 0);
    if (!ce) {
        seaf_warning ("Cache entry for %s in repo %s(%.8s) is not found "
                      "when update locked file.",
                      path, repo->name, repo->id);
        goto remove_from_db;
    }

    rawdata_to_hex (ce->sha1, file_id, 20);

    fullpath = g_build_filename (repo->worktree, path, NULL);

    file_exists = seaf_util_exists (fullpath);

    if (file_exists && seaf_stat (fullpath, &st) < 0) {
        seaf_warning ("Failed to stat %s: %s.\n", fullpath, strerror(errno));
        goto out;
    }

    if (repo->encrypted)
        crypt = seafile_crypt_new (repo->enc_version,
                                   repo->enc_key,
                                   repo->enc_iv);

    master = seaf_branch_manager_get_branch (seaf->branch_mgr, repo->id, "master");
    if (!master) {
        seaf_warning ("No master branch found for repo %s(%.8s).\n",
                      repo->name, repo->id);
        goto out;
    }

    gboolean conflicted;
    gboolean force_conflict = (file_exists && st.st_mtime != locked->old_mtime);
    if (seaf_fs_manager_checkout_file (seaf->fs_mgr,
                                       repo->id, repo->version,
                                       file_id, fullpath,
                                       ce->ce_mode, ce->ce_mtime.sec,
                                       crypt,
                                       path,
                                       master->commit_id,
                                       force_conflict,
                                       &conflicted,
                                       repo->email) < 0) {
        seaf_warning ("Failed to checkout previously locked file %s in repo "
                      "%s(%.8s).\n",
                      path, repo->name, repo->id);
    }

    seaf_sync_manager_update_active_path (seaf->sync_mgr,
                                          repo->id,
                                          path,
                                          S_IFREG,
                                          SYNC_STATUS_SYNCED);

out:
    cleanup_file_blocks (repo->id, repo->version, file_id);

remove_from_db:
    /* Remove the locked file record from db. */
    locked_file_set_remove (fset, path, TRUE);

    g_free (fullpath);
    g_free (crypt);
    seaf_branch_unref (master);
    return ret;
}

static gboolean
handle_locked_file_delete (SeafRepo *repo, struct index_state *istate,
                           LockedFileSet *fset, const char *path, LockedFile *locked)
{
    char *fullpath = NULL;
    SeafStat st;
    gboolean file_exists = TRUE;
    gboolean ret = TRUE;

    /* File is still locked, do nothing. */
    if (do_check_file_locked (path, repo->worktree))
        return FALSE;

    seaf_debug ("Delete previously locked file %s in repo %.8s.\n",
                path, repo->id);

    fullpath = g_build_filename (repo->worktree, path, NULL);

    file_exists = seaf_util_exists (fullpath);

    if (file_exists && seaf_stat (fullpath, &st) < 0) {
        seaf_warning ("Failed to stat %s: %s.\n", fullpath, strerror(errno));
        goto out;
    }

    if (file_exists && st.st_mtime == locked->old_mtime)
        seaf_util_unlink (fullpath);

out:
    /* Remove the locked file record from db. */
    locked_file_set_remove (fset, path, TRUE);

    g_free (fullpath);
    return ret;
}

static void *
check_locked_files (void *vdata)
{
    SeafRepo *repo = vdata;
    LockedFileSet *fset;
    GHashTableIter iter;
    gpointer key, value;
    char *path;
    LockedFile *locked;
    char index_path[SEAF_PATH_MAX];
    struct index_state istate;

    fset = seaf_repo_manager_get_locked_file_set (seaf->repo_mgr, repo->id);

    if (g_hash_table_size (fset->locked_files) == 0) {
        locked_file_set_free (fset);
        return vdata;
    }

    memset (&istate, 0, sizeof(istate));
    snprintf (index_path, SEAF_PATH_MAX, "%s/%s", repo->manager->index_dir, repo->id);
    if (read_index_from (&istate, index_path, repo->version) < 0) {
        seaf_warning ("Failed to load index.\n");
        return vdata;
    }

    gboolean success;
    g_hash_table_iter_init (&iter, fset->locked_files);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        path = key;
        locked = value;

        success = FALSE;
        if (strcmp (locked->operation, LOCKED_OP_UPDATE) == 0)
            success = handle_locked_file_update (repo, &istate, fset, path, locked);
        else if (strcmp (locked->operation, LOCKED_OP_DELETE) == 0)
            success = handle_locked_file_delete (repo, &istate, fset, path, locked);

        if (success)
            g_hash_table_iter_remove (&iter);
    }

    discard_index (&istate);
    locked_file_set_free (fset);

    return vdata;
}

static void
check_locked_files_done (void *vdata)
{
    SeafRepo *repo = vdata;
    repo->checking_locked_files = FALSE;
}

#endif

static void
check_folder_perms_done (HttpFolderPerms *result, void *user_data)
{
    HttpServerState *server_state = user_data;
    GList *ptr;
    HttpFolderPermRes *res;
    gint64 now = (gint64)time(NULL);

    server_state->checking_folder_perms = FALSE;

    if (!result->success) {
        /* If on star-up we find that checking folder perms fails,
         * we assume the server doesn't support it.
         */
        if (server_state->last_check_perms_time == 0)
            server_state->folder_perms_not_supported = TRUE;
        server_state->last_check_perms_time = now;
        return;
    }

    SyncInfo *info;
    for (ptr = result->results; ptr; ptr = ptr->next) {
        res = ptr->data;

        info = get_sync_info (seaf->sync_mgr, res->repo_id);
        if (info->in_sync)
            continue;

        seaf_repo_manager_update_folder_perms (seaf->repo_mgr, res->repo_id,
                                               FOLDER_PERM_TYPE_USER,
                                               res->user_perms);
        seaf_repo_manager_update_folder_perms (seaf->repo_mgr, res->repo_id,
                                               FOLDER_PERM_TYPE_GROUP,
                                               res->group_perms);
        seaf_repo_manager_update_folder_perm_timestamp (seaf->repo_mgr,
                                                        res->repo_id,
                                                        res->timestamp);
    }

    server_state->last_check_perms_time = now;
}

static void
check_folder_permissions_one_server (SeafSyncManager *mgr,
                                     const char *host,
                                     HttpServerState *server_state,
                                     GList *repos)
{
    GList *ptr;
    SeafRepo *repo;
    char *token;
    gint64 timestamp;
    HttpFolderPermReq *req;
    GList *requests = NULL;

    gint64 now = (gint64)time(NULL);

    if (server_state->http_version == 0 ||
        server_state->folder_perms_not_supported ||
        server_state->checking_folder_perms)
        return;

    if (server_state->last_check_perms_time > 0 &&
        now - server_state->last_check_perms_time < CHECK_FOLDER_PERMS_INTERVAL)
        return;

    for (ptr = repos; ptr; ptr = ptr->next) {
        repo = ptr->data;

        if (!repo->head)
            continue;

        if (g_strcmp0 (host, repo->server_url) != 0)
            continue;

        token = seaf_repo_manager_get_repo_property (seaf->repo_mgr,
                                                     repo->id, REPO_PROP_TOKEN);
        if (!token)
            continue;

        timestamp = seaf_repo_manager_get_folder_perm_timestamp (seaf->repo_mgr,
                                                                 repo->id);
        if (timestamp < 0)
            timestamp = 0;

        req = g_new0 (HttpFolderPermReq, 1);
        memcpy (req->repo_id, repo->id, 36);
        req->token = g_strdup(token);
        req->timestamp = timestamp;

        requests = g_list_append (requests, req);

        g_free (token);
    }

    if (!requests)
        return;

    server_state->checking_folder_perms = TRUE;

    /* The requests list will be freed in http tx manager. */
    http_tx_manager_get_folder_perms (seaf->http_tx_mgr,
                                      server_state->effective_host,
                                      server_state->use_fileserver_port,
                                      requests,
                                      check_folder_perms_done,
                                      server_state);
}

static void
check_folder_permissions (SeafSyncManager *mgr, GList *repos)
{
    GHashTableIter iter;
    gpointer key, value;
    char *host;
    HttpServerState *state;

    g_hash_table_iter_init (&iter, mgr->http_server_states);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        host = key;
        state = value;
        check_folder_permissions_one_server (mgr, host, state, repos);
    }
}

static void
check_server_locked_files_done (HttpLockedFiles *result, void *user_data)
{
    HttpServerState *server_state = user_data;
    GList *ptr;
    HttpLockedFilesRes *locked_res;
    gint64 now = (gint64)time(NULL);

    server_state->checking_locked_files = FALSE;

    if (!result->success) {
        /* If on star-up we find that checking locked files fails,
         * we assume the server doesn't support it.
         */
        if (server_state->last_check_locked_files_time == 0)
            server_state->locked_files_not_supported = TRUE;
        server_state->last_check_locked_files_time = now;
        return;
    }

    SyncInfo *info;
    GList *p;
    for (ptr = result->results; ptr; ptr = ptr->next) {
        locked_res = ptr->data;

        info = get_sync_info (seaf->sync_mgr, locked_res->repo_id);
        if (info->in_sync)
            continue;

        seaf_filelock_manager_update (seaf->filelock_mgr,
                                      locked_res->repo_id,
                                      locked_res->locked_files);

        seaf_filelock_manager_update_timestamp (seaf->filelock_mgr,
                                                locked_res->repo_id,
                                                locked_res->timestamp);
    }

    server_state->last_check_locked_files_time = now;
}

static void
check_locked_files_one_server (SeafSyncManager *mgr,
                                     const char *host,
                                     HttpServerState *server_state,
                                     GList *repos)
{
    GList *ptr;
    SeafRepo *repo;
    char *token;
    gint64 timestamp;
    HttpLockedFilesReq *req;
    GList *requests = NULL;

    gint64 now = (gint64)time(NULL);

    if (server_state->http_version == 0 ||
        server_state->locked_files_not_supported ||
        server_state->checking_locked_files)
        return;

    if (server_state->last_check_locked_files_time > 0 &&
        now - server_state->last_check_locked_files_time < CHECK_FOLDER_PERMS_INTERVAL)
        return;

    for (ptr = repos; ptr; ptr = ptr->next) {
        repo = ptr->data;

        if (!repo->head)
            continue;

        if (g_strcmp0 (host, repo->server_url) != 0)
            continue;

        token = seaf_repo_manager_get_repo_property (seaf->repo_mgr,
                                                     repo->id, REPO_PROP_TOKEN);
        if (!token)
            continue;

        timestamp = seaf_filelock_manager_get_timestamp (seaf->filelock_mgr,
                                                         repo->id);
        if (timestamp < 0)
            timestamp = 0;

        req = g_new0 (HttpLockedFilesReq, 1);
        memcpy (req->repo_id, repo->id, 36);
        req->token = g_strdup(token);
        req->timestamp = timestamp;

        requests = g_list_append (requests, req);

        g_free (token);
    }

    if (!requests)
        return;

    server_state->checking_locked_files = TRUE;

    /* The requests list will be freed in http tx manager. */
    http_tx_manager_get_locked_files (seaf->http_tx_mgr,
                                      server_state->effective_host,
                                      server_state->use_fileserver_port,
                                      requests,
                                      check_server_locked_files_done,
                                      server_state);
}

static void
check_server_locked_files (SeafSyncManager *mgr, GList *repos)
{
    GHashTableIter iter;
    gpointer key, value;
    char *host;
    HttpServerState *state;

    g_hash_table_iter_init (&iter, mgr->http_server_states);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        host = key;
        state = value;
        check_locked_files_one_server (mgr, host, state, repos);
    }
}

#if 0
static void
print_active_paths (SeafSyncManager *mgr)
{
    int n = seaf_sync_manager_active_paths_number(mgr);
    seaf_message ("%d active paths\n\n", n);
    if (n < 10) {
        char *paths_json = seaf_sync_manager_list_active_paths_json (mgr);
        seaf_message ("%s\n", paths_json);
        g_free (paths_json);
    }
}
#endif

static int
auto_sync_pulse (void *vmanager)
{
    SeafSyncManager *manager = vmanager;
    GList *repos, *ptr;
    SeafRepo *repo;
    gint64 now;

    repos = seaf_repo_manager_get_repo_list (manager->seaf->repo_mgr, -1, -1);

    check_folder_permissions (manager, repos);

    check_server_locked_files (manager, repos);

    /* Sort repos by last_sync_time, so that we don't "starve" any repo. */
    repos = g_list_sort_with_data (repos, cmp_repos_by_sync_time, NULL);

    for (ptr = repos; ptr != NULL; ptr = ptr->next) {
        repo = ptr->data;

        /* Every second, we'll check the worktree to see if it still exists.
         * We'll invalidate worktree if it gets moved or deleted.
         * But there is a hole here: If the user delete the worktree dir and
         * recreate a dir with the same name within a second, we'll falsely
         * see the worktree as valid. What's worse, the new worktree dir won't
         * be monitored.
         * This problem can only be solved by restart.
         */
        /* If repo has been checked out and the worktree doesn't exist,
         * we'll delete the repo automatically.
         */

        if (repo->head != NULL) {
            if (seaf_repo_check_worktree (repo) < 0) {
                if (!repo->worktree_invalid) {
                    // The repo worktree was valid, but now it's invalid
                    seaf_repo_manager_invalidate_repo_worktree (seaf->repo_mgr, repo);
                    if (!seafile_session_config_get_allow_invalid_worktree(seaf)) {
                        auto_delete_repo (manager, repo);
                    }
                }
                continue;
            } else {
                if (repo->worktree_invalid) {
                    // The repo worktree was invalid, but now it's valid again,
                    // so we start watch it
                    seaf_repo_manager_validate_repo_worktree (seaf->repo_mgr, repo);
                    continue;
                }
            }
        }

        repo->worktree_invalid = FALSE;

        if (!repo->token) {
            /* If the user has logged out of the account, the repo token would
             * be null */
            seaf_debug ("repo token of %s (%.8s) is null, would not sync it\n", repo->name, repo->id);
            continue;
        }

        /* Don't sync repos not checked out yet. */
        if (!repo->head)
            continue;

        if (!manager->priv->auto_sync_enabled || !repo->auto_sync)
            continue;

#ifdef WIN32
        if (repo->version > 0) {
            if (repo->checking_locked_files)
                continue;

            now = (gint64)time(NULL);
            if (repo->last_check_locked_time == 0 ||
                now - repo->last_check_locked_time >= CHECK_LOCKED_FILES_INTERVAL)
            {
                repo->checking_locked_files = TRUE;
                ccnet_job_manager_schedule_job (seaf->job_mgr,
                                                check_locked_files,
                                                check_locked_files_done,
                                                repo);
                repo->last_check_locked_time = now;

            }
        }
#endif

        SyncInfo *info = get_sync_info (manager, repo->id);

        if (info->in_sync)
            continue;

        if (repo->version > 0) {
            /* For repo version > 0, only use http sync. */
            if (check_http_protocol (manager, repo)) {
                sync_repo_v2 (manager, repo, FALSE);
            }
        } else {
            /* If relay is not ready or protocol version is not determined,
             * need to wait.
             */
            if (check_relay_status (manager, repo))
                sync_repo (manager, repo);
        }
    }

    g_list_free (repos);
    return TRUE;
}

inline static void
send_sync_error_notification (SeafRepo *repo, const char *type)
{
    GString *buf = g_string_new (NULL);
    g_string_append_printf (buf, "%s\t%s", repo->name, repo->id);
    seaf_mq_manager_publish_notification (seaf->mq_mgr,
                                          type,
                                          buf->str);
    g_string_free (buf, TRUE);
}

static void
on_repo_fetched (SeafileSession *seaf,
                 TransferTask *tx_task,
                 SeafSyncManager *manager)
{
    SyncInfo *info = get_sync_info (manager, tx_task->repo_id);
    SyncTask *task = info->current_task;

    /* Clone tasks are handled by clone manager. */
    if (tx_task->is_clone)
        return;

    if (task->repo->delete_pending) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
        seaf_repo_manager_del_repo (seaf->repo_mgr, task->repo);
        return;
    }

    if (tx_task->state == TASK_STATE_FINISHED) {
        memcpy (info->head_commit, tx_task->head, 41);

        if (!task->server_side_merge)
            merge_branches_if_necessary (task);
        else
            transition_sync_state (task, SYNC_STATE_DONE);
    } else if (tx_task->state == TASK_STATE_CANCELED) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
    } else if (tx_task->state == TASK_STATE_ERROR) {
        if (tx_task->error == TASK_ERR_ACCESS_DENIED) {
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_ACCESS_DENIED);
            if (!task->repo->access_denied_notified) {
                send_sync_error_notification (task->repo, "sync.access_denied");
                task->repo->access_denied_notified = 1;
            }
        } else if (tx_task->error == TASK_ERR_FILES_LOCKED) {
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_FILES_LOCKED);
        } else
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_FETCH);
    }
}

static void
on_repo_uploaded (SeafileSession *seaf,
                  TransferTask *tx_task,
                  SeafSyncManager *manager)
{
    SyncInfo *info = get_sync_info (manager, tx_task->repo_id);
    SyncTask *task = info->current_task;

    g_return_if_fail (task != NULL && info->in_sync);

    if (task->repo->delete_pending) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
        seaf_repo_manager_del_repo (seaf->repo_mgr, task->repo);
        return;
    }

    if (tx_task->state == TASK_STATE_FINISHED) {
        memcpy (info->head_commit, tx_task->head, 41);

        /* Save current head commit id for GC. */
        seaf_repo_manager_set_repo_property (seaf->repo_mgr,
                                             task->repo->id,
                                             REPO_LOCAL_HEAD,
                                             task->repo->head->commit_id);
        if (!task->server_side_merge)
            transition_sync_state (task, SYNC_STATE_DONE);
        else {
            task->uploaded = TRUE;
            if (!task->http_sync)
                start_sync_repo_proc (manager, task);
            else
                check_head_commit_http (task);
        }
    } else if (tx_task->state == TASK_STATE_CANCELED) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
    } else if (tx_task->state == TASK_STATE_ERROR) {
        if (tx_task->error == TASK_ERR_ACCESS_DENIED) {
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_ACCESS_DENIED);
            if (!task->repo->access_denied_notified) {
                send_sync_error_notification (task->repo, "sync.access_denied");
                task->repo->access_denied_notified = 1;
            }
        } else if (tx_task->error == TASK_ERR_QUOTA_FULL) {
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_QUOTA_FULL);
            /* Only notify "quota full" once. */
            if (!task->repo->quota_full_notified) {
                send_sync_error_notification (task->repo, "sync.quota_full");
                task->repo->quota_full_notified = 1;
            }
        } else
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_UPLOAD);
    }
}

static void
on_repo_http_fetched (SeafileSession *seaf,
                      HttpTxTask *tx_task,
                      SeafSyncManager *manager)
{
    SyncInfo *info = get_sync_info (manager, tx_task->repo_id);
    SyncTask *task = info->current_task;

    /* Clone tasks are handled by clone manager. */
    if (tx_task->is_clone)
        return;

    if (task->repo->delete_pending) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
        seaf_repo_manager_del_repo (seaf->repo_mgr, task->repo);
        return;
    }

    if (tx_task->state == HTTP_TASK_STATE_FINISHED) {
        memcpy (info->head_commit, tx_task->head, 41);
        transition_sync_state (task, SYNC_STATE_DONE);
    } else if (tx_task->state == HTTP_TASK_STATE_CANCELED) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
    } else if (tx_task->state == HTTP_TASK_STATE_ERROR) {
        if (tx_task->error == HTTP_TASK_ERR_FORBIDDEN) {
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_ACCESS_DENIED);
            if (!task->repo->access_denied_notified) {
                send_sync_error_notification (task->repo, "sync.access_denied");
                task->repo->access_denied_notified = 1;
            }
        } else if (tx_task->error == HTTP_TASK_ERR_FILES_LOCKED) {
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_FILES_LOCKED);
        } else if (tx_task->error == HTTP_TASK_ERR_REPO_DELETED) {
            on_repo_deleted_on_server (task, task->repo);
        } else
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_FETCH);
    }
}

static void
on_repo_http_uploaded (SeafileSession *seaf,
                       HttpTxTask *tx_task,
                       SeafSyncManager *manager)
{
    SyncInfo *info = get_sync_info (manager, tx_task->repo_id);
    SyncTask *task = info->current_task;

    g_return_if_fail (task != NULL && info->in_sync);

    if (task->repo->delete_pending) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
        seaf_repo_manager_del_repo (seaf->repo_mgr, task->repo);
        return;
    }

    if (tx_task->state == HTTP_TASK_STATE_FINISHED) {
        memcpy (info->head_commit, tx_task->head, 41);

        /* Save current head commit id for GC. */
        seaf_repo_manager_set_repo_property (seaf->repo_mgr,
                                             task->repo->id,
                                             REPO_LOCAL_HEAD,
                                             task->repo->head->commit_id);
        task->uploaded = TRUE;
        check_head_commit_http (task);
    } else if (tx_task->state == HTTP_TASK_STATE_CANCELED) {
        transition_sync_state (task, SYNC_STATE_CANCELED);
    } else if (tx_task->state == HTTP_TASK_STATE_ERROR) {
        if (tx_task->error == HTTP_TASK_ERR_FORBIDDEN) {
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_ACCESS_DENIED);
            if (!task->repo->access_denied_notified) {
                send_sync_error_notification (task->repo, "sync.access_denied");
                task->repo->access_denied_notified = 1;
            }
        } else if (tx_task->error == HTTP_TASK_ERR_NO_QUOTA) {
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_QUOTA_FULL);
            /* Only notify "quota full" once. */
            if (!task->repo->quota_full_notified) {
                send_sync_error_notification (task->repo, "sync.quota_full");
                task->repo->quota_full_notified = 1;
            }
        } else if (tx_task->error == HTTP_TASK_ERR_REPO_DELETED) {
            on_repo_deleted_on_server (task, task->repo);
        } else
            seaf_sync_manager_set_task_error (task, SYNC_ERROR_UPLOAD);
    }
}

const char *
sync_error_to_str (int error)
{
    if (error < 0 || error >= SYNC_ERROR_NUM) {
        seaf_warning ("illegal sync error: %d\n", error); 
        return NULL;
    }

    return sync_error_str[error];
}

const char *
sync_state_to_str (int state)
{
    if (state < 0 || state >= SYNC_STATE_NUM) {
        seaf_warning ("illegal sync state: %d\n", state); 
        return NULL;
    }

    return sync_state_str[state];
}

static void
disable_auto_sync_for_repos (SeafSyncManager *mgr)
{
    GList *repos;
    GList *ptr;
    SeafRepo *repo;

    repos = seaf_repo_manager_get_repo_list (seaf->repo_mgr, -1, -1);
    for (ptr = repos; ptr; ptr = ptr->next) {
        repo = ptr->data;
        seaf_wt_monitor_unwatch_repo (seaf->wt_monitor, repo->id);
        seaf_sync_manager_cancel_sync_task (mgr, repo->id);
        seaf_sync_manager_remove_active_path_info (mgr, repo->id);
    }

    g_list_free (repos);
}

int
seaf_sync_manager_disable_auto_sync (SeafSyncManager *mgr)
{
    if (!seaf->started) {
        seaf_message ("sync manager is not started, skip disable auto sync.\n");
        return -1;
    }

    disable_auto_sync_for_repos (mgr);

    mgr->priv->auto_sync_enabled = FALSE;
    g_debug ("[sync mgr] auto sync is disabled\n");
    return 0;
}

static void
enable_auto_sync_for_repos (SeafSyncManager *mgr)
{
    GList *repos;
    GList *ptr;
    SeafRepo *repo;

    repos = seaf_repo_manager_get_repo_list (seaf->repo_mgr, -1, -1);
    for (ptr = repos; ptr; ptr = ptr->next) {
        repo = ptr->data;
        seaf_wt_monitor_watch_repo (seaf->wt_monitor, repo->id, repo->worktree);
    }

    g_list_free (repos);
}

int
seaf_sync_manager_enable_auto_sync (SeafSyncManager *mgr)
{
    if (!seaf->started) {
        seaf_message ("sync manager is not started, skip enable auto sync.\n");
        return -1;
    }

    enable_auto_sync_for_repos (mgr);

    mgr->priv->auto_sync_enabled = TRUE;
    g_debug ("[sync mgr] auto sync is enabled\n");
    return 0;
}

int
seaf_sync_manager_is_auto_sync_enabled (SeafSyncManager *mgr)
{
    if (mgr->priv->auto_sync_enabled)
        return 1;
    else
        return 0;
}

static ActivePathsInfo *
active_paths_info_new (SeafRepo *repo)
{
    ActivePathsInfo *info = g_new0 (ActivePathsInfo, 1);

    info->paths = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
    info->syncing_tree = sync_status_tree_new (repo->worktree);
    info->synced_tree = sync_status_tree_new (repo->worktree);

    return info;
}

static void
active_paths_info_free (ActivePathsInfo *info)
{
    if (!info)
        return;
    g_hash_table_destroy (info->paths);
    sync_status_tree_free (info->syncing_tree);
    sync_status_tree_free (info->synced_tree);
    g_free (info);
}

void
seaf_sync_manager_update_active_path (SeafSyncManager *mgr,
                                      const char *repo_id,
                                      const char *path,
                                      int mode,
                                      SyncStatus status)
{
    ActivePathsInfo *info;
    SeafRepo *repo;

    if (!repo_id || !path) {
        seaf_warning ("BUG: empty repo_id or path.\n");
        return;
    }

    if (status <= SYNC_STATUS_NONE || status >= N_SYNC_STATUS) {
        seaf_warning ("BUG: invalid sync status %d.\n", status);
        return;
    }

    pthread_mutex_lock (&mgr->priv->paths_lock);

    info = g_hash_table_lookup (mgr->priv->active_paths, repo_id);
    if (!info) {
        repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id);
        if (!repo) {
            pthread_mutex_unlock (&mgr->priv->paths_lock);
            return;
        }
        info = active_paths_info_new (repo);
        g_hash_table_insert (mgr->priv->active_paths, g_strdup(repo_id), info);
    }

    SyncStatus existing = (SyncStatus) g_hash_table_lookup (info->paths, path);
    if (!existing) {
        g_hash_table_insert (info->paths, g_strdup(path), (void*)status);
        if (status == SYNC_STATUS_SYNCING)
            sync_status_tree_add (info->syncing_tree, path, mode);
        else if (status == SYNC_STATUS_SYNCED)
            sync_status_tree_add (info->synced_tree, path, mode);
        else {
#ifdef WIN32
            seaf_sync_manager_add_refresh_path (mgr, path);
#endif
        }
    } else if (existing != status) {
        g_hash_table_replace (info->paths, g_strdup(path), (void*)status);

        if (existing == SYNC_STATUS_SYNCING)
            sync_status_tree_del (info->syncing_tree, path);
        else if (existing == SYNC_STATUS_SYNCED)
            sync_status_tree_del (info->synced_tree, path);

        if (status == SYNC_STATUS_SYNCING)
            sync_status_tree_add (info->syncing_tree, path, mode);
        else if (status == SYNC_STATUS_SYNCED)
            sync_status_tree_add (info->synced_tree, path, mode);

#ifdef WIN32
        seaf_sync_manager_add_refresh_path (mgr, path);
#endif
    }

    pthread_mutex_unlock (&mgr->priv->paths_lock);
}

void
seaf_sync_manager_delete_active_path (SeafSyncManager *mgr,
                                      const char *repo_id,
                                      const char *path)
{
    ActivePathsInfo *info;

    if (!repo_id || !path) {
        seaf_warning ("BUG: empty repo_id or path.\n");
        return;
    }

    pthread_mutex_lock (&mgr->priv->paths_lock);

    info = g_hash_table_lookup (mgr->priv->active_paths, repo_id);
    if (!info) {
        pthread_mutex_unlock (&mgr->priv->paths_lock);
        return;
    }

    g_hash_table_remove (info->paths, path);
    sync_status_tree_del (info->syncing_tree, path);
    sync_status_tree_del (info->synced_tree, path);

    pthread_mutex_unlock (&mgr->priv->paths_lock);
}

static char *path_status_tbl[] = {
    "none",
    "syncing",
    "error",
    "ignored",
    "synced",
    "paused",
    "readonly",
    "locked",
    "locked_by_me",
    NULL,
};

static char *
get_repo_sync_status (SeafSyncManager *mgr, const char *repo_id)
{
    SyncInfo *info = get_sync_info (mgr, repo_id);
    SeafRepo *repo;

    if (info->in_error)
        return g_strdup(path_status_tbl[SYNC_STATUS_ERROR]);

    repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id);
    if (!repo)
        return g_strdup(path_status_tbl[SYNC_STATUS_NONE]);

    if (!repo->auto_sync || !mgr->priv->auto_sync_enabled)
        return g_strdup(path_status_tbl[SYNC_STATUS_PAUSED]);

    char allzeros[41] = {0};
    if (!info->in_sync && memcmp(allzeros, info->head_commit, 41) == 0)
        return g_strdup(path_status_tbl[SYNC_STATUS_NONE]);

    if (info->in_sync &&
        (info->current_task->state == SYNC_STATE_COMMIT ||
         info->current_task->state == SYNC_STATE_FETCH ||
         info->current_task->state == SYNC_STATE_UPLOAD ||
         info->current_task->state == SYNC_STATE_MERGE))
        return g_strdup(path_status_tbl[SYNC_STATUS_SYNCING]);
    else if (!repo->is_readonly)
        return g_strdup(path_status_tbl[SYNC_STATUS_SYNCED]);
    else
        return g_strdup(path_status_tbl[SYNC_STATUS_READONLY]);
}

char *
seaf_sync_manager_get_path_sync_status (SeafSyncManager *mgr,
                                        const char *repo_id,
                                        const char *path,
                                        gboolean is_dir)
{
    ActivePathsInfo *info;
    SyncInfo *sync_info;
    SyncStatus ret = SYNC_STATUS_NONE;

    if (!repo_id || !path) {
        seaf_warning ("BUG: empty repo_id or path.\n");
        return NULL;
    }

    if (path[0] == 0) {
        return get_repo_sync_status (mgr, repo_id);
    }

    /* If the repo is in error, all files in it should show no sync status. */
    sync_info = get_sync_info (mgr, repo_id);
    if (sync_info && sync_info->in_error)
        return SYNC_STATUS_NONE;

    pthread_mutex_lock (&mgr->priv->paths_lock);

    info = g_hash_table_lookup (mgr->priv->active_paths, repo_id);
    if (!info) {
        pthread_mutex_unlock (&mgr->priv->paths_lock);
        ret = SYNC_STATUS_NONE;
        goto out;
    }

    ret = (SyncStatus) g_hash_table_lookup (info->paths, path);
    if (is_dir && (ret == SYNC_STATUS_NONE)) {
        /* If a dir is not in the syncing tree but in the synced tree,
         * it's synced. Otherwise if it's in the syncing tree, some files
         * under it must be syncing, so it should be in syncing status too.
         */
        if (sync_status_tree_exists (info->syncing_tree, path))
            ret = SYNC_STATUS_SYNCING;
        else if (sync_status_tree_exists (info->synced_tree, path))
            ret = SYNC_STATUS_SYNCED;
    }

    pthread_mutex_unlock (&mgr->priv->paths_lock);

    if (ret == SYNC_STATUS_SYNCED) {
        if (!seaf_repo_manager_is_path_writable(seaf->repo_mgr, repo_id, path))
            ret = SYNC_STATUS_READONLY;
        else if (seaf_filelock_manager_is_file_locked_by_me (seaf->filelock_mgr,
                                                             repo_id, path))
            ret = SYNC_STATUS_LOCKED_BY_ME;
        else if (seaf_filelock_manager_is_file_locked (seaf->filelock_mgr,
                                                       repo_id, path))
            ret = SYNC_STATUS_LOCKED;
    }

out:
    return g_strdup(path_status_tbl[ret]);
}

static json_t *
active_paths_to_json (GHashTable *paths)
{
    json_t *array = NULL, *obj = NULL;
    GHashTableIter iter;
    gpointer key, value;
    char *path;
    SyncStatus status;

    array = json_array ();

    g_hash_table_iter_init (&iter, paths);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        path = key;
        status = (SyncStatus)value;

        obj = json_object ();
        json_object_set (obj, "path", json_string(path));
        json_object_set (obj, "status", json_string(path_status_tbl[status]));

        json_array_append (array, obj);
    }

    return array;
}

char *
seaf_sync_manager_list_active_paths_json (SeafSyncManager *mgr)
{
    json_t *array = NULL, *obj = NULL, *path_array = NULL;
    GHashTableIter iter;
    gpointer key, value;
    char *repo_id;
    ActivePathsInfo *info;
    char *ret = NULL;

    pthread_mutex_lock (&mgr->priv->paths_lock);

    array = json_array ();

    g_hash_table_iter_init (&iter, mgr->priv->active_paths);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        repo_id = key;
        info = value;

        obj = json_object();
        path_array = active_paths_to_json (info->paths);
        json_object_set (obj, "repo_id", json_string(repo_id));
        json_object_set (obj, "paths", path_array);

        json_array_append (array, obj);
    }

    pthread_mutex_unlock (&mgr->priv->paths_lock);

    ret = json_dumps (array, JSON_INDENT(4));
    if (!ret) {
        seaf_warning ("Failed to convert active paths to json\n");
    }

    json_decref (array);

    return ret;
}

int
seaf_sync_manager_active_paths_number (SeafSyncManager *mgr)
{
    GHashTableIter iter;
    gpointer key, value;
    ActivePathsInfo *info;
    int ret = 0;

    g_hash_table_iter_init (&iter, mgr->priv->active_paths);
    while (g_hash_table_iter_next (&iter, &key, &value)) {
        info = value;
        ret += g_hash_table_size(info->paths);
    }

    return ret;
}

void
seaf_sync_manager_remove_active_path_info (SeafSyncManager *mgr, const char *repo_id)
{
    ActivePathsInfo *info;

    pthread_mutex_lock (&mgr->priv->paths_lock);

    g_hash_table_remove (mgr->priv->active_paths, repo_id);

    pthread_mutex_unlock (&mgr->priv->paths_lock);

#ifdef WIN32
    /* This is a hack to tell Windows Explorer to refresh all open windows. */
    SHChangeNotify (SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
#endif
}

#ifdef WIN32

static wchar_t *
win_path (const char *path)
{
    char *ret = g_strdup(path);
    wchar_t *ret_w;
    char *p;

    for (p = ret; *p != 0; ++p)
        if (*p == '/')
            *p = '\\';

    ret_w = g_utf8_to_utf16 (ret, -1, NULL, NULL, NULL);

    g_free (ret);
    return ret_w;
}

static void *
refresh_windows_explorer_thread (void *vdata)
{
    GAsyncQueue *q = vdata;
    char *path;
    wchar_t *wpath;
    int count = 0;

    while (1) {
        path = g_async_queue_pop (q);
        wpath = win_path (path);

        SHChangeNotify (SHCNE_ATTRIBUTES, SHCNF_PATHW, wpath, NULL);

        g_free (path);
        g_free (wpath);

        if (++count >= 100) {
            g_usleep (G_USEC_PER_SEC);
            count = 0;
        }
    }
}

void
seaf_sync_manager_add_refresh_path (SeafSyncManager *mgr, const char *path)
{
    g_async_queue_push (mgr->priv->refresh_paths, g_strdup(path));
}

void
seaf_sync_manager_refresh_path (SeafSyncManager *mgr, const char *path)
{
    wchar_t *wpath;

    wpath = win_path (path);
    SHChangeNotify (SHCNE_ATTRIBUTES, SHCNF_PATHW, wpath, NULL);
    g_free (wpath);
}

#endif
