/*
 * Copyright (C) 2012 Red Hat, Inc.
 * Copyright © 2025 The Files contributors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * Author: Peter Eisenmann <p3732@getgoogleoff.me>
 */
#define G_LOG_DOMAIN "nautilus-search"

#include <config.h>
#include "nautilus-search-provider.h"

#include "nautilus-enum-types.h"
#include "nautilus-query.h"

#include <gio/gio.h>
#include <glib-object.h>

typedef struct
{
    const char *name;
    guint delayed_timeout_id;

    GPtrArray *hits;

    GMutex idle_submit_mutex;
    guint submit_on_idle_id;
    GPtrArray *hits_to_submit;

    /* Thread-safe variables */
    GCancellable *cancellable;
    NautilusQuery *query;
} NautilusSearchProviderPrivate;

G_DEFINE_TYPE_WITH_PRIVATE (NautilusSearchProvider, nautilus_search_provider, G_TYPE_OBJECT)

enum
{
    HITS_ADDED,
    FINISHED,
    LAST_SIGNAL
};

static guint signals[LAST_SIGNAL];

static void
setup_signals (void)
{
    /**
     * NautilusSearchProvider::hits-added:
     * @provider: the provider that found search hits
     * @hits: (transfer full): #GPtrArray of #NautilusSearchHit
     *
     * This signal is emitted when the provider has search hits.
     */
    signals[HITS_ADDED] = g_signal_new ("hits-added",
                                        NAUTILUS_TYPE_SEARCH_PROVIDER,
                                        G_SIGNAL_RUN_LAST, 0,
                                        NULL, NULL,
                                        g_cclosure_marshal_VOID__POINTER,
                                        G_TYPE_NONE, 1,
                                        G_TYPE_POINTER);

    /**
     * NautilusSearchProvider::provider-finished:
     * @provider: the provider that finished searching
     *
     * This signal is emitted when the provider finishes searching.
     */
    signals[FINISHED] = g_signal_new ("provider-finished", NAUTILUS_TYPE_SEARCH_PROVIDER,
                                      G_SIGNAL_RUN_LAST, 0,
                                      NULL, NULL,
                                      g_cclosure_marshal_VOID__VOID,
                                      G_TYPE_NONE, 0);
}

static const char *
search_provider_name (NautilusSearchProvider *self)
{
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    if (G_UNLIKELY (priv->name == NULL))
    {
        NautilusSearchProviderClass *klass = NAUTILUS_SEARCH_PROVIDER_CLASS (G_OBJECT_GET_CLASS (self));
        priv->name = klass->get_name (self);
    }

    return priv->name;
}

static void
actual_start (NautilusSearchProvider *self)
{
    NautilusSearchProviderClass *klass = NAUTILUS_SEARCH_PROVIDER_CLASS (G_OBJECT_GET_CLASS (self));
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    priv->delayed_timeout_id = 0;
    priv->cancellable = g_cancellable_new ();
    priv->hits = g_ptr_array_new_with_free_func (g_object_unref);
    /* Keep reference on self while running */
    g_object_ref (self);

    g_debug ("Search provider '%s' starting", search_provider_name (self));
    klass->start_search (self);
}

gboolean
nautilus_search_provider_start (NautilusSearchProvider *self,
                                NautilusQuery          *query)
{
    g_return_val_if_fail (NAUTILUS_IS_SEARCH_PROVIDER (self), FALSE);
    g_return_val_if_fail (NAUTILUS_IS_QUERY (query), FALSE);

    NautilusSearchProviderClass *klass = NAUTILUS_SEARCH_PROVIDER_CLASS (G_OBJECT_GET_CLASS (self));
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    /* Can't start provider again before it finished */
    g_return_val_if_fail (priv->cancellable == NULL, FALSE);

    if (!klass->should_search (self, query))
    {
        return FALSE;
    }

    g_set_object (&priv->query, query);

    guint delay_ms = klass->search_delay (self);
    if (delay_ms > 0)
    {
        g_debug ("Search provider '%s' delayed", search_provider_name (self));
        priv->delayed_timeout_id = g_timeout_add_once (delay_ms,
                                                       (GSourceOnceFunc) actual_start,
                                                       self);
    }
    else
    {
        actual_start (self);
    }

    return TRUE;
}

void
nautilus_search_provider_stop (NautilusSearchProvider *self)
{
    g_return_if_fail (NAUTILUS_IS_SEARCH_PROVIDER (self));

    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    if (priv->delayed_timeout_id != 0)
    {
        g_debug ("Search provider '%s' cancelled before starting", search_provider_name (self));
        g_clear_handle_id (&priv->delayed_timeout_id, g_source_remove);
        g_clear_object (&priv->query);
        g_signal_emit (self, signals[FINISHED], 0);
    }
    else if (nautilus_search_provider_should_stop (self))
    {
        return;
    }
    else
    {
        NautilusSearchProviderClass *klass = NAUTILUS_SEARCH_PROVIDER_CLASS (G_OBJECT_GET_CLASS (self));

        g_debug ("Search provider '%s' stopping", search_provider_name (self));
        g_cancellable_cancel (priv->cancellable);

        klass->stop_search (self);
    }
}

