/*
 *  $Id: recent-funcs.c 30107 2026-06-22 09:29:56Z yeti-dn $
 *  Copyright (C) 2026 David Necas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program 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 General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"

#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/utils.h"

#include "libgwyapp/recent-funcs.h"

#define FUNC_DELTA 0.02

typedef struct {
    gdouble weight;
    GQuark qname;
} RecentFunc;

typedef struct {
    GHashTable *funcs;
    GArray *head;
    gdouble scaled_delta;
    gdouble delta;
    GQuark qname;
    gboolean head_is_valid;

    FILE *fh;
} RecentFuncList;

static GHashTable *recent_lists = NULL;

static void
free_recent_func_list(gpointer p)
{
    RecentFuncList *rlist = (RecentFuncList*)p;
    g_hash_table_destroy(rlist->funcs);
    GWY_FREE_ARRAY(rlist->head);
    g_free(rlist);
}

static RecentFuncList*
ensure_func_list(const gchar *kind)
{
    if (!recent_lists)
        recent_lists = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, free_recent_func_list);

    GQuark qname = g_quark_from_string(kind);
    RecentFuncList *rlist = g_hash_table_lookup(recent_lists, GSIZE_TO_POINTER(qname));
    if (rlist)
        return rlist;

    rlist = g_new0(RecentFuncList, 1);
    rlist->delta = FUNC_DELTA;
    rlist->scaled_delta = rlist->delta;
    rlist->qname = qname;
    rlist->funcs = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
    g_hash_table_insert(recent_lists, GSIZE_TO_POINTER(qname), rlist);

    return rlist;
}

static void
sum_weights(G_GNUC_UNUSED gpointer key, gpointer value, gpointer user_data)
{
    const RecentFunc *rfunc = (const RecentFunc*)value;
    gdouble *sum = (gdouble*)user_data;
    *sum += rfunc->weight;
}

static void
normalise_weights(G_GNUC_UNUSED gpointer key, gpointer value, gpointer user_data)
{
    RecentFunc *rfunc = (RecentFunc*)value;
    const gdouble *factor = (const gdouble*)user_data;
    rfunc->weight *= *factor;
}

static void
normalise_list(RecentFuncList *rlist)
{
    gdouble sum = G_MINDOUBLE;
    g_hash_table_foreach(rlist->funcs, sum_weights, &sum);
    // Keep sums smaller than one unnormalised, except that we reset the scaling. Apart from someone making a mess in
    // the file, this can occur when the user has run only a handful functions so far, maybe in the initial session.
    // By normalising, we would then give them too high relative weight compared to everything that will come after
    // and will only get Δ.
    if (sum > 1.0) {
        gdouble factor = 1.0/sum;
        g_hash_table_foreach(rlist->funcs, normalise_weights, &factor);
    }
    else {
        gdouble factor = 1.0/rlist->scaled_delta;
        g_hash_table_foreach(rlist->funcs, normalise_weights, &factor);
    }
    rlist->scaled_delta = rlist->delta;
}

static void
update_head_or_invalidate(RecentFuncList *rlist, const RecentFunc *rfunc)
{
    if (!rlist->head_is_valid)
        return;
    GArray *head = rlist->head;
    g_assert(head);
    RecentFunc *headfuncs = &g_array_index(head, RecentFunc, 0);
    guint headlen = head->len;
    if (!headlen || rfunc->weight < headfuncs[headlen-1].weight)
        return;

    // Now @rfunc either already is in the head or just moved there.
    guint pos;
    for (pos = 0; pos < headlen; pos++) {
        if (headfuncs[pos].qname == rfunc->qname)
            break;
    }
    if (pos == headlen) {
        rlist->head_is_valid = FALSE;
        return;
    }

    // The function is already in head. We can just bubble it up.
    headfuncs[pos].weight = rfunc->weight;
    while (pos && headfuncs[pos].weight > headfuncs[pos-1].weight) {
        GWY_SWAP(RecentFunc, headfuncs[pos], headfuncs[pos-1]);
        pos--;
    }
}

/**
 * gwy_recent_funcs_record_use:
 * @kind: Function kind.
 * @name: Name of function.
 *
 * Records one use of a function.
 *
 * For meaningful statistics, programmatic execution should not be recorded, only user-invoked functions.
 **/
