/*
 * Copyright (C) 2010 Martin Robinson <mrobinson@webkit.org>
 * Copyright (C) Igalia S.L.
 * Copyright (C) 2013 Collabora Ltd.
 * All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */
#include "config.h"
#include "PasteboardHelper.h"

#include "DataObjectGtk.h"
#include "GRefPtrGtk.h"
#include "GtkVersioning.h"
#include "Pasteboard.h"
#include "TextResourceDecoder.h"
#include <gtk/gtk.h>
#include <wtf/gobject/GUniquePtr.h>
#include <wtf/text/CString.h>

namespace WebCore {

static GdkAtom textPlainAtom;
static GdkAtom markupAtom;
static GdkAtom netscapeURLAtom;
static GdkAtom uriListAtom;
static GdkAtom smartPasteAtom;
static GdkAtom unknownAtom;

static String gMarkupPrefix;

static void removeMarkupPrefix(String& markup)
{
    // The markup prefix is not harmful, but we remove it from the string anyway, so that
    // we can have consistent results with other ports during the layout tests.
    if (markup.startsWith(gMarkupPrefix))
        markup.remove(0, gMarkupPrefix.length());
}

static void initGdkAtoms()
{
    static gboolean initialized = FALSE;

    if (initialized)
        return;

    initialized = TRUE;

    textPlainAtom = gdk_atom_intern("text/plain;charset=utf-8", FALSE);
    markupAtom = gdk_atom_intern("text/html", FALSE);
    netscapeURLAtom = gdk_atom_intern("_NETSCAPE_URL", FALSE);
    uriListAtom = gdk_atom_intern("text/uri-list", FALSE);
    smartPasteAtom = gdk_atom_intern("application/vnd.webkitgtk.smartpaste", FALSE);
    unknownAtom = gdk_atom_intern("application/vnd.webkitgtk.unknown", FALSE);
    gMarkupPrefix = "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">";
}

PasteboardHelper* PasteboardHelper::defaultPasteboardHelper()
{
    DEPRECATED_DEFINE_STATIC_LOCAL(PasteboardHelper, defaultHelper, ());
    return &defaultHelper;
}

PasteboardHelper::PasteboardHelper()
    : m_targetList(gtk_target_list_new(0, 0))
{
    initGdkAtoms();

    gtk_target_list_add_text_targets(m_targetList, PasteboardHelper::TargetTypeText);
    gtk_target_list_add(m_targetList, markupAtom, 0, PasteboardHelper::TargetTypeMarkup);
    gtk_target_list_add_uri_targets(m_targetList, PasteboardHelper::TargetTypeURIList);
    gtk_target_list_add(m_targetList, netscapeURLAtom, 0, PasteboardHelper::TargetTypeNetscapeURL);
    gtk_target_list_add_image_targets(m_targetList, PasteboardHelper::TargetTypeImage, TRUE);
    gtk_target_list_add(m_targetList, unknownAtom, 0, PasteboardHelper::TargetTypeUnknown);
}

PasteboardHelper::~PasteboardHelper()
{
    gtk_target_list_unref(m_targetList);
}

GtkTargetList* PasteboardHelper::targetList() const
{
    return m_targetList;
}

static String selectionDataToUTF8String(GtkSelectionData* data)
{
    // g_strndup guards against selection data that is not null-terminated.
    GUniquePtr<gchar> markupString(g_strndup(reinterpret_cast<const char*>(gtk_selection_data_get_data(data)), gtk_selection_data_get_length(data)));
    return String::fromUTF8(markupString.get());
}

void PasteboardHelper::getClipboardContents(GtkClipboard* clipboard)
{
    DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
    ASSERT(dataObject);

    if (gtk_clipboard_wait_is_text_available(clipboard)) {
        GUniquePtr<gchar> textData(gtk_clipboard_wait_for_text(clipboard));
        if (textData)
            dataObject->setText(String::fromUTF8(textData.get()));
    }

    if (gtk_clipboard_wait_is_target_available(clipboard, markupAtom)) {
        if (GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, markupAtom)) {
            String markup(selectionDataToUTF8String(data));
            removeMarkupPrefix(markup);
            dataObject->setMarkup(markup);
            gtk_selection_data_free(data);
        }
    }

