/** @file per_thread_data.c
 */

// Copyright (C) 2018-2023 Sanford Rockowitz <rockowitz@minsoft.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "config.h"

#include <assert.h>
#include <glib-2.0/glib.h>
#include <string.h>
#include <sys/types.h>

#include "util/debug_util.h"
#include "util/glib_util.h"
#include "util/report_util.h"
#include "util/string_util.h"

#include "base/core.h"
#include "base/displays.h"
#include "base/parms.h"
#include "base/sleep.h"
#include "base/thread_retry_data.h"    // temp circular
#include "base/thread_sleep_data.h"

#include "base/per_thread_data.h"

// Master table of sleep data for all threads
GHashTable *    per_thread_data_hash = NULL;
static GPrivate this_thread_has_lock;
static GPrivate lock_depth; // GINT_TO_POINTER(0);
static bool     debug_mutex = false;
       int      ptd_lock_count = 0;
       int      ptd_unlock_count = 0;
       int      cross_thread_operation_blocked_count = 0;

void dbgrpt_per_thread_data_locks(int depth) {
   rpt_vstring(depth, "ptd_lock_count:                        %-4d", ptd_lock_count);
   rpt_vstring(depth, "ptd_unlock_count:                      %-4d", ptd_unlock_count);
   rpt_vstring(depth, "cross_thread_operation_blocked_count:  %-4d", cross_thread_operation_blocked_count);
}

static bool    cross_thread_operation_active = false;
static GMutex  cross_thread_operation_mutex;
static pid_t   cross_thread_operation_owner;

// The locking strategy relies on the fact that in practice conflicts
// will be rare, and critical sections short.
// Operations that occur on the
// are blocked only using a spin-lock.

// The groups of operations:
// - Operations that operate on the single Per_Thread_Data instance
//   associated with the currently executing thread.
// - Operations that operate on a single Per_Thread_Data instance,
//   but possibly not from the thread associated with the Per_Thread_Data instance.
// - Operations that operate on multiple Per_Thread_Data instances.
//   These are referred to as cross thread operations.
//   Alt, perhaps clearer, refer to them as multi-thread data instances.

/**
 */
bool ptd_cross_thread_operation_start() {
   // Only 1 cross thread action can be active at one time.
   // All per_thread actions must wait

   bool debug = false;
   debug = debug || debug_mutex;

   bool lock_performed = false;

   // which way is better?
   bool thread_has_lock  = GPOINTER_TO_INT(g_private_get(&this_thread_has_lock));
   int thread_lock_depth = GPOINTER_TO_INT(g_private_get(&lock_depth));
   assert ( ( thread_has_lock && thread_lock_depth  > 0) ||
            (!thread_has_lock && thread_lock_depth == 0) );
   DBGMSF(debug, "Already locked: %s", sbool(thread_has_lock));

   if (thread_lock_depth == 0) {    // (A)
   // if (!thread_has_lock) {
      // thread_lock_depth is per-thread, so must be unchanged from (A)
      g_mutex_lock(&cross_thread_operation_mutex);
      lock_performed = true;
      cross_thread_operation_active = true;

      ptd_lock_count++;

      // should this be a depth counter rather than a boolean?
      g_private_set(&this_thread_has_lock, GINT_TO_POINTER(true));

      Thread_Output_Settings * thread_settings = get_thread_settings();
      // intmax_t cur_thread_id = get_thread_id();
      intmax_t cur_thread_id = thread_settings->tid;
      cross_thread_operation_owner = cur_thread_id;
      DBGMSF(debug, "Locked by thread %d", cur_thread_id);
      sleep_millis(10);   // give all per-thread functions time to finish
   }
   g_private_set(&lock_depth, GINT_TO_POINTER(thread_lock_depth+1));
   DBGMSF(debug, "Returning: %s", sbool(lock_performed) );
   return lock_performed;
}


void ptd_cross_thread_operation_end() {
   int thread_lock_depth = GPOINTER_TO_INT(g_private_get(&lock_depth));
   g_private_set(&lock_depth, GINT_TO_POINTER(thread_lock_depth-1));
   assert(thread_lock_depth >= 1);

   if (thread_lock_depth == 1) {
   // if (unlock_requested) {
      cross_thread_operation_active = false;
      cross_thread_operation_owner = 0;
      g_private_set(&this_thread_has_lock, false);
      ptd_unlock_count++;
      assert(ptd_lock_count == ptd_unlock_count);
      g_mutex_unlock(&cross_thread_operation_mutex);
   }
   else {
      assert( ptd_lock_count > ptd_unlock_count );
   }
}


/** Block execution of single Per_Thread_Data operations when an operation
 *  involving multiple Per_Thead_Data instances is active.
 */

