/*
 * Seahorse
 *
 * Copyright (C) 2004 Stefan Walter
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <sys/types.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>

#include <gio/gio.h>
#include <gtk/gtk.h>

#include "seahorse-gpgmex.h"
#include "seahorse-vfs-data.h"
#include "seahorse-util.h"

#define PROGRESS_BLOCK  16 * 1024

/* The current/last VFS operation */
typedef enum _VfsAsyncOp
{
    VFS_OP_NONE,
    VFS_OP_OPENING,
    VFS_OP_READING,
    VFS_OP_WRITING,
    VFS_OP_FLUSHING,
    VFS_OP_SEEKING
}
VfsAsyncOp;

/* The state of the VFS system */
typedef enum _VfsAsyncState
{
    VFS_ASYNC_PROCESSING,
    VFS_ASYNC_CANCELLED,
    VFS_ASYNC_READY
}
VfsAsyncState;

/* A handle for VFS work */
typedef struct _VfsAsyncHandle {
    
    gpgme_data_t gdata;             /* A pointer to the outside gpgme_data_t handle */
    
    GFile *file;                    /* The file we're operating on */
    GCancellable *cancellable;      /* For cancelling any concurrent operation */
    gboolean writer;                /* Whether a writer or a reader */
    GInputStream *istream;          /* Stream for reading from */
    GOutputStream *ostream;         /* Stream for writing to */
    
    VfsAsyncOp    operation;        /* The last/current operation */
    VfsAsyncState state;            /* State of the last/current operation */

    GError *error;                  /* Result of the current operation */
    gpointer buffer;                /* Current operation's buffer */
    goffset processed;              /* Number of bytes processed in current op */
    
    goffset last;                   /* Last update sent about number of bytes */
    goffset total;                  /* Total number of bytes read or written */
    
    SeahorseVfsProgressCb progcb;   /* Progress callback */
    gpointer userdata;              /* User data for progress callback */
} VfsAsyncHandle;

static void vfs_data_cancel(VfsAsyncHandle *ah);

/* Waits for a given VFS operation to complete without halting the UI */
static gboolean
vfs_data_wait_results(VfsAsyncHandle* ah, gboolean errors)
{
    VfsAsyncOp op;
    gint code;
    
    seahorse_util_wait_until (ah->state != VFS_ASYNC_PROCESSING);

    op = ah->operation;
    ah->operation = VFS_OP_NONE;
    
    /* Cancelling looks like an error to our caller */
    if (ah->state == VFS_ASYNC_CANCELLED) {
        errno = 0;
        return FALSE;
    }
        
    g_assert (ah->state == VFS_ASYNC_READY);
    
    /* There was no operation going on, result codes are not relevant */
    if (op == VFS_OP_NONE)
        return TRUE;
    
    /* No error result */
    if (!ah->error) {
	    return TRUE;
    }

    code = -1;
    if (ah->error->domain == G_IO_ERROR)
        code = ah->error->code;
    
    if (code == G_IO_ERROR_CANCELLED) {
        vfs_data_cancel (ah);
        errno = 0;
        return FALSE;
    }
    
    /* Check for error codes */
    if(errors) {
        switch(code) {
        #define GIO_TO_SYS_ERR(v, s)    \
            case v: errno = s; break;
        GIO_TO_SYS_ERR (G_IO_ERROR_FAILED, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_NOT_FOUND, ENOENT);
        GIO_TO_SYS_ERR (G_IO_ERROR_EXISTS, EEXIST);
        GIO_TO_SYS_ERR (G_IO_ERROR_IS_DIRECTORY, EISDIR);
        GIO_TO_SYS_ERR (G_IO_ERROR_NOT_DIRECTORY, ENOTDIR);
        GIO_TO_SYS_ERR (G_IO_ERROR_NOT_EMPTY, ENOTEMPTY);
        GIO_TO_SYS_ERR (G_IO_ERROR_NOT_REGULAR_FILE, EINVAL);
        GIO_TO_SYS_ERR (G_IO_ERROR_NOT_SYMBOLIC_LINK, EINVAL);
        GIO_TO_SYS_ERR (G_IO_ERROR_NOT_MOUNTABLE_FILE, EINVAL);
        GIO_TO_SYS_ERR (G_IO_ERROR_FILENAME_TOO_LONG, ENAMETOOLONG);
        GIO_TO_SYS_ERR (G_IO_ERROR_INVALID_FILENAME, EINVAL);
        GIO_TO_SYS_ERR (G_IO_ERROR_TOO_MANY_LINKS, EMLINK);
        GIO_TO_SYS_ERR (G_IO_ERROR_NO_SPACE, ENOSPC);
        GIO_TO_SYS_ERR (G_IO_ERROR_INVALID_ARGUMENT, EINVAL);
        GIO_TO_SYS_ERR (G_IO_ERROR_PERMISSION_DENIED, EACCES);
        GIO_TO_SYS_ERR (G_IO_ERROR_NOT_SUPPORTED, EOPNOTSUPP);
        GIO_TO_SYS_ERR (G_IO_ERROR_NOT_MOUNTED, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_ALREADY_MOUNTED, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_CLOSED, EINVAL);
        GIO_TO_SYS_ERR (G_IO_ERROR_CANCELLED, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_PENDING, EAGAIN);
        GIO_TO_SYS_ERR (G_IO_ERROR_READ_ONLY, EPERM);
        GIO_TO_SYS_ERR (G_IO_ERROR_CANT_CREATE_BACKUP, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_WRONG_ETAG, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_TIMED_OUT, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_WOULD_RECURSE, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_BUSY, EBUSY);
        GIO_TO_SYS_ERR (G_IO_ERROR_WOULD_BLOCK, EWOULDBLOCK);
        GIO_TO_SYS_ERR (G_IO_ERROR_HOST_NOT_FOUND, ENOENT);
        GIO_TO_SYS_ERR (G_IO_ERROR_WOULD_MERGE, EIO);
        GIO_TO_SYS_ERR (G_IO_ERROR_FAILED_HANDLED, EIO);
        default: 
            errno = EIO;
            break;
        };
    }
            
    /* When errors on open we look cancelled */
    if(op == VFS_OP_OPENING)
        ah->state = VFS_ASYNC_CANCELLED;

    return FALSE;
}