    if (gtk_clipboard_wait_is_target_available(clipboard, uriListAtom)) {
        if (GtkSelectionData* data = gtk_clipboard_wait_for_contents(clipboard, uriListAtom)) {
            dataObject->setURIList(selectionDataToUTF8String(data));
            gtk_selection_data_free(data);
        }
    }
}

void PasteboardHelper::fillSelectionData(GtkSelectionData* selectionData, guint info, DataObjectGtk* dataObject)
{
    if (info == TargetTypeText)
        gtk_selection_data_set_text(selectionData, dataObject->text().utf8().data(), -1);

    else if (info == TargetTypeMarkup) {
        // Some Linux applications refuse to accept pasted markup unless it is
        // prefixed by a content-type meta tag.
        CString markup = String(gMarkupPrefix + dataObject->markup()).utf8();
        gtk_selection_data_set(selectionData, markupAtom, 8,
            reinterpret_cast<const guchar*>(markup.data()), markup.length());

    } else if (info == TargetTypeURIList) {
        CString uriList = dataObject->uriList().utf8();
        gtk_selection_data_set(selectionData, uriListAtom, 8,
            reinterpret_cast<const guchar*>(uriList.data()), uriList.length());

    } else if (info == TargetTypeNetscapeURL && dataObject->hasURL()) {
        String url(dataObject->url());
        String result(url);
        result.append("\n");

        if (dataObject->hasText())
            result.append(dataObject->text());
        else
            result.append(url);

        GUniquePtr<gchar> resultData(g_strdup(result.utf8().data()));
        gtk_selection_data_set(selectionData, netscapeURLAtom, 8,
            reinterpret_cast<const guchar*>(resultData.get()), strlen(resultData.get()));

    } else if (info == TargetTypeImage)
        gtk_selection_data_set_pixbuf(selectionData, dataObject->image());

    else if (info == TargetTypeSmartPaste)
        gtk_selection_data_set_text(selectionData, "", -1);

    else if (info == TargetTypeUnknown) {
        GVariantBuilder builder;
        g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY);

        auto types = dataObject->unknownTypes();
        auto end = types.end();
        for (auto it = types.begin(); it != end; ++it) {
            GUniquePtr<gchar> dictItem(g_strdup_printf("{'%s', '%s'}", it->key.utf8().data(), it->value.utf8().data()));
            g_variant_builder_add_parsed(&builder, dictItem.get());
        }

        GRefPtr<GVariant> variant = g_variant_builder_end(&builder);
        GUniquePtr<gchar> serializedVariant(g_variant_print(variant.get(), TRUE));
        gtk_selection_data_set(selectionData, unknownAtom, 1, reinterpret_cast<const guchar*>(serializedVariant.get()), strlen(serializedVariant.get()));
    }
}

GtkTargetList* PasteboardHelper::targetListForDataObject(DataObjectGtk* dataObject, SmartPasteInclusion shouldInludeSmartPaste)
{
    GtkTargetList* list = gtk_target_list_new(0, 0);

    if (dataObject->hasText())
        gtk_target_list_add_text_targets(list, TargetTypeText);

    if (dataObject->hasMarkup())
        gtk_target_list_add(list, markupAtom, 0, TargetTypeMarkup);

    if (dataObject->hasURIList()) {
        gtk_target_list_add_uri_targets(list, TargetTypeURIList);
        gtk_target_list_add(list, netscapeURLAtom, 0, TargetTypeNetscapeURL);
    }

    if (dataObject->hasImage())
        gtk_target_list_add_image_targets(list, TargetTypeImage, TRUE);

    if (dataObject->hasUnknownTypeData())
        gtk_target_list_add(list, unknownAtom, 0, TargetTypeUnknown);

    if (shouldInludeSmartPaste == IncludeSmartPaste)
        gtk_target_list_add(list, smartPasteAtom, 0, TargetTypeSmartPaste);

    return list;
}

