#include "com_sun_glass_ui_gtk_GtkSystemClipboard.h"
#include "glass_gtkcompat.h"
#include "glass_general.h"

#include <gtk/gtk.h>
#include <string.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

static GdkAtom MIME_TEXT_PLAIN_TARGET;

static GdkAtom MIME_TEXT_URI_LIST_TARGET;

static GdkAtom MIME_JAVA_IMAGE;

static GdkAtom FILES_TARGET;
static GdkAtom MIME_FILES_TARGET;

static void init_atoms()
{
    static int initialized = 0;

    if (!initialized) {
        MIME_TEXT_PLAIN_TARGET = gdk_atom_intern_static_string("text/plain");

        MIME_TEXT_URI_LIST_TARGET = gdk_atom_intern_static_string("text/uri-list");

        MIME_JAVA_IMAGE = gdk_atom_intern_static_string("application/x-java-rawimage");

        FILES_TARGET = gdk_atom_intern_static_string("FILE_NAME");
        MIME_FILES_TARGET = gdk_atom_intern_static_string("application/x-java-file-list");
        initialized = 1;
    }
}


static GtkClipboard* clipboard = NULL;

static GtkClipboard *get_clipboard() {
    if (clipboard == NULL) {
        clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
    }
    return clipboard;
}

static void add_target_from_jstring(JNIEnv *env, GtkTargetList *list, jstring string)
{
    const char *gstring = (*env)->GetStringUTFChars(env, string, NULL);
    if (g_strcmp0(gstring, "text/plain") == 0) {
        gtk_target_list_add_text_targets(list, 0);
    } else if (g_strcmp0(gstring, "application/x-java-rawimage") == 0) {
        gtk_target_list_add_image_targets(list, 0, TRUE);
        return;
    } else if (g_strcmp0(gstring, "application/x-java-file-list") == 0) {
        gtk_target_list_add(list, MIME_TEXT_URI_LIST_TARGET, 0, 0);
        return; // do not add application/x-java-file-list
    }

    gtk_target_list_add(list, gdk_atom_intern(gstring, FALSE), 0, 0);
    (*env)->ReleaseStringUTFChars(env, string, gstring);

}

static void data_to_targets(JNIEnv *env, jobject data, GtkTargetEntry **targets, gint *ntargets)
{
    jobject keys;
    jobject keysIterator;
    jstring next;

    GtkTargetList *list = gtk_target_list_new(NULL, 0);

    keys = (*env)->CallObjectMethod(env, data, jMapKeySet, NULL);
    CHECK_JNI_EXCEPTION(env)
    keysIterator = (*env)->CallObjectMethod(env, keys, jIterableIterator, NULL);
    CHECK_JNI_EXCEPTION(env)

    while ((*env)->CallBooleanMethod(env, keysIterator, jIteratorHasNext) == JNI_TRUE) {
        next = (*env)->CallObjectMethod(env, keysIterator, jIteratorNext, NULL);
        add_target_from_jstring(env, list, next);
    }
    *targets = gtk_target_table_new_from_list(list, ntargets);
    gtk_target_list_unref(list);

}

static void set_text_data(GtkSelectionData *selection_data, jstring data)
{
    const char *text_data = (*mainEnv)->GetStringUTFChars(mainEnv, data, NULL);
    guint ntext_data = strlen(text_data);

    gtk_selection_data_set_text(selection_data, text_data, ntext_data);
    (*mainEnv)->ReleaseStringUTFChars(mainEnv, data, text_data);
}

static void set_jstring_data(GtkSelectionData *selection_data, GdkAtom target, jstring data)
{
    const char *text_data = (*mainEnv)->GetStringUTFChars(mainEnv, data, NULL);
    guint ntext_data = strlen(text_data);

    //XXX is target == type ??
    gtk_selection_data_set(selection_data, target, 8, (const guchar *)text_data, ntext_data);
    (*mainEnv)->ReleaseStringUTFChars(mainEnv, data, text_data);
}

static void set_bytebuffer_data(GtkSelectionData *selection_data, GdkAtom target, jobject data)
{
    jbyteArray byteArray = (*mainEnv)->CallObjectMethod(mainEnv, data, byteBufferArray);
    jbyte* raw = (*mainEnv)->GetByteArrayElements(mainEnv, byteArray, NULL);
    jsize nraw = (*mainEnv)->GetArrayLength(mainEnv, byteArray);

    //XXX is target == type ??
    gtk_selection_data_set(selection_data, target, 8, (guchar *)raw, (gint)nraw);

    (*mainEnv)->ReleaseByteArrayElements(mainEnv, byteArray, raw, JNI_ABORT);
}