/* Called when a file open completes */
static void
vfs_data_open_done (GObject *source, GAsyncResult *res, gpointer callback_data)
{
    VfsAsyncHandle* ah = (VfsAsyncHandle*)callback_data;

    if(ah->state == VFS_ASYNC_PROCESSING) {
        g_assert (ah->operation == VFS_OP_OPENING);
	g_clear_error (&ah->error);
	
        if (ah->writer)
            ah->ostream = G_OUTPUT_STREAM (g_file_replace_finish (ah->file, res, &ah->error));
        else
            ah->istream = G_INPUT_STREAM (g_file_read_finish (ah->file, res, &ah->error));
    
        ah->state = VFS_ASYNC_READY;
        ah->total = 0;
        ah->last = 0;
    }
}

/* Open the given handle */
static void
vfs_data_open_helper (VfsAsyncHandle *ah)
{
    g_assert (ah->file != NULL);
    g_assert (ah->state == VFS_ASYNC_READY);

    ah->state = VFS_ASYNC_PROCESSING;
    ah->operation = VFS_OP_OPENING;

    /* Note that we always overwrite the file */
    if (ah->writer)
        g_file_replace_async (ah->file, NULL, FALSE, G_FILE_CREATE_NONE, G_PRIORITY_DEFAULT, ah->cancellable, vfs_data_open_done, ah);
    else
        g_file_read_async (ah->file, G_PRIORITY_DEFAULT, ah->cancellable, vfs_data_open_done, ah);
}

/* Open the given URI */
static VfsAsyncHandle* 
vfs_data_open (GFile *file, gboolean write, gboolean delayed)
{
    VfsAsyncHandle* ah;

    /* We only support delayed opening of write files */
    g_assert (write || !delayed);
    
    ah = g_new0 (VfsAsyncHandle, 1);
    ah->cancellable = g_cancellable_new ();
    ah->state = VFS_ASYNC_READY;
    ah->operation = VFS_OP_NONE;
    ah->file = file;
    ah->writer = write;
    g_object_ref (file);
    
    /* Open the file right here and now if requested */
    if (!delayed)
        vfs_data_open_helper (ah);
    
    return ah;
}

/* Called when a read completes */
static void
vfs_data_read_done (GObject *source, GAsyncResult *res, gpointer callback_data) 
{
    VfsAsyncHandle* ah = (VfsAsyncHandle*)callback_data;
	
    if (ah->state == VFS_ASYNC_PROCESSING) {
        g_assert (ah->operation == VFS_OP_READING);
	g_clear_error (&ah->error);

        ah->processed = g_input_stream_read_finish (ah->istream, res, &ah->error);
        ah->state = VFS_ASYNC_READY;
        ah->total += ah->processed;
        
        /* Call progress callback if setup */
        if (ah->progcb && ah->total >= ah->last + PROGRESS_BLOCK)
            (ah->progcb) (ah->gdata, ah->total, ah->userdata);
    }
}    
    