void ptd_cross_thread_operation_block() {
   // intmax_t cur_threadid = get_thread_id();
   Thread_Output_Settings * thread_settings = get_thread_settings();
   intmax_t cur_threadid = thread_settings->tid;
   if (cross_thread_operation_active && cur_threadid != cross_thread_operation_owner) {
      __sync_fetch_and_add(&cross_thread_operation_blocked_count, 1);
      do {
         sleep_millis(10);
      } while (cross_thread_operation_active);
   }
}


// GDestroyNotify: void (*GDestroyNotify) (gpointer data);

void per_thread_data_destroy(void * data) {
   if (data) {
      Per_Thread_Data * ptd = data;
      free(ptd->description);
      free(ptd);
   }
}

/** Initialize per_thread_data.c at program startup */
void init_thread_data_module() {
   per_thread_data_hash = g_hash_table_new_full(g_direct_hash, NULL, NULL, per_thread_data_destroy);
   // DBGMSG("per_thead_data_hash = %p", per_thread_data_hash);
}

void release_thread_data_module() {
   if (per_thread_data_hash) {
      g_hash_table_destroy(per_thread_data_hash);
   }
}

//
// Locking
//

static void ptd_init(Per_Thread_Data * ptd) {
   tsd_init_thread_sleep_data(ptd);
   trd_init(ptd);
}


/** Gets the #Per_Thread_Data struct for the current thread, using the
 *  current thread's id number. If the struct does not already exist, it
 *  is allocated and initialized.
 *
 *  @return pointer to #Per_Thread_Data struct
 *
 *  @remark
 *  The structs are maintained centrally rather than using a thread-local pointer
 *  to a block on the heap because the of a problems when the thread is closed.
 *  Valgrind complains of access errors for closed threads, even though the
 *  struct is on the heap and still readable.
 */
Per_Thread_Data * ptd_get_per_thread_data() {
   bool debug = false;
   // intmax_t cur_thread_id = get_thread_id();
   Thread_Output_Settings * thread_settings = get_thread_settings();
   intmax_t cur_thread_id = thread_settings->tid;
   // DBGMSF(debug, "Getting thread sleep data for thread %d", cur_thread_id);
   // bool this_function_owns_lock = ptd_lock_if_unlocked();
   assert(per_thread_data_hash);    // allocated by init_thread_data_module()
   // DBGMSG("per_thread_data_hash = %p", per_thread_data_hash);
   // n. data hash for current thread can only be looked up from current thread,
   // so there's nothing can happen to per_thread_data_hash before g_hash_table_insert()
   Per_Thread_Data * data = g_hash_table_lookup(per_thread_data_hash,
                                            GINT_TO_POINTER(cur_thread_id));
   if (!data) {
      DBGMSF(debug, "==> Per_Thread_Data not found for thread %d", cur_thread_id);
      data = g_new0(Per_Thread_Data, 1);
      data->thread_id = cur_thread_id;
      g_private_set(&lock_depth, GINT_TO_POINTER(0));
      ptd_init(data);
      DBGMSF(debug, "Initialized: %s. thread_sleep_data_defined: %s. thread_retry_data_defined; %s",
           sbool(data->initialized),
           sbool( data->thread_sleep_data_defined), sbool( data->thread_retry_data_defined));

      g_hash_table_insert(per_thread_data_hash,
                          GINT_TO_POINTER(cur_thread_id),
                          data);
      DBGMSF(debug, "Created Per_Thread_Data struct for thread id = %d", data->thread_id);
      DBGMSF(debug, "per_thread_data_hash size=%d", g_hash_table_size(per_thread_data_hash));
      // if (debug)
      //   dbgrpt_per_thread_data(data, 1);
   }
   // ptd_unlock_if_needed(this_function_owns_lock);
   return data;
}


// Thread description operations always operate on the Per_Thread_Data
// instance for the currently executing thread.

void ptd_set_thread_description(const char * description) {
   ptd_cross_thread_operation_block();
   Per_Thread_Data *  ptd = ptd_get_per_thread_data();
   // DBGMSG("thread: %d, description: %s", ptd->thread_id, description);
   if (ptd->description)
      free(ptd->description);
   ptd->description = strdup(description);
}


void ptd_append_thread_description(const char * addl_description) {
   ptd_cross_thread_operation_block();
   Per_Thread_Data *  ptd = ptd_get_per_thread_data();
   // DBGMSG("ptd->description = %s, addl_descripton = %s", ptd->description, addl_description);
   if (!ptd->description)
      ptd->description = strdup(addl_description);
   else if (str_contains(ptd->description, addl_description) < 0) {
      char * s = ptd->description;
      ptd->description = g_strdup_printf("%s; %s", ptd->description, addl_description);
      free(s);
   }

   // DBGMSG("Final ptd->description = %s", ptd->description);
}