void PasteboardHelper::fillDataObjectFromDropData(GtkSelectionData* data, guint /* info */, DataObjectGtk* dataObject)
{
    if (!gtk_selection_data_get_data(data))
        return;

    GdkAtom target = gtk_selection_data_get_target(data);
    if (target == textPlainAtom)
        dataObject->setText(selectionDataToUTF8String(data));
    else if (target == markupAtom) {
        String markup(selectionDataToUTF8String(data));
        removeMarkupPrefix(markup);
        dataObject->setMarkup(markup);
    } else if (target == uriListAtom) {
        dataObject->setURIList(selectionDataToUTF8String(data));
    } else if (target == netscapeURLAtom) {
        String urlWithLabel(selectionDataToUTF8String(data));
        Vector<String> pieces;
        urlWithLabel.split('\n', pieces);

        // Give preference to text/uri-list here, as it can hold more
        // than one URI but still take  the label if there is one.
        if (!dataObject->hasURIList())
            dataObject->setURIList(pieces[0]);
        if (pieces.size() > 1)
            dataObject->setText(pieces[1]);
    } else if (target == unknownAtom) {
        GRefPtr<GVariant> variant = g_variant_new_parsed(reinterpret_cast<const char*>(gtk_selection_data_get_data(data)));

        GUniqueOutPtr<gchar> key;
        GUniqueOutPtr<gchar> value;
        GVariantIter iter;

        g_variant_iter_init(&iter, variant.get());
        while (g_variant_iter_next(&iter, "{ss}", &key.outPtr(), &value.outPtr()))
            dataObject->setUnknownTypeData(key.get(), value.get());
    }
}

Vector<GdkAtom> PasteboardHelper::dropAtomsForContext(GtkWidget* widget, GdkDragContext* context)
{
    // Always search for these common atoms.
    Vector<GdkAtom> dropAtoms;
    dropAtoms.append(textPlainAtom);
    dropAtoms.append(markupAtom);
    dropAtoms.append(uriListAtom);
    dropAtoms.append(netscapeURLAtom);
    dropAtoms.append(unknownAtom);

    // For images, try to find the most applicable image type.
    GRefPtr<GtkTargetList> list = adoptGRef(gtk_target_list_new(0, 0));
    gtk_target_list_add_image_targets(list.get(), TargetTypeImage, TRUE);
    GdkAtom atom = gtk_drag_dest_find_target(widget, context, list.get());
    if (atom != GDK_NONE)
        dropAtoms.append(atom);

    return dropAtoms;
}

static DataObjectGtk* settingClipboardDataObject = 0;

static void getClipboardContentsCallback(GtkClipboard* clipboard, GtkSelectionData *selectionData, guint info, gpointer)
{
    DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
    ASSERT(dataObject);
    PasteboardHelper::defaultPasteboardHelper()->fillSelectionData(selectionData, info, dataObject);
}

static void clearClipboardContentsCallback(GtkClipboard* clipboard, gpointer data)
{
    DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
    ASSERT(dataObject);

    // Only clear the DataObject for this clipboard if we are not currently setting it.
    if (dataObject != settingClipboardDataObject)
        dataObject->clearAll();

    if (!data)
        return;

    GRefPtr<GClosure> callback = adoptGRef(static_cast<GClosure*>(data));
    GValue firstArgument = {0, {{0}}};
    g_value_init(&firstArgument, G_TYPE_POINTER);
    g_value_set_pointer(&firstArgument, clipboard);
    g_closure_invoke(callback.get(), nullptr, 1, &firstArgument, 0);
}

void PasteboardHelper::writeClipboardContents(GtkClipboard* clipboard, SmartPasteInclusion includeSmartPaste, GClosure* callback)
{
    DataObjectGtk* dataObject = DataObjectGtk::forClipboard(clipboard);
    GtkTargetList* list = targetListForDataObject(dataObject, includeSmartPaste);

    int numberOfTargets;
    GtkTargetEntry* table = gtk_target_table_new_from_list(list, &numberOfTargets);

    if (numberOfTargets > 0 && table) {
        settingClipboardDataObject = dataObject;

        if (gtk_clipboard_set_with_data(clipboard, table, numberOfTargets, getClipboardContentsCallback, clearClipboardContentsCallback, callback ? g_closure_ref(callback) : nullptr))
            gtk_clipboard_set_can_store(clipboard, nullptr, 0);
        else {
            // When gtk_clipboard_set_with_data fails the callbacks are ignored, so we need to release the reference we were passing to clearClipboardContentsCallback.
            if (callback)
                g_closure_unref(callback);
        }

        settingClipboardDataObject = nullptr;

    } else
        gtk_clipboard_clear(clipboard);

    if (table)
        gtk_target_table_free(table, numberOfTargets);
    gtk_target_list_unref(list);
}

bool PasteboardHelper::clipboardContentSupportsSmartReplace(GtkClipboard* clipboard)
{
    return gtk_clipboard_wait_is_target_available(clipboard, smartPasteAtom);
}

}