#define FILE_PREFIX "file://"
#define FILE_PREFIX_N 7

static void set_file_uri_data(GtkSelectionData *selection_data, jobjectArray data)
{
    jsize ndata = (*mainEnv)->GetArrayLength(mainEnv, data);
    jsize i;
    jsize string_size;
    jstring string;
    gchar **uris = g_malloc0_n(ndata + 1, sizeof(gchar*));
    for (i = 0; i < ndata; ++i) {
        string = (*mainEnv)->GetObjectArrayElement(mainEnv, data, i);
        string_size = (*mainEnv)->GetStringUTFLength(mainEnv, string);
        uris[i] = g_malloc0(string_size + FILE_PREFIX_N + 1);
        g_strlcpy(uris[i], FILE_PREFIX, FILE_PREFIX_N + 1);
        (*mainEnv)->GetStringUTFRegion(mainEnv, string, 0, string_size, (char*) uris[i] + FILE_PREFIX_N);
    }

    gtk_selection_data_set_uris(selection_data, uris);

    for (i = 0; i < ndata; ++i) {
        g_free(uris[i]);
    }
    g_free(uris);

}

static void set_image_data(GtkSelectionData *selection_data, jobject pixels)
{
    GdkPixbuf *pixbuf;

    (*mainEnv)->CallVoidMethod(mainEnv, pixels, jPixelsAttachData, PTR_TO_JLONG(&pixbuf));
    CHECK_JNI_EXCEPTION(mainEnv);
    gtk_selection_data_set_pixbuf(selection_data, pixbuf);

    g_object_unref(pixbuf);
}

static void set_data(GdkAtom target, GtkSelectionData *selection_data, jobject data)
{
    gchar *name = gdk_atom_name(target);
    jstring typeString;
    jobject result;

    if (gtk_targets_include_text(&target, 1)) {
        typeString = (*mainEnv)->NewStringUTF(mainEnv, "text/plain");
        result = (*mainEnv)->CallObjectMethod(mainEnv, data, jMapGet, typeString, NULL);
        if (result != NULL)
            set_text_data(selection_data, (jstring)result);
    } else if (gtk_targets_include_image(&target, 1, TRUE)) {
        typeString = (*mainEnv)->NewStringUTF(mainEnv, "application/x-java-rawimage");
        result = (*mainEnv)->CallObjectMethod(mainEnv, data, jMapGet, typeString, NULL);
        if (result != NULL) {
            set_image_data(selection_data, result);
            CHECK_JNI_EXCEPTION(mainEnv);
        }
    } else if (target == MIME_TEXT_URI_LIST_TARGET) {
        typeString = (*mainEnv)->NewStringUTF(mainEnv, name);
        if ((*mainEnv)->CallBooleanMethod(mainEnv, data, jMapContainsKey, typeString, NULL)) {
            result = (*mainEnv)->CallObjectMethod(mainEnv, data, jMapGet, typeString, NULL);
            if (result != NULL)
                set_jstring_data(selection_data, target, (jstring)result);
        } else {
            typeString = (*mainEnv)->NewStringUTF(mainEnv, "application/x-java-file-list");
            result = (*mainEnv)->CallObjectMethod(mainEnv, data, jMapGet, typeString, NULL);
            if (result != NULL)
                set_file_uri_data(selection_data, (jobjectArray) result);
        }
    } else {
        typeString = (*mainEnv)->NewStringUTF(mainEnv, name);
        result = (*mainEnv)->CallObjectMethod(mainEnv, data, jMapGet, typeString, NULL);
        if (result != NULL) {
            if ((*mainEnv)->IsInstanceOf(mainEnv, result, string)) {
                set_jstring_data(selection_data, target, (jstring)result);
            } else if ((*mainEnv)->IsInstanceOf(mainEnv, result, byteBuffer)) {
                set_bytebuffer_data(selection_data, target, result);
            }
        }
    }

    g_free(name);
}

static void set_data_func(GtkClipboard *clipboard, GtkSelectionData *selection_data,
        guint info, gpointer user_data)
{
    jobject data = user_data; //HashMap
    GdkAtom target;
    target = gtk_selection_data_get_target(selection_data);

    set_data(target, selection_data, data);
    LOG_EXCEPTION(mainEnv);
    (*mainEnv)->ExceptionClear(mainEnv);
}

