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

#include "fm-folder.h"
#include "fm-monitor.h"
#include "fm-marshal.h"
#include <string.h>

enum {
    FILES_ADDED,
    FILES_REMOVED,
    FILES_CHANGED,
    LOADED,
    UNMOUNT,
    ERROR,
    N_SIGNALS
};

static FmFolder* fm_folder_new_internal(FmPath* path, GFile* gf);
static FmFolder* fm_folder_get_internal(FmPath* path, GFile* gf);
static void fm_folder_finalize              (GObject *object);

static void on_file_info_finished(FmFileInfoJob* job, FmFolder* folder);
static gboolean on_idle(FmFolder* folder);

G_DEFINE_TYPE(FmFolder, fm_folder, G_TYPE_OBJECT);

static GList* _fm_folder_get_file_by_name(FmFolder* folder, const char* name);

static guint signals[N_SIGNALS];
static GHashTable* hash = NULL; /* FIXME: should this be guarded with a mutex? */

static void fm_folder_class_init(FmFolderClass *klass)
{
    GObjectClass *g_object_class;
    g_object_class = G_OBJECT_CLASS(klass);
    g_object_class->finalize = fm_folder_finalize;
    fm_folder_parent_class = (GObjectClass*)g_type_class_peek(G_TYPE_OBJECT);

    /*
    * files-added is emitted when there is a new file created in the dir.
    * The param is GList* of the newly added file.
    */
    signals[ FILES_ADDED ] =
        g_signal_new ( "files-added",
                       G_TYPE_FROM_CLASS ( klass ),
                       G_SIGNAL_RUN_FIRST,
                       G_STRUCT_OFFSET ( FmFolderClass, files_added ),
                       NULL, NULL,
                       g_cclosure_marshal_VOID__POINTER,
                       G_TYPE_NONE, 1, G_TYPE_POINTER );

    /*
    * files-removed is emitted when there is a file deleted in the dir.
    * The param is GList* of the removed files.
    */
    signals[ FILES_REMOVED ] =
        g_signal_new ( "files-removed",
                       G_TYPE_FROM_CLASS ( klass ),
                       G_SIGNAL_RUN_FIRST,
                       G_STRUCT_OFFSET ( FmFolderClass, files_removed ),
                       NULL, NULL,
                       g_cclosure_marshal_VOID__POINTER,
                       G_TYPE_NONE, 1, G_TYPE_POINTER );

    /*
    * file-changed is emitted when there is a file changed in the dir.
    * The param is VFSFileInfo of the newly created file.
    */
    signals[ FILES_CHANGED ] =
        g_signal_new ( "files-changed",
                       G_TYPE_FROM_CLASS ( klass ),
                       G_SIGNAL_RUN_FIRST,
                       G_STRUCT_OFFSET ( FmFolderClass, files_changed ),
                       NULL, NULL,
                       g_cclosure_marshal_VOID__POINTER,
                       G_TYPE_NONE, 1, G_TYPE_POINTER );

    signals[ LOADED ] =
        g_signal_new ( "loaded",
                       G_TYPE_FROM_CLASS ( klass ),
                       G_SIGNAL_RUN_FIRST,
                       G_STRUCT_OFFSET ( FmFolderClass, loaded ),
                       NULL, NULL,
                       g_cclosure_marshal_VOID__VOID,
                       G_TYPE_NONE, 0);

    signals[ UNMOUNT ] =
        g_signal_new ( "unmount",
                       G_TYPE_FROM_CLASS ( klass ),
                       G_SIGNAL_RUN_FIRST,
                       G_STRUCT_OFFSET ( FmFolderClass, unmount ),
                       NULL, NULL,
                       g_cclosure_marshal_VOID__VOID,
                       G_TYPE_NONE, 0);

    signals[ERROR] =
        g_signal_new( "error",
                      G_TYPE_FROM_CLASS ( klass ),
                      G_SIGNAL_RUN_LAST,
                      G_STRUCT_OFFSET ( FmFolderClass, error ),
                      NULL, NULL,
                      fm_marshal_INT__POINTER_INT,
                      G_TYPE_INT, 2, G_TYPE_POINTER, G_TYPE_INT );
}


static void fm_folder_init(FmFolder *self)
{
    self->files = fm_file_info_list_new();
}