static void
search_provider_submit_hits (NautilusSearchProvider *self,
                             GPtrArray              *hits)
{
    if (nautilus_search_provider_should_stop (self) ||
        hits == NULL ||
        hits->len == 0)
    {
        g_clear_pointer (&hits, g_ptr_array_unref);
        return;
    }

    g_debug ("Search provider '%s' found %d hits", search_provider_name (self), hits->len);
    g_signal_emit (self, signals[HITS_ADDED], 0, g_steal_pointer (&hits));
}

static void
search_provider_submit_on_idle (NautilusSearchProvider *self)
{
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);
    GPtrArray *hits;

    g_mutex_lock (&priv->idle_submit_mutex);
    priv->submit_on_idle_id = 0;
    hits = g_steal_pointer (&priv->hits_to_submit);
    g_mutex_unlock (&priv->idle_submit_mutex);

    search_provider_submit_hits (self, hits);
}

void
nautilus_search_provider_finished (NautilusSearchProvider *self)
{
    g_return_if_fail (NAUTILUS_IS_SEARCH_PROVIDER (self));

    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);
    GPtrArray *ready_hits;

    g_mutex_lock (&priv->idle_submit_mutex);
    g_clear_handle_id (&priv->submit_on_idle_id, g_source_remove);
    ready_hits = g_steal_pointer (&priv->hits_to_submit);
    g_mutex_unlock (&priv->idle_submit_mutex);

    search_provider_submit_hits (self, ready_hits);
    search_provider_submit_hits (self, g_steal_pointer (&priv->hits));

    g_clear_object (&priv->cancellable);
    g_clear_object (&priv->query);

    g_debug ("Search provider '%s' finished", search_provider_name (self));

    /* Drop self-reference, counterpart to start() */
    g_object_unref (self);

    g_signal_emit (self, signals[FINISHED], 0);
}

/**
 * Protected methods, generic type for convenience.
 * These functions may be called outside the main context.
 */

gboolean
nautilus_search_provider_should_stop (gpointer self)
{
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    return priv->cancellable == NULL || g_cancellable_is_cancelled (priv->cancellable);
}

GCancellable *
nautilus_search_provider_get_cancellable (gpointer self)
{
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    return priv->cancellable;
}

NautilusQuery *
nautilus_search_provider_get_query (gpointer self)
{
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    return priv->query;
}

void
nautilus_search_provider_add_hit (gpointer           self,
                                  NautilusSearchHit *hit)
{
    const guint BATCH_LIMIT = 100;

    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    g_ptr_array_add (priv->hits, hit);

    if (priv->hits->len >= BATCH_LIMIT)
    {
        nautilus_search_provider_flush_hits (self);
    }
}

void
nautilus_search_provider_flush_hits (gpointer self)
{
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    if (nautilus_search_provider_should_stop (self) ||
        priv->hits->len == 0)
    {
        /* Nothing to do */
        return;
    }

    NautilusSearchProviderClass *klass = (NautilusSearchProviderClass *) G_OBJECT_GET_CLASS (self);

    if (klass->run_in_thread (self))
    {
        G_MUTEX_AUTO_LOCK (&priv->idle_submit_mutex, locker);

        if (priv->submit_on_idle_id != 0)
        {
            /* Last batch is still pending, don't schedule another */
            return;
        }
        /* Schedule submit from main context */
        priv->hits_to_submit = g_steal_pointer (&priv->hits);
        priv->submit_on_idle_id = g_idle_add_once ((GSourceOnceFunc) search_provider_submit_on_idle,
                                                   self);
    }
    else
    {
        search_provider_submit_hits (self, g_steal_pointer (&priv->hits));
    }

    priv->hits = g_ptr_array_new_with_free_func (g_object_unref);
}

/* End protected methods */

static void
nautilus_search_provider_init (NautilusSearchProvider *self)
{
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    g_mutex_init (&priv->idle_submit_mutex);
}

static void
search_provider_dispose (GObject *object)
{
    NautilusSearchProvider *self = NAUTILUS_SEARCH_PROVIDER (object);
    NautilusSearchProviderPrivate *priv = nautilus_search_provider_get_instance_private (self);

    g_warn_if_fail (priv->submit_on_idle_id == 0);

    g_mutex_clear (&priv->idle_submit_mutex);
    g_clear_object (&priv->cancellable);
    g_clear_object (&priv->query);
    g_clear_pointer (&priv->hits, g_ptr_array_unref);
    g_clear_handle_id (&priv->delayed_timeout_id, g_source_remove);

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

static gboolean
default_should_search (NautilusSearchProvider *provider,
                       NautilusQuery          *query)
{
    return TRUE;
}

static gboolean
default_run_in_thread (NautilusSearchProvider *provider)
{
    return FALSE;
}

static guint
default_search_delay (NautilusSearchProvider *provider)
{
    return 0;
}

static void
default_stop_search (NautilusSearchProvider *provider)
{
}

static void
nautilus_search_provider_class_init (NautilusSearchProviderClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);
    NautilusSearchProviderClass *search_provider_class = NAUTILUS_SEARCH_PROVIDER_CLASS (klass);

    object_class->dispose = search_provider_dispose;
    search_provider_class->run_in_thread = default_run_in_thread;
    search_provider_class->should_search = default_should_search;
    search_provider_class->search_delay = default_search_delay;
    search_provider_class->stop_search = default_stop_search;

    setup_signals ();
}
