/*
 * 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * See the COPYING file for license information.
 *
 * Guillaume Chazarain <guichaz@gmail.com>
 */

/************************************************
 * IPC between gliv processes to reuse a window *
 ************************************************/

#include <unistd.h>             /* unlink(), close(), write() */
#include <sys/types.h>          /* struct sockaddr */
#include <sys/socket.h>         /* socket(), ... */
#include <sys/un.h>             /* AF_LOCAL */
#include <string.h>             /* strncpy() */
#include <stdio.h>              /* perror(), fread(), stdin */
#include <sys/stat.h>

#include "gliv.h"
#include "ipc.h"
#include "files_list.h"
#include "next_image.h"

extern GlivImage *current_image;

/* The current server. */
static guint watch_id;
static GIOChannel *server = NULL;

/* Transform a file descriptor into a GIOChannel. */
static GIOChannel *make_channel(gint fd)
{
    GIOChannel *channel;
    GError *err = NULL;

    channel = g_io_channel_unix_new(fd);
    g_io_channel_set_close_on_unref(channel, TRUE);
    g_io_channel_set_encoding(channel, NULL, &err);
    if (err != NULL) {
        g_printerr("%s\n", err->message);
        g_error_free(err);
        g_io_channel_unref(channel);
        channel = NULL;
    }

    return channel;
}

/*** Where do we keep this socket? ***/

static gchar *get_socket_name(void)
{
    gchar *user_name, *repl, *res;

    repl = user_name = g_strdup(g_get_user_name());

    /* Do people have '/' in their user names? */
    while ((repl = strchr(repl, '/')) != NULL)
        *repl = '_';

    res = g_strdup_printf("%s/.19830128gliv_%s", g_get_tmp_dir(), user_name);
    g_free(user_name);

    return res;
}

/*** State encoding by the client for the server. ***/

static gboolean should_clear_list(gchar command)
{
    return command == 'c';
}

static gchar make_command(gboolean clear_list)
{
    return clear_list ? 'c' : 'C';
}

/*
 * We send the working directory in order to deal with relative filenames.
 */
static gboolean write_param(GIOChannel * channel, gboolean clear_list)
{
    gchar *current_dir;
    gint length;
    gboolean res = TRUE;
    gsize bytes = 0;
    GError *err = NULL;
    gchar command = make_command(clear_list);

    g_io_channel_write_chars(channel, &command, 1, &bytes, &err);
    if (bytes != 1) {
        if (err != NULL) {
            g_printerr("%s\n", err->message);
            g_error_free(err);
        }
        return FALSE;
    }

    current_dir = g_get_current_dir();
    length = strlen(current_dir) + 1;

    g_io_channel_write_chars(channel, current_dir, length, &bytes, &err);
    if (bytes != length) {
        res = FALSE;
        if (err != NULL) {
            g_printerr("%s\n", err->message);
            g_error_free(err);
        }
    }

    g_free(current_dir);

    return res;
}

/*
 * We read what the function above sent. If the server and the client are
 * running in the same directory we put NULL in *client_dir.
 */
static gboolean read_param(GIOChannel * channel, gchar ** client_dir,
                           gboolean * clear_list)
{
    size_t size = 0;
    gchar *server_dir;
    struct stat client_stat, server_stat;
    GError *err = NULL;
    gchar command;

    g_io_channel_read_chars(channel, &command, 1, &size, &err);
    if (size != 1) {
        if (err != NULL) {
            g_printerr("%s\n", err->message);
            g_error_free(err);
        }
        return FALSE;
    }
    *clear_list = should_clear_list(command);

    *client_dir = NULL;
    g_io_channel_read_line(channel, client_dir, &size, NULL, &err);
    if (err != NULL) {
        g_printerr("%s\n", err->message);
        g_error_free(err);
        return FALSE;
    }

    server_dir = g_get_current_dir();
    if (stat(*client_dir, &client_stat) < 0 ||
        stat(server_dir, &server_stat) < 0) {

        g_free(server_dir);
        g_free(*client_dir);
        return FALSE;
    }

    if (client_stat.st_dev == server_stat.st_dev &&
        client_stat.st_ino == server_stat.st_ino) {
        /*
         * The client and server are in the same directory, we can use relative
         * filenames without problems.
         */
        g_free(*client_dir);
        *client_dir = NULL;
    }

    g_free(server_dir);
    return TRUE;
}

/*** Server ***/

static gint create_server_socket(void)
{
    struct sockaddr_un name;
    gint sock;
    gchar *filename;
    size_t size;

    sock = socket(PF_LOCAL, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket");
        return -1;
    }

    name.sun_family = AF_LOCAL;

    filename = get_socket_name();
    strncpy(name.sun_path, filename, sizeof(name.sun_path) - 1);
    name.sun_path[sizeof(name.sun_path) - 1] = '\0';

    /* The latest launched gliv is the server. */
    unlink(filename);

    g_free(filename);
    size = SUN_LEN(&name);

    if (bind(sock, (struct sockaddr *) &name, size) < 0) {
        perror("bind");
        close(sock);
        sock = -1;
    } else if (listen(sock, 5) < 0) {
        perror("listen");
        close(sock);
        sock = -1;
    }

    return sock;
}