void on_file_info_finished(FmFileInfoJob* job, FmFolder* folder)
{
    GList* l;
    GSList* files_to_add = NULL;
    GSList* files_to_update = NULL;

    for(l=fm_list_peek_head_link(job->file_infos);l;l=l->next)
    {
        FmFileInfo* fi = (FmFileInfo*)l->data;
        GList* l2 = _fm_folder_get_file_by_name(folder, fi->path->name);
        if(l2) /* the file is already in the folder, update */
        {
            FmFileInfo* fi2 = (FmFileInfo*)l2->data;
            /* FIXME: will fm_file_info_copy here cause problems?
             *        the file info might be referenced by others, too.
             *        we're mofifying an object referenced by others.
             *        we should redesign the API, or document this clearly
             *        in future API doc.
             */
            fm_file_info_copy(fi2, fi);
            files_to_update = g_slist_prepend(files_to_update, fi2);
        }
        else
        {
            files_to_add = g_slist_prepend(files_to_add, fi);
            fm_file_info_ref(fi);
            fm_list_push_tail(folder->files, fi);
        }
    }
    if(files_to_add)
    {
        g_signal_emit(folder, signals[FILES_ADDED], 0, files_to_add);
        g_slist_free(files_to_add);
    }
    if(files_to_update)
    {
        g_signal_emit(folder, signals[FILES_CHANGED], 0, files_to_update);
        g_slist_free(files_to_update);
    }

    folder->pending_jobs = g_slist_remove(folder->pending_jobs, job);
}

gboolean on_idle(FmFolder* folder)
{
    GSList* l;
    FmFileInfoJob* job = NULL;
    FmPath* path;
    folder->idle_handler = 0;
    if(folder->files_to_update || folder->files_to_add)
        job = (FmFileInfoJob*)fm_file_info_job_new(NULL, 0);

    if(folder->files_to_update)
    {
        GSList* prev = NULL;
        for(l=folder->files_to_update;l;)
        {
            /* if a file is already in files_to_add, remove it. */
            if(g_slist_find_custom(folder->files_to_add, l->data, (GCompareFunc)strcmp))
            {
                GSList* tmp = l;
                l=l->next;
                if(G_LIKELY(prev))
                    prev->next = l;
                g_free(tmp->data);
                g_slist_free_1(tmp);
                if(G_UNLIKELY(tmp == folder->files_to_update))
                    folder->files_to_update = l;
                continue;
            }
            path = fm_path_new_child(folder->dir_path, (char*)l->data);
            fm_file_info_job_add(job, path);
            fm_path_unref(path);
            g_free(l->data);

            prev = l;
            l=l->next;
        }
        g_slist_free(folder->files_to_update);
        folder->files_to_update = NULL;
    }

    if(folder->files_to_add)
    {
        for(l=folder->files_to_add;l;l=l->next)
        {
            path = fm_path_new_child(folder->dir_path, (char*)l->data);
            fm_file_info_job_add(job, path);
            fm_path_unref(path);
            g_free(l->data);
        }
        g_slist_free(folder->files_to_add);
        folder->files_to_add = NULL;
    }

    if(job)
    {
        g_signal_connect(job, "finished", on_file_info_finished, folder);
        folder->pending_jobs = g_slist_prepend(folder->pending_jobs, job);
        fm_job_run_async(FM_JOB(job));
    }

    if(folder->files_to_del)
    {
        GSList* ll;
        for(ll=folder->files_to_del;ll;ll=ll->next)
        {
            GList* l= (GList*)ll->data;
            ll->data = l->data;
            fm_list_delete_link_nounref(folder->files , l);
        }
        g_signal_emit(folder, signals[FILES_REMOVED], 0, folder->files_to_del);
        g_slist_foreach(folder->files_to_del, (GFunc)fm_file_info_unref, NULL);
        g_slist_free(folder->files_to_del);
        folder->files_to_del = NULL;
    }

    return FALSE;
}

