/*
 * Copyright (C) 2005 Novell, Inc.
 * Copyright © 2025 The Files contributors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * Author: Anders Carlsson <andersca@imendio.com>
 * Author: Peter Eisenmann <p3732@getgoogleoff.me>
 */
#define G_LOG_DOMAIN "nautilus-search"

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

#include "nautilus-file-utilities.h"
#include "nautilus-query.h"
#include "nautilus-search-engine-model.h"
#include "nautilus-search-engine-localsearch.h"
#include "nautilus-search-engine-recent.h"
#include "nautilus-search-engine-simple.h"
#include "nautilus-search-hit.h"
#include "nautilus-search-provider.h"

#include <glib/gi18n.h>

struct _NautilusSearchEngine
{
    GObject parent_instance;

    NautilusSearchType search_type;

    NautilusSearchProvider *localsearch;
    NautilusSearchProvider *model;
    NautilusSearchProvider *recent;
    NautilusSearchProvider *simple;

    GHashTable *uris;
    guint providers_started;
    guint providers_finished;

    NautilusQuery *query;
    gboolean running;
    gboolean starting;
    gboolean restart;
};

enum
{
    PROP_0,
    PROP_RUNNING,
    PROP_SEARCH_TYPE,
    N_PROPERTIES
};
static GParamSpec *properties[N_PROPERTIES];

enum
{
    HITS_ADDED,
    SEARCH_FINISHED,
    LAST_SIGNAL
};
static guint signals[LAST_SIGNAL];

G_DEFINE_FINAL_TYPE (NautilusSearchEngine, nautilus_search_engine, G_TYPE_OBJECT)

static void
check_providers_status (NautilusSearchEngine *self);

static void
search_engine_start_provider (NautilusSearchProvider *provider,
                              NautilusSearchEngine   *self)
{
    if (provider == NULL)
    {
        return;
    }
    else if (nautilus_search_provider_start (provider, self->query))
    {
        self->providers_started++;
    }
}

static void
search_engine_start_real (NautilusSearchEngine *self)
{
    g_return_if_fail (self->running);

    self->providers_started = 0;
    self->providers_finished = 0;

    self->starting = TRUE;
    search_engine_start_provider (self->localsearch, self);
    search_engine_start_provider (self->model, self);
    search_engine_start_provider (self->recent, self);
    search_engine_start_provider (self->simple, self);
    self->starting = FALSE;

    /* Providers could already be finished */
    check_providers_status (self);
}

void
nautilus_search_engine_start (NautilusSearchEngine *self,
                              NautilusQuery        *query)
{
    g_return_if_fail (query != NULL);

    g_autoptr (NautilusQuery) query_to_copy = g_object_ref (query);

    g_clear_object (&self->query);
    self->query = nautilus_query_copy (query_to_copy);

    if (self->running)
    {
        self->restart = TRUE;
        return;
    }

    /* Keep reference on self while running */
    g_object_ref (self);
    self->running = TRUE;
    g_object_notify (G_OBJECT (self), "running");

    g_debug ("Search engine start");
    search_engine_start_real (self);
}

void
nautilus_search_engine_stop (NautilusSearchEngine *self)
{
    g_debug ("Search engine stop");

    if (self->localsearch != NULL)
    {
        nautilus_search_provider_stop (self->localsearch);
    }
    if (self->model != NULL)
    {
        nautilus_search_provider_stop (self->model);
    }
    if (self->recent != NULL)
    {
        nautilus_search_provider_stop (self->recent);
    }
    if (self->simple != NULL)
    {
        nautilus_search_provider_stop (self->simple);
    }

    self->restart = FALSE;
}

static void
search_provider_hits_added (NautilusSearchProvider *provider,
                            GPtrArray              *transferred_hits,
                            NautilusSearchEngine   *self)
{
    g_autoptr (GPtrArray) hits = transferred_hits;

    if (!self->running || self->restart)
    {
        g_debug ("Ignoring hits-added, since engine is %s",
                 !self->running ? "not running" : "waiting to restart");
        return;
    }

    g_autoptr (GPtrArray) added = g_ptr_array_new_with_free_func (g_object_unref);
    for (guint i = 0; i < hits->len; i++)
    {
        NautilusSearchHit *hit = hits->pdata[i];
        const char *uri = nautilus_search_hit_get_uri (hit);

        if (!g_hash_table_contains (self->uris, uri))
        {
            g_hash_table_add (self->uris, g_strdup (uri));
            g_ptr_array_add (added, g_object_ref (hit));
        }
    }

    if (added->len > 0)
    {
        g_signal_emit (self, signals[HITS_ADDED], 0, added);
    }
}