void
gwy_recent_funcs_record_use(const gchar *kind,
                            const gchar *name)
{
    g_return_if_fail(kind && name);
    RecentFuncList *rlist = ensure_func_list(kind);
    GQuark quark = g_quark_from_string(name);
    RecentFunc *rfunc = g_hash_table_lookup(rlist->funcs, GSIZE_TO_POINTER(quark));
    if (!rfunc) {
        rfunc = g_new(RecentFunc, 1);
        rfunc->qname = quark;
        rfunc->weight = 0.0;
        g_hash_table_insert(rlist->funcs, GSIZE_TO_POINTER(quark), rfunc);
    }

    // Conceptually, we want to do:
    //
    //    this weight → this weight + Δ
    //
    // and then
    //
    //    all weights → weight/(1 + Δ)
    //
    // So if the weight sum started normalised to 1, it will remain normalised to 1. However, this needs to touch all
    // the weights every time. Therefore, we do this instead (starting with Δ' = Δ):
    //
    //    this weight → this weight + Δ'
    //    Δ' → Δ'(1 + Δ)
    //
    // And only normalise the sum to 1 occasionally. Like when saving or when Δ' exceedes some large number.
    rfunc->weight += rlist->scaled_delta;
    rlist->scaled_delta *= 1.0 + rlist->delta;

    if (rlist->scaled_delta > 1000.0) {
        // The normalisation factor is 1/(1 + scaled_delta). However, to avoid accumulating errors, calculate the
        // sum the hard way. The amortised cost is low anyway.
        normalise_list(rlist);
    }
    update_head_or_invalidate(rlist, rfunc);
}

static void
rebuild_head(G_GNUC_UNUSED gpointer key, gpointer value, gpointer user_data)
{
    const RecentFunc *rfunc = (const RecentFunc*)value;
    GArray *head = (GArray*)user_data;
    RecentFunc *headfuncs = &g_array_index(head, RecentFunc, 0);
    guint headlen = head->len;

    guint pos;
    for (pos = 0; pos < headlen; pos++) {
        if (!headfuncs[pos].qname || rfunc->weight > headfuncs[pos].weight)
            break;
        // Ensure stable sort for equal weights.
        if (rfunc->weight == headfuncs[pos].weight
            && strcmp(g_quark_to_string(rfunc->qname), g_quark_to_string(headfuncs[pos].qname)) < 0)
            break;
    }
    if (pos == headlen)
        return;
    if (pos == headlen-1) {
        headfuncs[pos] = *rfunc;
        return;
    }

    g_array_remove_index_fast(head, headlen-1);
    g_array_insert_vals(head, pos, rfunc, 1);
}

static guint
make_valid_head(RecentFuncList *rlist, guint n)
{
    GArray *head = rlist->head;
    guint ntotal = g_hash_table_size(rlist->funcs);
    if (rlist->head_is_valid) {
        g_assert(head);
        if (head->len >= n)
            return n;
        if (head->len == ntotal)
            return head->len;
    }
    if (!rlist->head)
        head = rlist->head = g_array_new(FALSE, FALSE, sizeof(RecentFunc));

    // Do not create the head larger than how many functions we have recorded in total.
    guint newlen = MIN(ntotal, n);
    g_array_set_size(head, newlen);
    // This is more to avoid dealing with a zero-sized array in rebuild_head() than for optimisaiton.
    if (newlen) {
        RecentFunc *headfuncs = &g_array_index(head, RecentFunc, 0);
        gwy_clear(headfuncs, newlen);
        g_hash_table_foreach(rlist->funcs, rebuild_head, head);
    }
    rlist->head_is_valid = TRUE;
    return head->len;
}

/**
 * gwy_recent_funcs_get_head:
 * @kind: Function kind.
 * @names: (out) (array-size n): Array of size at least @n where to put the names of most used functions.
 * @n: The size of @names.
 *
 * Obtains a sorted list of most and/or most recently used functions.
 *
 * The functions are sorted by usage, in descending order. The output array may not be fully filled with function
 * names if there are not enough functions in the usage history. Check the return value. The empty elements are set to
 * NULL.
 *
 * Normally, only a handful of most recent functions is needed, and the usage recording is designed for that. If you
 * request a long list, recoding future usage can become somewhat inefficient.
 *
 * Returns: The number of items actually filled in @names.
 **/
guint
gwy_recent_funcs_get_head(const gchar *kind,
                          const gchar **names,
                          guint n)
{
    g_return_val_if_fail(kind && (names || !n), 0);
    if (!recent_lists) {
        gwy_clear(names, n);
        return 0;
    }

    GQuark qkind = g_quark_try_string(kind);
    RecentFuncList *rlist = qkind ? g_hash_table_lookup(recent_lists, GSIZE_TO_POINTER(qkind)) : NULL;
    if (!rlist) {
        gwy_clear(names, n);
        return 0;
    }

    guint len = make_valid_head(rlist, n);
    for (guint i = 0; i < len; i++)
        names[i] = g_quark_to_string(g_array_index(rlist->head, RecentFunc, i).qname);
    if (n > len)
        gwy_clear(names + len, n - len);
    return len;
}