static void on_folder_changed(GFileMonitor* mon, GFile* gf, GFile* other, GFileMonitorEvent evt, FmFolder* folder)
{
    GList* l;
    char* name;
/*
    const char* names[]={
        "G_FILE_MONITOR_EVENT_CHANGED",
        "G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT",
        "G_FILE_MONITOR_EVENT_DELETED",
        "G_FILE_MONITOR_EVENT_CREATED",
        "G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED",
        "G_FILE_MONITOR_EVENT_PRE_UNMOUNT",
        "G_FILE_MONITOR_EVENT_UNMOUNTED"
    };
    name = g_file_get_basename(gf);
    g_debug("folder: %p, file %s event: %s", folder, name, names[evt]);
    g_free(name);
*/

    if(g_file_equal(gf, folder->gf))
    {
        g_debug("event of the folder itself: %d", evt);
        /* FIXME: handle unmount events */
        if(evt == G_FILE_MONITOR_EVENT_PRE_UNMOUNT)
        {
            g_debug("folder is going to be unmounted");
        }
        else if(evt == G_FILE_MONITOR_EVENT_UNMOUNTED)
        {
            g_signal_emit(folder, signals[UNMOUNT], 0);
            g_debug("folder is unmounted");
        }
        return;
    }

    name = g_file_get_basename(gf);
    switch(evt)
    {
    case G_FILE_MONITOR_EVENT_CREATED:
        folder->files_to_add = g_slist_append(folder->files_to_add, name);
        break;
    case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
    case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
        folder->files_to_update = g_slist_append(folder->files_to_update, name);
        break;
    case G_FILE_MONITOR_EVENT_DELETED:
        l = _fm_folder_get_file_by_name(folder, name);
        if(l && !g_slist_find(folder->files_to_del, l) )
            folder->files_to_del = g_slist_prepend(folder->files_to_del, l);
        g_free(name);
        break;
    default:
        /* g_debug("folder %p %s event: %s", folder, name, names[evt]); */
        g_free(name);
        return;
    }
    if(!folder->idle_handler)
        folder->idle_handler = g_idle_add_full(G_PRIORITY_LOW, on_idle, folder, NULL);
}

static void on_job_finished(FmDirListJob* job, FmFolder* folder)
{
    GList* l;
    GSList* files = NULL;
    /* actually manually disconnecting from 'finished' signal is not
     * needed since the signal is only emit once, and later the job
     * object will be distroyed very soon. */
    /* g_signal_handlers_disconnect_by_func(job, on_job_finished, folder); */
    for(l = fm_list_peek_head_link(job->files); l; l=l->next)
    {
        FmFileInfo* inf = (FmFileInfo*)l->data;
        files = g_slist_prepend(files, inf);
        fm_list_push_tail(folder->files, inf);
    }
    if(G_LIKELY(files))
        g_signal_emit(folder, signals[FILES_ADDED], 0, files);

    if(job->dir_fi)
        folder->dir_fi = fm_file_info_ref(job->dir_fi);

    folder->job = NULL; /* the job object will be freed in idle handler. */
    g_signal_emit(folder, signals[LOADED], 0);
}

static FmJobErrorAction on_job_err(FmDirListJob* job, GError* err, FmJobErrorSeverity severity, FmFolder* folder)
{
	FmJobErrorAction ret;
	g_signal_emit(folder, signals[ERROR], 0, err, severity, &ret);
    return ret;
}

FmFolder* fm_folder_new_internal(FmPath* path, GFile* gf)
{
    GError* err = NULL;
    FmFolder* folder = (FmFolder*)g_object_new(FM_TYPE_FOLDER, NULL);
    folder->dir_path = fm_path_ref(path);

    folder->gf = (GFile*)g_object_ref(gf);

    folder->mon = fm_monitor_directory(gf, &err);
    if(folder->mon)
        g_signal_connect(folder->mon, "changed", G_CALLBACK(on_folder_changed), folder );
    else
        g_error_free(err);

    fm_folder_reload(folder);
    return folder;
}

FmFolder* fm_folder_get_internal(FmPath* path, GFile* gf)
{
    FmFolder* folder;
    /* FIXME: should we provide a generic FmPath cache in fm-path.c
     * to associate all kinds of data structures with FmPaths? */

    /* FIXME: should creation of the hash table be moved to fm_init()? */
    if( G_LIKELY(hash) )
        folder = (FmFolder*)g_hash_table_lookup(hash, path);
    else
    {
        hash = g_hash_table_new((GHashFunc)fm_path_hash, (GEqualFunc)fm_path_equal);
        folder = NULL;
    }

    if( G_UNLIKELY(!folder) )
    {
        GFile* _gf = NULL;
        if(!gf)
            _gf = gf = fm_path_to_gfile(path);
        folder = fm_folder_new_internal(path, gf);
        if(_gf)
            g_object_unref(_gf);
        g_hash_table_insert(hash, folder->dir_path, folder);
    }
    else
        return (FmFolder*)g_object_ref(folder);
    return folder;
}