static void clear_data_func(GtkClipboard *clipboard, gpointer user_data)
{
    jobject data = user_data;
    (*mainEnv)->DeleteGlobalRef(mainEnv, data);
}

static jobject get_data_text(JNIEnv *env)
{
    gchar *data = gtk_clipboard_wait_for_text(get_clipboard());
    if (data == NULL) {
        return NULL;
    }
    jstring jdata = (*env)->NewStringUTF(env, data);
    g_free(data);
    return jdata;
}

static jobject get_data_file_list(JNIEnv *env)
{
    jobjectArray result = NULL;
    guint i,size;
    jstring str;
    gchar *path;
    gchar **uris;

    uris = gtk_clipboard_wait_for_uris(get_clipboard());
    if (uris == NULL) {
        return NULL;
    }
    size = g_strv_length(uris);
    result = (*env)->NewObjectArray(env, size, string, NULL);

    for (i = 0; i < size; ++i) {
        path = uris[i];
        if (g_str_has_prefix(path, "file://")) {
            path += 7;
        }
        str = (*env)->NewStringUTF(env, path);
        (*env)->SetObjectArrayElement(env, result, i, str);
    }

    g_strfreev(uris);
    return result;
}

static jobject get_data_image(JNIEnv* env) {
    GdkPixbuf* pixbuf;
    guchar *data;
    jbyteArray data_array;
    jobject buffer, result;
    int w,h,stride;

    pixbuf = gtk_clipboard_wait_for_image(get_clipboard());
    if (pixbuf == NULL) {
        return NULL;
    }

    if (!gdk_pixbuf_get_has_alpha(pixbuf)) {
        GdkPixbuf *tmp_buf = gdk_pixbuf_add_alpha(pixbuf, FALSE, 0, 0, 0);
        g_object_unref(pixbuf);
        pixbuf = tmp_buf;
    }
    w = gdk_pixbuf_get_width(pixbuf);
    h = gdk_pixbuf_get_height(pixbuf);
    stride = gdk_pixbuf_get_rowstride(pixbuf);

    data = gdk_pixbuf_get_pixels(pixbuf);

    //Actually, we are converting RGBA to BGRA, but that's the same operation
    data = (guchar*) convert_BGRA_to_RGBA((int*)data, stride, h);

    data_array = (*env)->NewByteArray(env, stride*h);
    (*env)->SetByteArrayRegion(env, data_array, 0, stride*h, (jbyte*)data);

    buffer = (*env)->CallStaticObjectMethod(env, byteBuffer, byteBufferWrap, data_array);
    result = (*env)->NewObject(env, gtk_pixels, jGtkPixelsInit, w, h, buffer);

    g_free(data);
    gdk_pixbuf_unref(pixbuf);

    return result;

}

static jobject get_data_raw(JNIEnv *env, const char* mime, gboolean string_data)
{
    GtkSelectionData *data;
    const guchar *raw_data;
    jsize length;
    jbyteArray array;
    jobject result = NULL;
    data = gtk_clipboard_wait_for_contents(get_clipboard(), gdk_atom_intern(mime, FALSE));
    if (data != NULL) {
        raw_data = glass_gtk_selection_data_get_data_with_length(data, &length);
        if (string_data) {
            result = (*env)->NewStringUTF(env, (const char*)raw_data);
        } else {
            array = (*env)->NewByteArray(env, length);
            (*env)->SetByteArrayRegion(env, array, 0, length, (const jbyte*)raw_data);
            result = (*env)->CallStaticObjectMethod(env, byteBuffer, byteBufferWrap, array);
        }
        gtk_selection_data_free(data);
    }
    return result;
}

/*
 * Class:     com_sun_glass_ui_gtk_GtkSystemClipboard
 * Method:    isOwner
 * Signature: ()Z
 */
JNIEXPORT jboolean JNICALL Java_com_sun_glass_ui_gtk_GtkSystemClipboard_isOwner
  (JNIEnv *env, jobject obj)
{
    return JNI_FALSE; //There's no straightforward way how to check for this in GTK.
                      // Returning false is safe however, as it just affects the case when the same application,
                      // that has clipboard selection, checks for clipboard content. When this is false,
                      // we have to use native X11 calls in this case.
}

/*
 * Class:     com_sun_glass_ui_gtk_GtkSystemClipboard
 * Method:    pushToSystem
 * Signature: (Ljava/util/HashMap;I)V
 */
JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkSystemClipboard_pushToSystem
  (JNIEnv * env, jobject obj, jobject data, jint supported)
{
    GtkTargetEntry* targets = NULL;
    gint ntargets;
    data = (*env)->NewGlobalRef(env, data);
    init_atoms();
    data_to_targets(env, data, &targets, &ntargets);
    CHECK_JNI_EXCEPTION(env)
    gtk_clipboard_set_with_data(get_clipboard(), targets, ntargets, set_data_func, clear_data_func, data);
    gtk_target_table_free(targets, ntargets);
}

/*
 * Class:     com_sun_glass_ui_gtk_GtkSystemClipboard
 * Method:    pushTargetActionToSystem
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkSystemClipboard_pushTargetActionToSystem
  (JNIEnv * env, jobject obj, jint action)
{
    //Not used for clipboard. DnD only
}

/*
 * Class:     com_sun_glass_ui_gtk_GtkSystemClipboard
 * Method:    popFromSystem
 * Signature: (Ljava/lang/String;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_com_sun_glass_ui_gtk_GtkSystemClipboard_popFromSystem
  (JNIEnv * env, jobject obj, jstring mime)
{
    const char* cmime = (*env)->GetStringUTFChars(env, mime, NULL);
    jobject result;

    init_atoms();
    if (g_strcmp0(cmime, "text/plain") == 0) {
        result = get_data_text(env);
    } else if (g_str_has_prefix(cmime, "text/")) {
        result = get_data_raw(env, cmime, TRUE);
    } else if (g_strcmp0(cmime, "application/x-java-file-list") == 0) {
        result = get_data_file_list(env);
    } else if (g_strcmp0(cmime, "application/x-java-rawimage") == 0 ) {
        result = get_data_image(env);
    } else {
        result = get_data_raw(env, cmime, FALSE);
    }

    (*env)->ReleaseStringUTFChars(env, mime, cmime);

    return result;
}

/*
 * Class:     com_sun_glass_ui_gtk_GtkSystemClipboard
 * Method:    supportedSourceActionsFromSystem
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_com_sun_glass_ui_gtk_GtkSystemClipboard_supportedSourceActionsFromSystem
  (JNIEnv *env, jobject obj)
{
    //Not used for clipboard. DnD only
    return 0;
}

/*
 * Class:     com_sun_glass_ui_gtk_GtkSystemClipboard
 * Method:    mimesFromSystem
 * Signature: ()[Ljava/lang/String;
 */
JNIEXPORT jobjectArray JNICALL Java_com_sun_glass_ui_gtk_GtkSystemClipboard_mimesFromSystem
  (JNIEnv * env, jobject obj)
{
    GdkAtom *targets;
    gint ntargets;
    gint i;
    GdkAtom *convertible;
    GdkAtom *convertible_ptr;
    gchar *name;
    jclass resultClass = (*env)->FindClass(env, "Ljava/lang/String;");
    jobjectArray result;
    jstring tmpString;

    init_atoms();

    gtk_clipboard_wait_for_targets(get_clipboard(), &targets, &ntargets);
    convertible = g_malloc_n(ntargets * 2, sizeof(GdkAtom)); //theoretically, the number can double
    convertible_ptr = convertible;
    for (i = 0; i < ntargets; ++i) {
        //handle text targets
        //if (targets[i] == TEXT_TARGET || targets[i] == STRING_TARGET || targets[i] == UTF8_STRING_TARGET) {
        if (gtk_targets_include_text(targets + i, 1)) {
            *(convertible_ptr++) = MIME_TEXT_PLAIN_TARGET;
        } else if (gtk_targets_include_image(targets + i, 1, TRUE)) {
            *(convertible_ptr++) = MIME_JAVA_IMAGE;
        }
        //TODO text/x-moz-url ?

        //XXX: URI targets are not necessary file targets, can we add the type anyway?
        if (targets[i] == MIME_TEXT_URI_LIST_TARGET) {
            *(convertible_ptr++) = MIME_FILES_TARGET;
        }

        name = gdk_atom_name(targets[i]);
        *(convertible_ptr++) = targets[i];
        g_free(name);
    }

    result = (*env)->NewObjectArray(env, convertible_ptr - convertible, resultClass, NULL);
    for (i = 0; convertible + i < convertible_ptr; ++i) {
        name = gdk_atom_name(convertible[i]);
        tmpString = (*env)->NewStringUTF(env, name);
        (*env)->SetObjectArrayElement(env, result, (jsize)i, tmpString);
        g_free(name);
    }

    g_free(targets);
    g_free(convertible);
    return result;
}