static void
normalise(G_GNUC_UNUSED gpointer key, gpointer value, G_GNUC_UNUSED gpointer user_data)
{
    RecentFuncList *rlist = (RecentFuncList*)value;
    normalise_list(rlist);
}

/**
 * gwy_recent_funcs_load:
 * @filename: File name to load recently used functions from.
 * @error: Optional location to store any error occurred.
 *
 * Loads information about recent function usage from a file.
 *
 * Any existing information is completely replaced. However, this function should normally be only called once during
 * program initialisation.
 *
 * Returns: %TRUE if the operation succeeded.
 **/
gboolean
gwy_recent_funcs_load(const gchar *filename,
                      GError **error)
{
    g_return_val_if_fail(filename, FALSE);

    gchar *text;
    if (!g_file_get_contents(filename, &text, NULL, error))
        return FALSE;

    if (recent_lists) {
        g_hash_table_destroy(recent_lists);
        recent_lists = NULL;
    }

    gchar *p = text;
    for (gchar *line = gwy_str_next_line(&p); line; line = gwy_str_next_line(&p)) {
        g_strstrip(line);
        if (!*line)
            continue;

        gchar *pieces[3] = { line, NULL, NULL };
        gchar *piece = line;
        for (guint i = 1; i < 3; i++) {
            while (*piece && !g_ascii_isspace(*piece))
                piece++;
            if (!*piece)
                break;
            while (g_ascii_isspace(*piece)) {
                *piece = '\0';
                piece++;
            }
            pieces[i] = piece;
        }
        if (!pieces[2])
            continue;

        gwy_debug("<%s> <%s> <%s>", pieces[0], pieces[1], pieces[2]);
        // Normally this means a parsing failure. But even if we somehow get a valid non-positive weihgt, skip it.
        gdouble weight = g_ascii_strtod(pieces[2], NULL);
        if (!(weight > 0.0))
            continue;
        RecentFuncList *rlist = ensure_func_list(pieces[0]);
        RecentFunc *rfunc = g_new(RecentFunc, 1);
        rfunc->qname = g_quark_from_string(pieces[1]);
        rfunc->weight = weight;
        g_hash_table_insert(rlist->funcs, GSIZE_TO_POINTER(rfunc->qname), rfunc);
    }

    g_free(text);

    // There can be no recent functions yet, and that is OK.
    if (recent_lists)
        g_hash_table_foreach(recent_lists, normalise, NULL);

    return TRUE;
}

static void
write_func(G_GNUC_UNUSED gpointer key, gpointer value, gpointer user_data)
{
    const RecentFunc *rfunc = (const RecentFunc*)value;
    RecentFuncList *rlist = (RecentFuncList*)user_data;
    gchar buf[G_ASCII_DTOSTR_BUF_SIZE];
    g_ascii_dtostr(buf, sizeof(buf), rfunc->weight);
    fprintf(rlist->fh, "%s %s %s\n", g_quark_to_string(rlist->qname), g_quark_to_string(rfunc->qname), buf);
}

static void
save_list(G_GNUC_UNUSED gpointer key, gpointer value, gpointer user_data)
{
    RecentFuncList *rlist = (RecentFuncList*)value;
    // No need to normalise because we need to normalise upon loading anyway.
    rlist->fh = (FILE*)user_data;
    g_hash_table_foreach(rlist->funcs, write_func, rlist);
    rlist->fh = NULL;
}

/**
 * gwy_recent_funcs_save:
 * @filename: File name to save recently used functions to.
 * @error: Optional location to store any error occurred.
 *
 * Saves information about recent function usage to a file.
 *
 * Returns: %TRUE if the operation succeeded.
 **/
gboolean
gwy_recent_funcs_save(const gchar *filename,
                      GError **error)
{
    g_return_val_if_fail(filename, FALSE);

    FILE *fh = gwy_fopen(filename, "w");
    if (!fh) {
        g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(errno),
                    _("Cannot open file for writing: %s."), g_strerror(errno));
        return FALSE;
    }

    if (recent_lists)
        g_hash_table_foreach(recent_lists, save_list, fh);

    fclose(fh);

    return TRUE;
}

/**
 * SECTION: recent-funcs
 * @title: Recent functions
 * @short_description: Remember the most and/or most recently used functions
 *
 * Every time a function is used, record it with gwy_recent_funcs_record_use(). You can then use
 * gwy_recent_funcs_get_head() to obtain a list of the most and/or most recently used functions. The usage weight
 * slowly fades away over time. Frequently used functions will always come on top. For less used functions, recency
 * has a larger influence.
 **/

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
