/*
 * Copyright (C) 2013, 2020 Igalia S.L.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "cmakeconfig.h"
#include "BrowserSearchBox.h"

struct _BrowserSearchBox {
    GtkBox parent;

    WebKitWebView *webView;
    GtkWidget *entry;
    GtkWidget *prevButton;
    GtkWidget *nextButton;
    GActionGroup *actionGroup;
    GMenu *optionsMenu;
    GtkWidget *optionsPopover;
    GtkWidget *caseCheckButton;
    GtkWidget *begginigWordCheckButton;
    GtkWidget *capitalAsBegginigWordCheckButton;
};

G_DEFINE_TYPE(BrowserSearchBox, browser_search_box, GTK_TYPE_BOX)

static void setFailedStyleForEntry(BrowserSearchBox *searchBox, gboolean failedSearch)
{
    if (failedSearch)
        gtk_style_context_add_class(gtk_widget_get_style_context(searchBox->entry), "search-failed");
    else
        gtk_style_context_remove_class(gtk_widget_get_style_context(searchBox->entry), "search-failed");
}

static void doSearch(BrowserSearchBox *searchBox)
{
    GtkEntry *entry = GTK_ENTRY(searchBox->entry);
    if (!gtk_entry_get_text_length(entry)) {
        setFailedStyleForEntry(searchBox, FALSE);
        gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_SECONDARY, NULL);
        webkit_find_controller_search_finish(webkit_web_view_get_find_controller(searchBox->webView));
        return;
    }

    if (!gtk_entry_get_icon_name(entry, GTK_ENTRY_ICON_SECONDARY))
        gtk_entry_set_icon_from_icon_name(entry, GTK_ENTRY_ICON_SECONDARY, "edit-clear-symbolic");

    WebKitFindOptions options = WEBKIT_FIND_OPTIONS_WRAP_AROUND;
    GAction *action = g_action_map_lookup_action(G_ACTION_MAP(searchBox->actionGroup), "case-sensitive");
    GVariant *state = g_action_get_state(action);
    if (!g_variant_get_boolean(state))
        options |= WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE;
    g_variant_unref(state);

    action = g_action_map_lookup_action(G_ACTION_MAP(searchBox->actionGroup), "beginning-word");
    state = g_action_get_state(action);
    if (g_variant_get_boolean(state))
        options |= WEBKIT_FIND_OPTIONS_AT_WORD_STARTS;
    g_variant_unref(state);

    action = g_action_map_lookup_action(G_ACTION_MAP(searchBox->actionGroup), "capital-as-beginning-word");
    state = g_action_get_state(action);
    if (g_variant_get_boolean(state))
        options |= WEBKIT_FIND_OPTIONS_TREAT_MEDIAL_CAPITAL_AS_WORD_START;
    g_variant_unref(state);

#if GTK_CHECK_VERSION(3, 98, 5)
    const gchar *text = gtk_editable_get_text(GTK_EDITABLE(entry));
#else
    const gchar *text = gtk_entry_get_text(entry);
#endif
    webkit_find_controller_search(webkit_web_view_get_find_controller(searchBox->webView), text, options, G_MAXUINT);
}

static void searchNext(BrowserSearchBox *searchBox)
{
    webkit_find_controller_search_next(webkit_web_view_get_find_controller(searchBox->webView));
}

static void searchPrevious(BrowserSearchBox *searchBox)
{
    webkit_find_controller_search_previous(webkit_web_view_get_find_controller(searchBox->webView));
}

static void searchEntryMenuIconPressedCallback(BrowserSearchBox *searchBox, GtkEntryIconPosition iconPosition, GdkEvent *event)
{
    if (iconPosition != GTK_ENTRY_ICON_PRIMARY)
        return;

#if GTK_CHECK_VERSION(3, 98, 5)
    GtkWidget *popover = gtk_popover_menu_new_from_model(G_MENU_MODEL(searchBox->optionsMenu));
    gtk_widget_set_parent(popover, GTK_WIDGET(searchBox));
#else
    GtkWidget *popover = gtk_popover_menu_new();
    gtk_popover_bind_model(GTK_POPOVER(popover), G_MENU_MODEL(searchBox->optionsMenu), NULL);
    gtk_popover_set_relative_to(GTK_POPOVER(popover), searchBox->entry);
#endif
    searchBox->optionsPopover = popover;
    GdkRectangle rect;
    gtk_entry_get_icon_area(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, &rect);
    gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
    gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
    gtk_popover_popup(GTK_POPOVER(popover));
}

static void searchEntryClearIconReleasedCallback(BrowserSearchBox *searchBox, GtkEntryIconPosition iconPosition)
{
    if (iconPosition != GTK_ENTRY_ICON_SECONDARY)
        return;

#if GTK_CHECK_VERSION(3, 98, 5)
    gtk_editable_set_text(GTK_EDITABLE(searchBox->entry), "");
#else
    gtk_entry_set_text(GTK_ENTRY(searchBox->entry), "");
#endif
}

static void searchEntryChangedCallback(GtkEntry *entry, BrowserSearchBox *searchBox)
{
    doSearch(searchBox);
}

static void searchEntryActivatedCallback(BrowserSearchBox *searchBox)
{
    searchNext(searchBox);
}

static void searchPreviousButtonCallback(GSimpleAction *action, GVariant *parameter, gpointer userData)
{
    searchPrevious(BROWSER_SEARCH_BOX(userData));
}

static void searchNextButtonCallback(GSimpleAction *action, GVariant *parameter, gpointer userData)
{
    searchNext(BROWSER_SEARCH_BOX(userData));
}

static void searchMenuCheckButtonToggledCallback(GSimpleAction *action, GVariant *state, gpointer userData)
{
    g_simple_action_set_state(action, state);
    doSearch(BROWSER_SEARCH_BOX(userData));
}

static void findControllerFailedToFindTextCallback(BrowserSearchBox *searchBox)
{
    setFailedStyleForEntry(searchBox, TRUE);
}

static void findControllerFoundTextCallback(BrowserSearchBox *searchBox)
{
    setFailedStyleForEntry(searchBox, FALSE);
}

static const GActionEntry actions[] = {
    { "next", searchNextButtonCallback, NULL, NULL, NULL, { 0 } },
    { "previous", searchPreviousButtonCallback, NULL, NULL, NULL, { 0 } },
    { "case-sensitive", NULL, NULL, "false", searchMenuCheckButtonToggledCallback, { 0 } },
    { "beginning-word", NULL, NULL, "false", searchMenuCheckButtonToggledCallback, { 0 } },
    { "capital-as-beginning-word", NULL, NULL, "false", searchMenuCheckButtonToggledCallback, { 0 } }
};

static void browser_search_box_init(BrowserSearchBox *searchBox)
{
    GSimpleActionGroup *actionGroup = g_simple_action_group_new();
    searchBox->actionGroup = G_ACTION_GROUP(actionGroup);
    g_action_map_add_action_entries(G_ACTION_MAP(actionGroup), actions, G_N_ELEMENTS(actions), searchBox);
    gtk_widget_insert_action_group(GTK_WIDGET(searchBox), "find", G_ACTION_GROUP(actionGroup));

    gtk_orientable_set_orientation(GTK_ORIENTABLE(searchBox), GTK_ORIENTATION_HORIZONTAL);
#if GTK_CHECK_VERSION(3, 98, 5)
    gtk_widget_add_css_class(GTK_WIDGET(searchBox), "linked");
    gtk_widget_add_css_class(GTK_WIDGET(searchBox), "raised");
#else
    GtkStyleContext *styleContext = gtk_widget_get_style_context(GTK_WIDGET(searchBox));
    gtk_style_context_add_class(styleContext, GTK_STYLE_CLASS_LINKED);
    gtk_style_context_add_class(styleContext, GTK_STYLE_CLASS_RAISED);
#endif

    searchBox->entry = gtk_entry_new();
    gtk_widget_set_halign(searchBox->entry, GTK_ALIGN_FILL);
    gtk_widget_set_hexpand(searchBox->entry, TRUE);
    gtk_entry_set_placeholder_text(GTK_ENTRY(searchBox->entry), "Search");
    gtk_entry_set_icon_from_icon_name(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, "edit-find-symbolic");
    gtk_entry_set_icon_activatable(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, TRUE);
    gtk_entry_set_icon_sensitive(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, TRUE);
    gtk_entry_set_icon_tooltip_text(GTK_ENTRY(searchBox->entry), GTK_ENTRY_ICON_PRIMARY, "Search options");
#if GTK_CHECK_VERSION(3, 98, 5)
    gtk_box_append(GTK_BOX(searchBox), searchBox->entry);
#else
    gtk_container_add(GTK_CONTAINER(searchBox), searchBox->entry);
    gtk_widget_show(searchBox->entry);
#endif

    GtkCssProvider *cssProvider = gtk_css_provider_new();
#if GTK_CHECK_VERSION(3, 98, 5)
    gtk_css_provider_load_from_data(cssProvider, ".search-failed { background-color: #ff6666; }", -1);
#else
    gtk_css_provider_load_from_data(cssProvider, ".search-failed { background-color: #ff6666; }", -1, NULL);
#endif
    gtk_style_context_add_provider(gtk_widget_get_style_context(searchBox->entry), GTK_STYLE_PROVIDER(cssProvider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    g_object_unref(cssProvider);

#if GTK_CHECK_VERSION(3, 98, 5)
    searchBox->prevButton = gtk_button_new_from_icon_name("go-up-symbolic");
#else
    searchBox->prevButton = gtk_button_new_from_icon_name("go-up-symbolic", GTK_ICON_SIZE_MENU);
#endif
    GtkButton *button = GTK_BUTTON(searchBox->prevButton);
    gtk_actionable_set_action_name(GTK_ACTIONABLE(button), "find.previous");
    gtk_widget_set_focus_on_click(searchBox->prevButton, FALSE);
#if GTK_CHECK_VERSION(3, 98, 5)
    gtk_box_append(GTK_BOX(searchBox), searchBox->prevButton);
#else
    gtk_box_pack_start(GTK_BOX(searchBox), searchBox->prevButton, FALSE, FALSE, 0);
    gtk_widget_show(searchBox->prevButton);
#endif

#if GTK_CHECK_VERSION(3, 98, 5)
    searchBox->nextButton = gtk_button_new_from_icon_name("go-down-symbolic");
#else
    searchBox->nextButton = gtk_button_new_from_icon_name("go-down-symbolic", GTK_ICON_SIZE_MENU);
#endif
    button = GTK_BUTTON(searchBox->nextButton);
    gtk_actionable_set_action_name(GTK_ACTIONABLE(button), "find.next");
    gtk_widget_set_focus_on_click(searchBox->nextButton, FALSE);
#if GTK_CHECK_VERSION(3, 98, 5)
    gtk_box_append(GTK_BOX(searchBox), searchBox->nextButton);
#else
    gtk_box_pack_start(GTK_BOX(searchBox), searchBox->nextButton, FALSE, FALSE, 0);
    gtk_widget_show(searchBox->nextButton);
#endif

    searchBox->optionsMenu = g_menu_new();
    g_menu_append(searchBox->optionsMenu, "Ca_se sensitive", "find.case-sensitive");
    g_menu_append(searchBox->optionsMenu, "Only at the _beginning of words", "find.beginning-word");
    g_menu_append(searchBox->optionsMenu, "Capital _always as beginning of word", "find.capital-as-beginning-word");

    g_signal_connect_swapped(searchBox->entry, "icon-press", G_CALLBACK(searchEntryMenuIconPressedCallback), searchBox);
    g_signal_connect_swapped(searchBox->entry, "icon-release", G_CALLBACK(searchEntryClearIconReleasedCallback), searchBox);
    g_signal_connect_after(searchBox->entry, "changed", G_CALLBACK(searchEntryChangedCallback), searchBox);
    g_signal_connect_swapped(searchBox->entry, "activate", G_CALLBACK(searchEntryActivatedCallback), searchBox);
}

static void browserSearchBoxFinalize(GObject *gObject)
{
    BrowserSearchBox *searchBox = BROWSER_SEARCH_BOX(gObject);

    if (searchBox->webView) {
        g_object_unref(searchBox->webView);
        searchBox->webView = NULL;
    }

    if (searchBox->optionsMenu) {
        g_object_unref(searchBox->optionsMenu);
        searchBox->optionsMenu = NULL;
    }

    g_object_unref(searchBox->actionGroup);

    G_OBJECT_CLASS(browser_search_box_parent_class)->finalize(gObject);
}

static void browserSearchBoxDispose(GObject *object)
{
#if GTK_CHECK_VERSION(3, 98, 5)
    BrowserSearchBox *searchBox = BROWSER_SEARCH_BOX(object);
    g_clear_pointer(&searchBox->optionsPopover, gtk_widget_unparent);
#endif

    G_OBJECT_CLASS(browser_search_box_parent_class)->dispose(object);
}

#if GTK_CHECK_VERSION(3, 98, 5)
static void browserSearchBoxSizeAllocate(GtkWidget *widget, int width, int height, int baseline)
{
    GTK_WIDGET_CLASS(browser_search_box_parent_class)->size_allocate(widget, width, height, baseline);

    BrowserSearchBox *searchBox = BROWSER_SEARCH_BOX(widget);
    if (searchBox->optionsPopover)
        gtk_popover_present(GTK_POPOVER(searchBox->optionsPopover));
}
#endif

static void browser_search_box_class_init(BrowserSearchBoxClass *klass)
{
    GObjectClass *gObjectClass = G_OBJECT_CLASS(klass);
    gObjectClass->finalize = browserSearchBoxFinalize;
    gObjectClass->dispose = browserSearchBoxDispose;

#if GTK_CHECK_VERSION(3, 98, 5)
    GtkWidgetClass *widgetClass = GTK_WIDGET_CLASS(klass);
    widgetClass->size_allocate = browserSearchBoxSizeAllocate;
#endif
}

GtkWidget *browser_search_box_new(WebKitWebView *webView)
{
    g_return_val_if_fail(WEBKIT_IS_WEB_VIEW(webView), NULL);

    GtkWidget *searchBox = GTK_WIDGET(g_object_new(BROWSER_TYPE_SEARCH_BOX, NULL));
    BROWSER_SEARCH_BOX(searchBox)->webView = g_object_ref(webView);

    WebKitFindController *controller = webkit_web_view_get_find_controller(webView);
    g_signal_connect_swapped(controller, "failed-to-find-text", G_CALLBACK(findControllerFailedToFindTextCallback), searchBox);
    g_signal_connect_swapped(controller, "found-text", G_CALLBACK(findControllerFoundTextCallback), searchBox);

    return searchBox;
}

GtkEntry *browser_search_box_get_entry(BrowserSearchBox *searchBox)
{
    g_return_val_if_fail(BROWSER_IS_SEARCH_BOX(searchBox), NULL);

    return GTK_ENTRY(searchBox->entry);
}