static void fm_folder_finalize(GObject *object)
{
    FmFolder *self;
    g_return_if_fail(object != NULL);
    g_return_if_fail(FM_IS_FOLDER(object));

    self = FM_FOLDER(object);

    if(self->job)
    {
        g_signal_handlers_disconnect_by_func(self->job, on_job_finished, self);
        g_signal_handlers_disconnect_by_func(self->job, on_job_err, self);
        fm_job_cancel(FM_JOB(self->job)); /* FIXME: is this ok? */
        /* the job will be freed automatically in idle handler. */
    }

    if(self->pending_jobs)
    {
        GSList* l;
        for(l = self->pending_jobs;l;l=l->next)
        {
            FmJob* job = FM_JOB(l->data);
            g_signal_handlers_disconnect_by_func(job, on_job_finished, self);
            fm_job_cancel(job);
            /* the job will be freed automatically in idle handler. */
        }
    }

    /* remove from hash table */
    g_hash_table_remove(hash, self->dir_path);
    if(self->dir_path)
        fm_path_unref(self->dir_path);

    if(self->dir_fi)
        fm_file_info_unref(self->dir_fi);

    if(self->gf)
        g_object_unref(self->gf);

    if(self->mon)
    {
        g_signal_handlers_disconnect_by_func(self->mon, on_folder_changed, self);
        g_object_unref(self->mon);
    }

    if(self->idle_handler)
    {
        g_source_remove(self->idle_handler);
        if(self->files_to_add)
        {
            g_slist_foreach(self->files_to_add, (GFunc)g_free, NULL);
            g_slist_free(self->files_to_add);
        }
        if(self->files_to_update)
        {
            g_slist_foreach(self->files_to_update, (GFunc)g_free, NULL);
            g_slist_free(self->files_to_update);
        }
        if(self->files_to_del)
            g_slist_free(self->files_to_del);
    }

    fm_list_unref(self->files);

    if (G_OBJECT_CLASS(fm_folder_parent_class)->finalize)
        (* G_OBJECT_CLASS(fm_folder_parent_class)->finalize)(object);
}

FmFolder*    fm_folder_get_for_gfile    (GFile* gf)
{
    FmPath* path = fm_path_new_for_gfile(gf);
    FmFolder* folder = fm_folder_new_internal(path, gf);
    fm_path_unref(path);
    return folder;
}

FmFolder* fm_folder_get_for_path(FmPath* path)
{
    return fm_folder_get_internal(path, NULL);
}

FmFolder* fm_folder_get_for_path_name(const char* path)
{
    FmPath* fm_path = fm_path_new(path);
    FmFolder* folder = fm_folder_get_internal(fm_path, NULL);
    fm_path_unref(fm_path);
    return folder;
}

/* FIXME: should we use GFile here? */
FmFolder*    fm_folder_get_for_uri    (const char* uri)
{
    GFile* gf = g_file_new_for_uri(uri);
    FmFolder* folder = fm_folder_get_for_gfile(gf);
    g_object_unref(gf);
    return folder;
}

void fm_folder_reload(FmFolder* folder)
{
    /* FIXME: remove all items and re-run a dir list job. */
    GSList* files_to_del = NULL;
    GList* l = fm_list_peek_head_link(folder->files);
    if(l)
    {
        for(;l;l=l->next)
        {
            FmFileInfo* fi = (FmFileInfo*)l->data;
            files_to_del = g_slist_prepend(files_to_del, fi);
        }
        g_signal_emit(folder, signals[FILES_REMOVED], 0, files_to_del);
        fm_list_clear(folder->files); /* fm_file_info_unref will be invoked. */
        g_slist_free(files_to_del);
    }

    folder->job = fm_dir_list_job_new(folder->dir_path);
    g_signal_connect(folder->job, "finished", G_CALLBACK(on_job_finished), folder);
    g_signal_connect(folder->job, "error", G_CALLBACK(on_job_err), folder);
    fm_job_run_async(FM_JOB(folder->job));
}

FmFileInfoList* fm_folder_get_files (FmFolder* folder)
{
    return folder->files;
}

GList* _fm_folder_get_file_by_name(FmFolder* folder, const char* name)
{
    GList* l = fm_list_peek_head_link(folder->files);
    for(;l;l=l->next)
    {
        FmFileInfo* fi = (FmFileInfo*)l->data;
        if(strcmp(fi->path->name, name) == 0)
            return l;
    }
    return NULL;
}

FmFileInfo* fm_folder_get_file_by_name(FmFolder* folder, const char* name)
{
    GList* l = _fm_folder_get_file_by_name(folder, name);
    return l ? (FmFileInfo*)l->data : NULL;
}

FmFolder* fm_folder_get(FmPath* path)
{
    return fm_folder_get_internal(path, NULL);
}

gboolean fm_folder_get_is_loading(FmFolder* folder)
{
    return (folder->job != NULL);
}