/* Called by gpgme to read data */
static ssize_t
vfs_data_read (void *handle, void *buffer, size_t size)
{
    VfsAsyncHandle* ah = (VfsAsyncHandle*)handle;
    ssize_t sz = 0;
    
    /* Just in case we have an operation, like open */
    if (!vfs_data_wait_results(ah, TRUE))
        return -1;
        
    g_assert (ah->state == VFS_ASYNC_READY);
    
    /* Start async operation */
    ah->buffer = buffer;
    ah->state = VFS_ASYNC_PROCESSING;
    ah->operation = VFS_OP_READING;
    g_input_stream_read_async (ah->istream, buffer, size, G_PRIORITY_DEFAULT, ah->cancellable, vfs_data_read_done, ah);
    
    /* Wait for it */
    if (!vfs_data_wait_results (ah, TRUE))
        return -1;
    
    /* Return results */
    sz = (ssize_t)ah->processed;
    ah->state = VFS_ASYNC_READY;
    
    ah->buffer = NULL;
    ah->processed = 0;
    
    return sz;
}

/* Called when a write completes */
static void
vfs_data_write_done (GObject *source, GAsyncResult *res, gpointer callback_data) 
{
    VfsAsyncHandle* ah = (VfsAsyncHandle*)callback_data;

    if(ah->state == VFS_ASYNC_PROCESSING) {
        g_assert (ah->operation == VFS_OP_WRITING);
	g_clear_error (&ah->error);
    
        ah->processed = g_output_stream_write_finish (ah->ostream, res, &ah->error);
        ah->state = VFS_ASYNC_READY;
        ah->total += ah->processed;
        
        /* Call progress callback if setup */
        if (ah->progcb && ah->total >= ah->last + PROGRESS_BLOCK)
            (ah->progcb) (ah->gdata, ah->total, ah->userdata);
    }
}    

/* Called when a flush completes */
static void
vfs_data_flush_done (GObject *source, GAsyncResult *res, gpointer callback_data) 
{
    VfsAsyncHandle* ah = (VfsAsyncHandle*)callback_data;

    if(ah->state == VFS_ASYNC_PROCESSING) {
        g_assert (ah->operation == VFS_OP_FLUSHING);
	g_clear_error (&ah->error);
    
        g_output_stream_flush_finish (ah->ostream, res, &ah->error);
        ah->state = VFS_ASYNC_READY;
    }
}

/* Called by gpgme to write data */
static ssize_t
vfs_data_write (void *handle, const void *buffer, size_t size)
{
    VfsAsyncHandle* ah = (VfsAsyncHandle*)handle;
    ssize_t sz = 0;

    /* If the file isn't open yet, then do that now */
    if (!ah->ostream && ah->state == VFS_ASYNC_READY)
        vfs_data_open_helper (ah);
    
    /* Just in case we have an operation, like open */
    if (!vfs_data_wait_results(ah, TRUE))
        return -1;
           
    g_assert (ah->state == VFS_ASYNC_READY);
    
    /* Start async operation */
    ah->buffer = (gpointer)buffer;
    ah->state = VFS_ASYNC_PROCESSING;
    ah->operation = VFS_OP_WRITING;
    g_output_stream_write_async (ah->ostream, buffer, size, G_PRIORITY_DEFAULT, ah->cancellable, vfs_data_write_done, ah);
    
    /* Wait for it */
    if (!vfs_data_wait_results(ah, TRUE))
        return -1;

    /* Return results */
    sz = (ssize_t)ah->processed;
    
    ah->buffer = NULL;
    ah->processed = 0;

    /* Sadly GPGME doesn't support errors on close, so we have to flush after each write */
    ah->state = VFS_ASYNC_PROCESSING;
    ah->operation = VFS_OP_FLUSHING;
    g_output_stream_flush_async (ah->ostream, G_PRIORITY_DEFAULT, ah->cancellable, vfs_data_flush_done, ah);

    /* Wait for it */
    if (!vfs_data_wait_results(ah, TRUE))
        return -1;

    return sz;
}