const char * ptd_get_thread_description_t() {
   static GPrivate  x_key = G_PRIVATE_INIT(g_free);
   static GPrivate  x_len_key = G_PRIVATE_INIT(g_free);

   ptd_cross_thread_operation_block();
   Per_Thread_Data *  ptd = ptd_get_per_thread_data();
   char * buf = NULL;
   if (ptd->description) {
      char * buf = get_thread_dynamic_buffer(&x_key, &x_len_key, strlen(ptd->description)+1);
      strcpy(buf,ptd->description);
   }
   return buf;
}


char * int_array_to_string(uint16_t * start, int ct) {
   int bufsz = ct*10;
   char * buf = calloc(1, bufsz);
   int next = 0;
   for (int ctr =0; ctr < ct && next < bufsz; ctr++) {
      g_snprintf(buf+next, bufsz-next,"%s%d", (next > 0) ? ", " : "", *(start+ctr) );
      next = strlen(buf);
   }
   // DBGMSG("start=%p, ct=%d, returning %s", start, ct, buf);
   return buf;
}


/** Output a debug report of a #Per_Thread_Data struct.
 *
 *  @param  data   pointer to #Per_Thread_Data struct
 *  @param  depth  logical indentation level
 *
 *  // relies on caller for possible blocking
 */
void dbgrpt_per_thread_data(Per_Thread_Data * data, int depth) {
   int d1 = depth+1;
   rpt_structure_loc("Per_Thread_Data", data, depth);
 //rpt_int( "sizeof(Per_Thread_Data)",  NULL, sizeof(Per_Thread_Data),   d1);
   rpt_bool("initialized",                NULL, data->initialized,           d1);
   rpt_int( "thread_id",                  NULL, data->thread_id,             d1);
   rpt_vstring(d1,"description                %s", data->description);
   rpt_bool("sleep data initialized" ,    NULL, data->thread_sleep_data_defined, d1);

   // Sleep multiplier adjustment:
   rpt_vstring(d1, "sleep-multiplier value:           %15.2f", data->sleep_multiplier_factor);
   rpt_int("sleep_multiplier_ct",         NULL, data->sleep_multiplier_ct,        d1);
   rpt_vstring(d1, "sleep_multiplier_changer_ct:      %15d",   data->sleep_multipler_changer_ct);
   rpt_vstring(d1, "highest_sleep_multiplier_ct:      %15d",   data->highest_sleep_multiplier_ct);

   rpt_vstring(d1,"cur_dh:                 %s", dh_repr(data->cur_dh) );

   // Dynamic sleep adjustment:
   rpt_bool("dynamic_sleep_enabled",      NULL, data->dynamic_sleep_enabled,     d1);
   rpt_int("cur_ok_status_count",         NULL, data->cur_ok_status_count,       d1);
   rpt_int("cur_error_status_count",      NULL, data->cur_error_status_count,    d1);
   rpt_int("total_ok_status_count",       NULL, data->total_ok_status_count,     d1);
   rpt_int("total_error",                 NULL, data->total_error_status_count,  d1);
   rpt_int("other_status_ct",             NULL, data->total_other_status_ct,     d1);

   rpt_int("calls_since_last_check",      NULL, data->calls_since_last_check,    d1);
   rpt_int("total_adjustment_checks",     NULL, data->total_adjustment_checks,   d1);
   rpt_int("adjustment_ct",               NULL, data->total_adjustment_ct,       d1);
// rpt_int("max_adjustment_ct",           NULL, data->total_max_adjustment_ct,   d1);
// rpt_int("non_adjustment_ct",           NULL, data->total_non_adjustment_ct,   d1);

   rpt_vstring(d1, "cur_sleep_adjustmet_factor     %15.2f", data->cur_sleep_adjustment_factor);
// rpt_vstring(d1, "thread_adjustment_increment        %15.2f", data->thread_adjustment_increment);

   // Maxtries history
   rpt_bool("retry data initialized"    , NULL, data->thread_retry_data_defined, d1);

   rpt_vstring(d1, "Highest maxtries:                  %d,%d,%d,%d",
                    data->highest_maxtries[0], data->highest_maxtries[1],
                    data->highest_maxtries[2], data->highest_maxtries[3]);
   rpt_vstring(d1, "Current maxtries:                  %d,%d,%d,%d",
                    data->current_maxtries[0], data->current_maxtries[1],
                    data->current_maxtries[2], data->current_maxtries[3]);
   rpt_vstring(d1, "Lowest maxtries:                   %d,%d,%d,%d",
                    data->lowest_maxtries[0], data->lowest_maxtries[1],
                    data->lowest_maxtries[2], data->lowest_maxtries[3]);

   for (int retry_type = 0; retry_type < 4; retry_type++) {
      int upper_bound = data->highest_maxtries[retry_type] + 1;
      assert(upper_bound <= MAX_MAX_TRIES + 1);
      char * buf = int_array_to_string( data->try_stats[retry_type].counters, upper_bound);
      rpt_vstring(d1, "try_stats[%d=%-27s].counters = %s",
                      retry_type, retry_type_name(retry_type), buf);
      free(buf);
   }
}