/* Put the '\0' separated strings in an array. */
static gchar **rebuild_args_array(gchar * data, gint length, gint * count,
                                  gchar * client_dir)
{
    gint i;
    gchar *last = data + length, *ptr = data;
    gchar **tab;

    /* How many strings? */
    *count = 0;
    while (ptr < last) {
        ptr += strlen(ptr) + 1;
        (*count)++;
    }

    /* Cut them. */
    tab = g_new(gchar *, *count);
    for (i = 0; i < *count; i++) {
        if (client_dir == NULL || data[0] == '/')
            tab[i] = g_strdup(data);
        else
            tab[i] = g_build_filename(client_dir, data, NULL);

        data += strlen(data) + 1;
    }

    return tab;
}

/* Add the filenames and open the next image. */
static void process_data(gchar * data, gint length, gchar * client_dir,
                         gboolean clear_list)
{
    gint i, count, nb_inserted;
    gchar **tab;
    GList *before_inserted = NULL;
    GList *after_inserted = NULL;

    tab = rebuild_args_array(data, length, &count, client_dir);

    if (clear_list) {
        before_inserted = current_image ? current_image->node : get_list_head();
        after_inserted = before_inserted ? before_inserted->next : NULL;
    }

    nb_inserted = insert_after_current(tab, count, TRUE, FALSE);
    while (before_inserted) {
        add_obsolete_node(before_inserted);
        before_inserted = before_inserted->prev;
    }

    while (after_inserted) {
        add_obsolete_node(after_inserted);
        after_inserted = after_inserted->next;
    }
    new_images(nb_inserted);

    for (i = 0; i < count; i++)
        g_free(tab[i]);

    g_free(tab);
}

/* Called when there is something to read. */
static gboolean handle_connect(GIOChannel * source, GIOCondition unused1,
                               gpointer unused2)
{
    gchar *data;
    gsize length = 0;
    gint fd;
    GError *err = NULL;
    GIOChannel *channel;
    gchar *client_dir;
    gboolean clear_list;

    fd = accept(g_io_channel_unix_get_fd(source), NULL, NULL);
    if (fd < 0) {
        perror("accept");
        return TRUE;
    }

    channel = make_channel(fd);
    if (channel == NULL)
        return TRUE;

    /* Fill the param. */
    if (!read_param(channel, &client_dir, &clear_list)) {
        g_io_channel_unref(channel);
        return TRUE;
    }

    /* Read the filenames. */
    g_io_channel_read_to_end(channel, &data, &length, &err);
    if (err != NULL) {
        g_printerr("%s\n", err->message);
        g_error_free(err);
        g_io_channel_unref(channel);
        return TRUE;
    }

    process_data(data, length, client_dir, clear_list);
    g_io_channel_unref(channel);
    return TRUE;
}

gboolean start_server(void)
{
    gint socket;
    GIOChannel *channel;

    socket = create_server_socket();
    if (socket < 0)
        return FALSE;

    channel = make_channel(socket);
    if (channel == NULL)
        return FALSE;

    /* Remove the previous wait. */
    if (server != NULL) {
        g_source_remove(watch_id);
        g_io_channel_unref(server);
    }

    /* Let GTK+ inform us about clients. */
    server = channel;
    watch_id = g_io_add_watch(channel, G_IO_IN, (GIOFunc) handle_connect, NULL);

    return FALSE;
}

/*** Client ***/

static gint create_client_socket(void)
{
    struct sockaddr_un name;
    gint sock;
    gchar *filename;
    size_t size;

    sock = socket(PF_LOCAL, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("socket");
        return -1;
    }

    name.sun_family = AF_LOCAL;

    filename = get_socket_name();
    strncpy(name.sun_path, filename, sizeof(name.sun_path) - 1);
    name.sun_path[sizeof(name.sun_path) - 1] = '\0';
    g_free(filename);

    size = SUN_LEN(&name);
    if (connect(sock, (struct sockaddr *) &name, size) < 0) {
        perror("connect");
        close(sock);
        sock = -1;
    }

    return sock;
}

gboolean connect_server(gboolean clear_list)
{
    gboolean ret = TRUE;
    gint fd;
    GIOChannel *channel;
    GList *list;

    fd = create_client_socket();
    if (fd < 0)
        return FALSE;

    channel = make_channel(fd);
    if (channel == NULL)
        goto out;

    /* Send our current directory. */
    if (!write_param(channel, clear_list)) {
        g_io_channel_unref(channel);
        ret = FALSE;
        goto out;
    }

    /* Send all the filenames with '\0' between them. */
    for (list = get_list_head(); list != NULL; list = list->next) {
        gsize size;
        gint length = strlen(list->data) + 1;
        GError *err = NULL;

        g_io_channel_write_chars(channel, list->data, length, &size, &err);
        if (err != NULL) {
            g_printerr("%s\n", err->message);
            g_error_free(err);
            g_io_channel_unref(channel);
            goto out;
        }
    }

    g_io_channel_unref(channel);

  out:
    return ret;
}