/* Called from gpgme to seek a file */
static off_t
vfs_data_seek (void *handle, off_t offset, int whence)
{
    VfsAsyncHandle* ah = (VfsAsyncHandle*)handle;
    GSeekable *seekable = NULL;
    GSeekType wh;
    
    /* If the file isn't open yet, then do that now */
    if (!ah->ostream && !ah->istream && ah->state == VFS_ASYNC_READY)
        vfs_data_open_helper (ah);
        
    /* Just in case we have an operation, like open */
    if (!vfs_data_wait_results(ah, TRUE))
        return (off_t)-1;
           
    g_assert (ah->state == VFS_ASYNC_READY);
    
    if (ah->writer && G_IS_SEEKABLE (ah->ostream))
        seekable = G_SEEKABLE (ah->ostream);
    else
        seekable = G_SEEKABLE (ah->istream);
    
    if (!seekable || !g_seekable_can_seek (seekable)) {
        errno = ENOTSUP;
        return -1;
    }
    
    switch(whence)
    {
    case SEEK_SET:
        wh = G_SEEK_SET;
        break;
    case SEEK_CUR:
        wh = G_SEEK_CUR;
        break;
    case SEEK_END:
        wh = G_SEEK_END;
        break;
    default:
        g_assert_not_reached();
        break;
    }
    
    /* All seek operations are not async */
    g_clear_error (&ah->error);
    g_seekable_seek (seekable, (goffset)offset, wh, ah->cancellable, &ah->error);
    
    /* Start async operation */
    ah->state = VFS_ASYNC_READY;
    if (!vfs_data_wait_results (ah, TRUE))
        return -1;
    
    /* Return results */
    ah->state = VFS_ASYNC_READY;
    return offset;
}

/* Dummy callback for closing a file */
static void
vfs_data_close_done (GObject *source, GAsyncResult *res, gpointer callback_data) 
{
    /* 
     * GPGME doesn't have a way to return errors from close. So we mitigate this 
     * by always flushing data above. This isn't foolproof, but seems like the 
     * best we can do.
     */
}

/* Called to cancel the current operation.
 * Puts the async handle into a cancelled state. */
static void 
vfs_data_cancel (VfsAsyncHandle *ah)
{
    switch (ah->state) {
    case VFS_ASYNC_CANCELLED:
        break;

    case VFS_ASYNC_PROCESSING:
        g_cancellable_cancel (ah->cancellable);
        break;
        
    case VFS_ASYNC_READY:
        break;
    }
    
    if (ah->ostream)
        g_output_stream_close_async (ah->ostream, G_PRIORITY_DEFAULT, NULL, vfs_data_close_done, ah);
    else if (ah->istream)
        g_input_stream_close_async (ah->istream, G_PRIORITY_DEFAULT, NULL, vfs_data_close_done, ah);
    else 
	return;
    
    vfs_data_wait_results (ah, FALSE);
    
    ah->state = VFS_ASYNC_CANCELLED;
}

/* Called by gpgme to close a file */
static void
vfs_data_release (void *handle)
{
    VfsAsyncHandle* ah = (VfsAsyncHandle*)handle;

    vfs_data_cancel (ah);
    
    if (ah->file)
        g_object_unref (ah->file);
    ah->file = NULL;
    
    if (ah->istream)
        g_object_unref (ah->istream);
    ah->istream = NULL;
    
    if (ah->ostream)
        g_object_unref (ah->ostream);
    ah->ostream = NULL;
    
    if (ah->cancellable)
        g_object_unref (ah->cancellable);
    ah->cancellable = NULL;

    g_clear_error (&ah->error);
    
    g_free (ah);
}

/* GPGME vfs file operations */
static struct gpgme_data_cbs vfs_data_cbs = {
    vfs_data_read,
    vfs_data_write,
    vfs_data_seek,
    vfs_data_release
};

/* -----------------------------------------------------------------------------
 */

/* Create a data on the given uri, remote uris get gnome-vfs backends,
 * local uris get normal file access. */
static gpgme_data_t
create_vfs_data (GFile *file, guint mode, SeahorseVfsProgressCb progcb,
                 gpointer userdata, gpg_error_t* err) 
{
    gpgme_error_t gerr;
    gpgme_data_t ret = NULL;
    VfsAsyncHandle* handle = NULL;
    
    if (!err)
        err = &gerr;
    
    handle = vfs_data_open (file, mode & SEAHORSE_VFS_WRITE, 
                                  mode & SEAHORSE_VFS_DELAY);
    if (handle) {
        
        *err = gpgme_data_new_from_cbs (&ret, &vfs_data_cbs, handle);
        if (!GPG_IS_OK (*err)) {
            vfs_data_cbs.release (handle);
            ret = NULL;
        }
        
        handle->progcb = progcb;
        handle->userdata = userdata;
        handle->gdata = ret;
    }

    return ret;
}


gpgme_data_t 
seahorse_vfs_data_create (const gchar *uri, guint mode, GError **err)
{
    GFile *file = g_file_new_for_uri (uri);
    gpgme_data_t data;
    
    data = seahorse_vfs_data_create_full (file, mode, NULL, NULL, err);
    g_object_unref (file);
    
    return data;
}