/** Applies a specified function with signature GFunc to all
 *  #Per_Thread_Data instances.
 *
 *  @param  func  function to apply
 *  \parm   arg   an arbitrary argument passed as a pointer
 *
 *  This is a multi-instance operation.
 */
void ptd_apply_all(Ptd_Func func, void * arg) {
   ptd_cross_thread_operation_start();
   bool debug = false;
   assert(per_thread_data_hash);    // allocated by init_thread_data_module()

      GHashTableIter iter;
      gpointer key, value;
      g_hash_table_iter_init (&iter,per_thread_data_hash);
      while (g_hash_table_iter_next (&iter, &key, &value)) {
         Per_Thread_Data * data = value;
         DBGMSF(debug, "Thread id: %d", data->thread_id);
         func(data, arg);
      }

   ptd_cross_thread_operation_end();
}


/** Apply a given function to all #Per_Thread_Data structs, ordered by thread id.
 *  Note that this report includes structs for threads that have been closed.
 *
 *  @param func function to apply
 *  @param arg pointer or integer value
 *
 *  This is a multi-instance operation.
 */
void ptd_apply_all_sorted(Ptd_Func func, void * arg) {
   bool debug = false;
   DBGMSF(debug, "Starting");
   ptd_cross_thread_operation_start();
   assert(per_thread_data_hash);

   DBGMSF(debug, "hash table size = %d", g_hash_table_size(per_thread_data_hash));
   GList * keys = g_hash_table_get_keys (per_thread_data_hash);
   GList * new_head = g_list_sort(keys, gaux_ptr_intcomp);
   GList * l;
   for (l = new_head; l != NULL; l = l->next) {
      int key = GPOINTER_TO_INT(l->data);
      DBGMSF(debug, "Key: %d", key);
      Per_Thread_Data * data = g_hash_table_lookup(per_thread_data_hash, l->data);
      assert(data);

      func(data, arg);
   }
   g_list_free(new_head);   // would keys also work?

   ptd_cross_thread_operation_end();
   DBGMSF(debug, "Done");
}


/** Emits a brief summary of a #Per_Thread_Data instance,
 *  showing the thread id number and description.
 *
 *  ptd   pointer to #Per_Thread_Data instance
 *  arg   logical indentation
 *
 *  @note
 *  This function has a GFunc signature
 *  @note
 *  Called only by multi-thread-data functions that hold lock
 */
void ptd_thread_summary(Per_Thread_Data * ptd, void * arg) {
   int depth = GPOINTER_TO_INT(arg);
   int d1 = depth+1;
   ptd_cross_thread_operation_block();

   // simple but ugly
   // rpt_vstring(depth, "Thread: %d. Description:%s",
   //             ptd->thread_id, ptd->description);

   // rpt_vstring(depth, "Thread: %d", ptd->thread_id);
   // char * header = "Description: ";

   char header[100];
   g_snprintf(header, 100, "Thread %d: ", ptd->thread_id);

   int hdrlen = strlen(header);
   if (!ptd->description)
      rpt_vstring(d1, "%s", header);
   else {
      Null_Terminated_String_Array pieces =
            strsplit_maxlength(ptd->description, 60, " ");
      int ntsa_ndx = 0;
      while (true) {
         char * s = pieces[ntsa_ndx++];
         if (!s)
            break;
         rpt_vstring(d1, "%-*s%s", hdrlen, header, s);
         if (strlen(header) > 0) {
            // *header = '\0';    // better, test it next time working with this function
            strcpy(header, "");
            // header = "";
         }
      }
      ntsa_free(pieces, true);
   }
}


/** Emits a brief summary (thread id and description) for each
 * #Per_Thread_Data instance.
 *
 *  @param  depth   logical indentation depth
 */
void ptd_list_threads(int depth) {
   // bool this_function_owns_lock = ptd_lock_if_unlocked();

   int d1 = depth +1;
   // rpt_label(depth, "Have data for threads:");
   rpt_label(depth, "Report has per-thread data for threads:");
   ptd_apply_all_sorted(ptd_thread_summary, GINT_TO_POINTER(d1));
   rpt_nl();
}


void report_all_thread_status_counts(int depth) {
   bool debug = false;
   DBGMSF(debug, "Starting");
   rpt_label(depth, "No per-thread status code statistics are collected");
   rpt_nl();
   DBGMSF(debug, "Done");
}