static void
check_providers_status (NautilusSearchEngine *self)
{
    g_assert (self->running);

    if (self->starting || self->providers_finished < self->providers_started)
    {
        return;
    }

    if (self->restart)
    {
        g_debug ("Search engine finished and restarting");
    }
    else
    {
        g_debug ("Search engine finished");

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

    g_hash_table_remove_all (self->uris);

    if (self->restart)
    {
        self->restart = FALSE;
        g_debug ("Search engine restarting");
        search_engine_start_real (self);
    }
    else
    {
        self->running = FALSE;
        g_object_notify (G_OBJECT (self), "running");

        g_object_unref (self);
    }
}

static void
search_provider_finished (NautilusSearchEngine *self)
{
    g_debug ("Search provider finished");

    self->providers_finished++;

    check_providers_status (self);
}

typedef NautilusSearchProvider *(* CreateFunc) (void);

static void
setup_provider (NautilusSearchEngine    *self,
                NautilusSearchProvider **provider_pointer,
                NautilusSearchType       provider_flag,
                CreateFunc               create_func)
{
    if (self->search_type & provider_flag)
    {
        if (*provider_pointer == NULL)
        {
            *provider_pointer = create_func ();

            g_signal_connect (*provider_pointer, "hits-added",
                              G_CALLBACK (search_provider_hits_added),
                              self);
            g_signal_connect_swapped (*provider_pointer, "provider-finished",
                                      G_CALLBACK (search_provider_finished),
                                      self);
        }
    }
    else
    {
        g_clear_object (provider_pointer);
    }
}

void
nautilus_search_engine_set_search_type (NautilusSearchEngine *self,
                                        NautilusSearchType    search_type)
{
    if (self->search_type == search_type)
    {
        return;
    }

    self->search_type = search_type;

    setup_provider (self, &self->localsearch, NAUTILUS_SEARCH_TYPE_LOCALSEARCH,
                    (CreateFunc) nautilus_search_engine_localsearch_new);
    setup_provider (self, &self->model, NAUTILUS_SEARCH_TYPE_MODEL,
                    (CreateFunc) nautilus_search_engine_model_new);
    setup_provider (self, &self->recent, NAUTILUS_SEARCH_TYPE_RECENT,
                    (CreateFunc) nautilus_search_engine_recent_new);
    setup_provider (self, &self->simple, NAUTILUS_SEARCH_TYPE_SIMPLE,
                    (CreateFunc) nautilus_search_engine_simple_new);
}

static void
nautilus_search_engine_finalize (GObject *object)
{
    NautilusSearchEngine *self = NAUTILUS_SEARCH_ENGINE (object);

    g_hash_table_destroy (self->uris);

    g_clear_object (&self->localsearch);
    g_clear_object (&self->recent);
    g_clear_object (&self->model);
    g_clear_object (&self->simple);
    g_clear_object (&self->query);

    G_OBJECT_CLASS (nautilus_search_engine_parent_class)->finalize (object);
}

static void
nautilus_search_engine_get_property (GObject    *object,
                                     guint       prop_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
    NautilusSearchEngine *self = NAUTILUS_SEARCH_ENGINE (object);

    switch (prop_id)
    {
        case PROP_RUNNING:
        {
            g_value_set_boolean (value, self->running);
        }
        break;

        default:
        {
            G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        }
    }
}

static void
search_engine_set_property (GObject      *object,
                            guint         prop_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
    NautilusSearchEngine *self = NAUTILUS_SEARCH_ENGINE (object);

    switch (prop_id)
    {
        case PROP_SEARCH_TYPE:
        {
            NautilusSearchType search_type = g_value_get_int (value);
            nautilus_search_engine_set_search_type (self, search_type);
        }
        break;

        default:
        {
            G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
        }
    }
}

static void
nautilus_search_engine_class_init (NautilusSearchEngineClass *class)
{
    GObjectClass *object_class;

    object_class = (GObjectClass *) class;

    object_class->finalize = nautilus_search_engine_finalize;
    object_class->get_property = nautilus_search_engine_get_property;
    object_class->set_property = search_engine_set_property;

    /**
     * NautilusSearchEngine::running:
     *
     * Whether the search engine is running a search.
     */
    properties[PROP_RUNNING] =
        g_param_spec_boolean ("running",
                              "search running",
                              "Whether the engine is running a search",
                              FALSE,
                              G_PARAM_READABLE);
    properties[PROP_SEARCH_TYPE] =
        g_param_spec_int ("search-type",
                          "search type",
                          "a #NautilusSearchType",
                          0, G_MAXINT, 0,
                          G_PARAM_WRITABLE);

    g_object_class_install_properties (object_class, N_PROPERTIES, properties);

    /**
     * NautilusSearchEngine::hits-added:
     *
     * @engine: The engine which emitted the signal.
     * @hits: (transfer none): #GPtrArray of #NautilusSearchHit
     *
     * Emitted when search hits are found.
     */
    signals[HITS_ADDED] = g_signal_new (
        "hits-added", G_TYPE_FROM_CLASS (object_class),
        G_SIGNAL_RUN_LAST, 0, NULL, NULL,
        g_cclosure_marshal_VOID__POINTER,
        G_TYPE_NONE, 1, G_TYPE_POINTER);

    /**
     * NautilusSearchEngine::search-finished:
     *
     * @engine: The engine which emitted the signal.
     *
     * Emitted when the search finishes. Not emitted in-between restarts.
     */
    signals[SEARCH_FINISHED] = g_signal_new (
        "search-finished", G_TYPE_FROM_CLASS (object_class),
        G_SIGNAL_RUN_LAST, 0, NULL, NULL,
        g_cclosure_marshal_VOID__VOID,
        G_TYPE_NONE, 0);
}

static void
nautilus_search_engine_init (NautilusSearchEngine *self)
{
    self->uris = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
}

NautilusSearchEngine *
nautilus_search_engine_new (NautilusSearchType search_type)
{
    return g_object_new (NAUTILUS_TYPE_SEARCH_ENGINE,
                         "search-type", search_type,
                         NULL);
}