gpgme_data_t 
seahorse_vfs_data_create_full (GFile *file, guint mode, SeahorseVfsProgressCb progcb,
                               gpointer userdata, GError **err)
{
    gpgme_data_t data;
    gpgme_error_t gerr;
    
    g_return_val_if_fail (!err || !*err, NULL);
    data = create_vfs_data (file, mode, progcb, userdata, &gerr);
    if (!data)
        seahorse_util_gpgme_to_error (gerr, err);
    return data;
}

gboolean
seahorse_vfs_set_file_contents (const gchar *uri, const gchar *text, gint len, 
                                GError **err)
{
    gpgme_data_t data;
    gboolean ret;
    
    data = seahorse_vfs_data_create (uri, SEAHORSE_VFS_WRITE, err);
    if (!data)
        return FALSE;
    
    ret = seahorse_vfs_data_write_all (data, (void*)text, len, err);
    
    gpgme_data_release (data);
    return ret;
}

gboolean 
seahorse_vfs_data_write_all (gpgme_data_t data, const void* buffer, gint len, GError **err)
{
    guchar *text = (guchar*)buffer;
    gint written;
    
    if (len < 0)
        len = strlen ((gchar*)text);
    
    while (len > 0) {
        written = gpgme_data_write (data, (void*)text, len);
        if (written < 0) {
            if (errno == EAGAIN || errno == EINTR)
                continue;
            g_set_error (err, G_FILE_ERROR, g_file_error_from_errno (errno), 
                         "%s", strerror (errno));
            return FALSE;
        }
        
        len -= written;
        text += written;
    }
    
    return TRUE;
}

/* -----------------------------------------------------------------------------
 * MULTIPLE CONCATED FILES 
 */

/* Called by gpgme to read data */
static ssize_t
multi_data_read (void *handle, void *buffer, size_t size)
{
    gpgme_data_t data = NULL;
    ssize_t sz = 0;
    GList *datas, *l;
    guchar *buf;

    datas = (GList*)handle;
    buf = (guchar*)buffer;
    
    /* Find first non-null data thingy */
    for (l = datas; l; l = g_list_next (l)) {
        data = (gpgme_data_t)l->data;
        if (data)
            break;
    }
    
    while (size > 0 && data) {
        sz = gpgme_data_read (data, buf, size);
        
        /* An error */
        if (sz < 0)
            return sz;
        
        if (sz > size)
            sz = size;
        
        /* Free current data, get next one */
        if (sz < size) {
            l->data = NULL;
            gpgmex_data_release (data);
            l = g_list_next (l);
            data = (gpgme_data_t)(l ? l->data : NULL);
        }
        
        /* Read later in buffer */
        size -= sz;
        buf += sz;
    }
    
    return sz;
}

/* Called by gpgme to write data */
static ssize_t
multi_data_write (void *handle, const void *buffer, size_t size)
{
    /* No writing */
    errno = EBADF;
    return -1;
}

/* Called from gpgme to seek a file */
static off_t
multi_data_seek(void *handle, off_t offset, int whence)
{
    /* No real seeking */
    return offset;
}

/* Called by gpgme to close a file */
static void
multi_data_release(void *handle)
{
    GList *datas = (GList*)handle;
    GList *l;

    for (l = datas; l; l = g_list_next (l))
        gpgmex_data_release ((gpgme_data_t)l->data);
    g_list_free (datas);
}

static struct gpgme_data_cbs multi_data_cbs = 
{
    multi_data_read,
    multi_data_write,
    multi_data_seek,
    multi_data_release
};

gpgme_data_t
seahorse_vfs_data_read_multi (const gchar **uris, GError **err)
{
    gpgme_error_t gerr;
    gpgme_data_t data;
    GList *datas = NULL;
    const gchar **u;
    
    for (u = uris; *u; u++) {
        if(!(*u)[0])
            continue;
        data = seahorse_vfs_data_create (*u, SEAHORSE_VFS_READ, err);
        if (!data)
            break;
        datas = g_list_prepend (datas, data);
    }
    
    /* Failed somewhere */
    if (!data) {
        multi_data_release (datas);
        return NULL;
    }
    
    datas = g_list_reverse (datas);
    gerr = gpgme_data_new_from_cbs (&data, &multi_data_cbs, datas);
    if (!GPG_IS_OK (gerr)) {
        seahorse_util_gpgme_to_error (gerr, err);
        multi_data_release (datas);
        return NULL;
    }

    return data;
}
