/** @file api_base.c
 *
 *  C API base functions.
 */

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

#include "config.h"

#include <dlfcn.h>     // _GNU_SOURCE for dladdr()
#include <errno.h>
#include <glib-2.0/glib.h>
#include <signal.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

#include "public/ddcutil_c_api.h"

#include "util/ddcutil_config_file.h"
#include "util/dbus_util.h"
#include "util/debug_util.h"
#include "util/file_util.h"
#include "util/linux_util.h"
#include "util/msg_util.h"
#include "util/regex_util.h"
#include "util/report_util.h"
#include "util/sysfs_filter_functions.h"
#include "util/traced_function_stack.h"
#include "util/xdg_util.h"

#include "base/base_services.h"
#include "base/build_info.h"
#include "base/build_timestamp.h"
#include "base/core_per_thread_settings.h"
#include "base/display_lock.h"
#include "base/core.h"
#include "base/dsa2.h"
#include "base/dw_base.h"
#include "base/parms.h"
#include "base/per_display_data.h"
#include "base/per_thread_data.h"
#include "base/rtti.h"
#include "base/trace_control.h"
#include "base/tuned_sleep.h"

#include "cmdline/cmd_parser.h"
#include "cmdline/parsed_cmd.h"

#include "sysfs/sysfs_base.h"

#include "i2c/i2c_bus_core.h"   // for testing watch_devices
#include "i2c/i2c_execute.h"    // for i2c_set_addr()

#include "ddc/ddc_common_init.h"
#include "ddc/ddc_displays.h"
#include "ddc/ddc_multi_part_io.h"
#include "ddc/ddc_packet_io.h"
#include "ddc/ddc_serialize.h"
#include "ddc/ddc_services.h"
#include "ddc/ddc_try_data.h"
#include "ddc/ddc_vcp.h"

#ifdef WATCH_DISPLAYS
#include "dw/dw_main.h"
#include "dw/dw_services.h"
#include "dw/dw_status_events.h"
#endif

#include "libmain/api_base_internal.h"
#include "libmain/api_error_info_internal.h"
#include "libmain/api_services_internal.h"

//
// Forward Declarations
//

void init_api_base();

//
// Globals
//

bool library_initialized = false;
bool library_initialization_failed = false;
static bool client_opened_syslog = false;
static bool enable_init_msgs = false;
static FILE * flog = NULL;
static DDCA_Stats_Type requested_stats = 0;
static bool per_display_stats = false;
static bool dsa_detail_stats;
static int    active_calls = 0;
static int    max_active_calls = 0;
static GMutex active_calls_mutex;
static bool   api_quiesced = false;
static GMutex api_quiesced_mutex;


//
// Precondition Failure
//

DDCI_Api_Precondition_Failure_Mode api_failure_mode = DDCI_PRECOND_STDERR_RETURN;

#ifdef UNUSED
static DDCI_Api_Precondition_Failure_Mode
ddci_set_precondition_failure_mode(
      DDCI_Api_Precondition_Failure_Mode failure_mode)
{
   DDCI_Api_Precondition_Failure_Mode old = api_failure_mode;
   api_failure_mode = failure_mode;
   return old;
}

static DDCI_Api_Precondition_Failure_Mode
ddci_get_precondition_failure_mode()
{
   return api_failure_mode;
}
#endif


//
// Library Build Information
//

DDCA_Ddcutil_Version_Spec
ddca_ddcutil_version(void) {
   static DDCA_Ddcutil_Version_Spec vspec = {255,255,255};
   static bool vspec_init = false;

   if (!vspec_init) {
#ifndef NDEBUG
      int ct =
#endif
            sscanf(get_base_ddcutil_version(),
                      "%hhu.%hhu.%hhu", &vspec.major, &vspec.minor, &vspec.micro);
#ifndef NDEBUG
      assert(ct == 3);
#endif
      vspec_init = true;
   }
   // DBGMSG("Returning: %d.%d.%d", vspec.major, vspec.minor, vspec.micro);
   return vspec;
}


/**  Returns the ddcutil version as a string in the form "major.minor.micro".
 *
 */
const char *
ddca_ddcutil_version_string(void) {
   return get_base_ddcutil_version();
}


// Returns the full ddcutil version as a string that may be suffixed with an extension
const char *
ddca_ddcutil_extended_version_string(void) {
   return get_full_ddcutil_version();
}


#ifdef UNUSED
// Indicates whether the ddcutil library was built with support for USB connected monitors.
bool
ddca_built_with_usb(void) {
#ifdef ENABLE_USB
   return true;
#else
   return false;
#endif
}
#endif

// Alternative to individual ddca_built_with...() functions.
// conciseness vs documentability
// how to document bits?   should doxygen doc be in header instead?

DDCA_Build_Option_Flags
ddca_build_options(void) {
   uint8_t result = 0x00;
#ifdef ENABLE_USB
         result |= DDCA_BUILT_WITH_USB;
#endif
#ifdef FAILSIM_ENABLED
         result |= DDCA_BUILT_WITH_FAILSIM;
#endif
   // DBGMSG("Returning 0x%02x", result);
   return result;
}


const char *
ddci_libddcutil_filename(void) {
   Dl_info info = {NULL,NULL,NULL,NULL};
   static char fullname[PATH_MAX];
   static char * p = NULL;
   if (!p) {
      dladdr(ddca_build_options, &info);
      p = realpath(info.dli_fname, fullname);
      assert(p == fullname);
   }
   return p;
}


const char *
ddca_libddcutil_filename(void) {
	return ddci_libddcutil_filename();
}


bool increment_active_api_calls(const char * funcname) {
   bool debug = false;
   DBGTRC_STARTING(debug, DDCA_TRC_NONE, "funcname=%s, active_calls=%d", funcname, active_calls);

   bool result = true;
   g_mutex_lock(&api_quiesced_mutex);  // blocks API calls from starting
   g_mutex_lock(&active_calls_mutex);
   if (api_quiesced || library_disabled)
      result = false;
   else {
      active_calls++;
      if (active_calls > max_active_calls)
         max_active_calls = active_calls;
   }
   g_mutex_unlock(&active_calls_mutex);
   g_mutex_unlock(&api_quiesced_mutex);

   DBGTRC_DONE(debug, DDCA_TRC_NONE, "funcname=%s, returning %s", funcname, SBOOL(result));
   return result;
}


void decrement_active_api_calls(const char * funcname) {
   bool debug = false;
   DBGTRC_STARTING(debug, DDCA_TRC_NONE, "funcname=%s, active_calls=%d", funcname, active_calls);

   bool oops = false;
   g_mutex_lock(&active_calls_mutex);
   if (active_calls > 0) {
      active_calls--;
   }
   else {
      oops = true;
   }
   g_mutex_unlock(&active_calls_mutex);
   if (oops) {
      MSG_W_SYSLOG(DDCA_SYSLOG_ERROR, "Unmatched active call ct in %s", funcname);
   }

   DBGTRC_DONE(debug, DDCA_TRC_NONE, "funcname=%s, oops=%s", funcname, SBOOL(oops));
}


/** Quiesce the API.
 *
 *  When quiesced, API calls that can affect monitor state terminate
 *  immediately with status DDCRC_QUIESCED.
 *
 *  This function waits at most QUEESCE_POLL_MAX_MILLISEC miliseconds
 *  (defined in parms.h) for outstanding API calls to complete.
 *  If calls are still outstanding, an error messages is written to the
 *  system log, but this does not prevent queiescing,
 */
void quiesce_api() {
   bool debug = false;
   DBGTRC_STARTING(debug, DDCA_TRC_API, "");

   SYSLOG2(DDCA_SYSLOG_NOTICE, "Quiescing libddcutil API...");
   bool oops = false;
   int slept_microsec = 0;

   g_mutex_lock(&api_quiesced_mutex);

   g_mutex_lock(&active_calls_mutex);
   if (active_calls > 0) {
      int poll_max_millisec      = QUIESCE_POLL_MAX_MILLISEC;
      int poll_interval_millisec = QUIESCE_POLL_INTERVAL_MILLISEC;
      int poll_max_microsec = poll_max_millisec * 1000;
      int poll_interval_microsec = poll_interval_millisec * 1000;
      oops = true;
      for (; slept_microsec < poll_max_microsec; slept_microsec += poll_interval_microsec) {
         usleep(poll_interval_microsec);
         if (active_calls == 0) {
            oops = false;
            break;
         }
      }
   }
   g_mutex_unlock(&active_calls_mutex);

   api_quiesced = true;
   g_mutex_unlock(&api_quiesced_mutex);

   if (oops) {
      MSG_W_SYSLOG(DDCA_SYSLOG_ERROR,
            "Error quiescing libddcutil API. %d active API calls outstanding.", active_calls);
   }
   else {
      SYSLOG2(DDCA_SYSLOG_NOTICE, "Quiesce libddcutil API complete");
   }

   DBGTRC_DONE(debug, DDCA_TRC_API,
         "Terminating with %d active API calls outstanding. Waited %d millisec",
         active_calls, slept_microsec/1000);
}


/** Unquiesce the API.
 */
void unquiesce_api() {
   bool debug = false;
   DBGTRC_STARTING(debug, DDCA_TRC_API, "");

   SYSLOG2(DDCA_SYSLOG_NOTICE, "Unquiescing libddcutil API...");
   g_mutex_lock(&api_quiesced_mutex);
   api_quiesced = false;
   g_mutex_unlock(&api_quiesced_mutex);

   DBGTRC_DONE(debug, DDCA_TRC_API, "");
}



Error_Info* perform_parse(
      int     new_argc,
      char ** new_argv,
      char *  combined,
      Parsed_Cmd ** parsed_cmd_loc)
{
   GPtrArray * errmsgs = g_ptr_array_new_with_free_func(g_free);
   bool debug = false;

   Error_Info * result = NULL;
   DBGF(debug, "Calling parse_command(), errmsgs=%p\n", errmsgs);
   *parsed_cmd_loc = parse_command(new_argc, new_argv, MODE_LIBDDCUTIL, errmsgs);
   DBGF(debug, "*parsed_cmd_loc=%p, errmsgs->len=%d", *parsed_cmd_loc, errmsgs->len);
   ASSERT_IFF(*parsed_cmd_loc, errmsgs->len == 0);
   if (!*parsed_cmd_loc) {
      if (test_emit_syslog(DDCA_SYSLOG_ERROR)) {
         syslog(LOG_ERR, "Invalid option string: %s",  combined);
         for (int ndx = 0; ndx < errmsgs->len; ndx++) {
             char * msg =  g_ptr_array_index(errmsgs,ndx);
             syslog(LOG_ERR, "%s", msg);
         }
      }
      result = ERRINFO_NEW(DDCRC_INVALID_CONFIG_FILE, "Invalid option string: %s",  combined);
      for (int ndx = 0; ndx < errmsgs->len; ndx++) {
         char * msg =  g_ptr_array_index(errmsgs, ndx);
         errinfo_add_cause(result, errinfo_new(DDCRC_INVALID_CONFIG_FILE, __func__, msg));
      }
   }
   else {
      if (debug)
         dbgrpt_parsed_cmd(*parsed_cmd_loc, 1);
   }
   g_ptr_array_free(errmsgs, true);
   ASSERT_IFF(*parsed_cmd_loc, !result);
   return result;

}


static inline void emit_parse_info_msg(const char * msg, GPtrArray* infomsgs) {
   if (infomsgs)
      g_ptr_array_add(infomsgs, g_strdup_printf("%s%s", "libddcutil: ", msg));
   SYSLOG2(DDCA_SYSLOG_NOTICE,"%s", msg);
}


//
// Initialization
//
static Error_Info *
get_parsed_libmain_config(const char * libopts_string,
                          bool         disable_config_file,
                          GPtrArray*   infomsgs,
                          Parsed_Cmd** parsed_cmd_loc)
{
   bool debug = false;
   DBGF(debug, "Starting. disable_config_file = %s, libopts_string = %s",
               sbool(disable_config_file), libopts_string);

   char * msg = g_strdup_printf("Options passed from client: %s",
                                (libopts_string) ? libopts_string : "");
   emit_parse_info_msg(msg, infomsgs);
   free(msg);

   Error_Info * result = NULL;
   *parsed_cmd_loc = NULL;

   char ** libopts_tokens = NULL;
   int libopts_token_ct = 0;
   if (libopts_string) {
      libopts_token_ct = tokenize_options_line(libopts_string, &libopts_tokens);
      DBGF(debug, "libopts_token_ct = %d, libopts_tokens=%p:", libopts_token_ct,libopts_tokens);
      if (debug)
         ntsa_show(libopts_tokens);
   }
   Null_Terminated_String_Array cmd_name_array = calloc(2 + libopts_token_ct, sizeof(char*));
   cmd_name_array[0] = strdup("libddcutil");   // so libddcutil not a special case for parser
   int ndx = 0;
   for (; ndx < libopts_token_ct; ndx++)
      cmd_name_array[ndx+1] = g_strdup(libopts_tokens[ndx]);
   cmd_name_array[ndx+1] = NULL;
   ntsa_free(libopts_tokens,true);

   DBGF(debug, "cmd_name_array=%p, cmd_name_array[1]=%p -> %s",
                cmd_name_array, cmd_name_array[0], cmd_name_array[0]);

   char ** new_argv = NULL;
   int     new_argc = 0;
   char *  untokenized_option_string = NULL;

   if (disable_config_file) {
      DBGF(debug, "config file disabled");
      new_argv = ntsa_copy(cmd_name_array, true);
      new_argc = ntsa_length(cmd_name_array);
      ntsa_free(cmd_name_array, true);
   }
   else {
      GPtrArray * errmsgs = g_ptr_array_new_with_free_func(g_free);
      char *  config_fn = NULL;
      DBGF(debug, "Calling apply_config_file()...");
      int apply_config_rc = apply_config_file(
                                    "libddcutil",  // use this section of config file
                                    ntsa_length(cmd_name_array), cmd_name_array,
                                    &new_argc,
                                    &new_argv,
                                    &untokenized_option_string,
                                    &config_fn,
                                    errmsgs);
      ntsa_free(cmd_name_array, true);
      assert(apply_config_rc <= 0);
      ASSERT_IFF(apply_config_rc == 0, errmsgs->len == 0);
      // DBGF(debug, "Calling ntsa_free(cmd_name_array=%p", cmd_name_array);

      DBGF(debug, "apply_config_file() returned: %d (%s), new_argc=%d, new_argv=%p:",
                  apply_config_rc, psc_desc(apply_config_rc), new_argc, new_argv);

      if (apply_config_rc == -EBADMSG) {
         result = errinfo_new(DDCRC_INVALID_CONFIG_FILE, __func__,
                              "Error(s) processing configuration file: %s", config_fn);
         for (int ndx = 0; ndx < errmsgs->len; ndx++) {
            errinfo_add_cause(result,
                  errinfo_new(DDCRC_INVALID_CONFIG_FILE, __func__, g_ptr_array_index(errmsgs, ndx)));
         }

      }
      // else if (apply_config_rc == -ENOENT) {
      //    result = errinfo_new(-ENOENT, __func__, "Configuration file not found");
      // }
      else if (apply_config_rc < 0) {
         result = errinfo_new(apply_config_rc, __func__,
                     "Unexpected error reading configuration file: %s", psc_desc(apply_config_rc));
      }
      else {
         assert( new_argc == ntsa_length(new_argv) );
         if (debug)
            ntsa_show(new_argv);

         if (untokenized_option_string && strlen(untokenized_option_string) > 0) {
            char * msg = g_strdup_printf("Using options from %s: %s",
                                         config_fn, untokenized_option_string);
            emit_parse_info_msg(msg, infomsgs);
            free(msg);
         }
      }
      g_ptr_array_free(errmsgs, true);
      free(config_fn);
   }

   if (!result) {   // if no errors
      assert(new_argc >= 1);
      char * combined = strjoin((const char**)(new_argv+1), new_argc, " ");
      char * msg = g_strdup_printf("Applying combined libddcutil options: %s", combined);
      emit_parse_info_msg(msg, infomsgs);
      free(msg);

      result = perform_parse(new_argc, new_argv, combined, parsed_cmd_loc);
      ntsa_free(new_argv, true);
      free(combined);
      free(untokenized_option_string);
   }

   DBGF(debug, "Done.     *parsed_cmd_loc=%p. Returning %s",
              *parsed_cmd_loc, errinfo_summary(result));

   ASSERT_IFF(*parsed_cmd_loc, !result);
   return result;
}


#ifdef TESTING_CLEANUP
void done() {
   printf("(%s) Starting\n", __func__);
   _ddca_terminate();
   SYSLOG(LOG_INFO, "(%s) executing done()", __func__);
   printf("(%s) Done.\n", __func__);
}

void dummy_sigterm_handler() {
   printf("(%s) Executing. library_initialized = %s\n",
         __func__, SBOOL(library_initialized));
}

void atexit_func() {
   printf("(%s) Executing. library_initialized = %s\n",
         __func__, SBOOL(library_initialized));
}
#endif


/** Initializes the ddcutil library module.
 *
 *  Called automatically when the shared library is loaded.
 *
 *  Registers functions in RTTI table, performs additional initialization
 *  that cannot fail.
 */
void  __attribute__ ((constructor))
_libddcutil_constructor(void) {
   bool debug = false;
   char * s = getenv("DDCUTIL_DEBUG_LIBINIT");
   if (s && strlen(s) > 0)
      debug = true;

   DBGF(debug, "Starting. library built %s at %s", BUILD_DATE, BUILD_TIME);
   detect_stdout_stderr_redirection();
   DBGF(debug, "stdout_stderr_redirected = %s", SBOOL(stdout_stderr_redirected));
   syslog(LOG_NOTICE, "Starting libddcutil. library built %s at %s. stdout_stderr_redirected=%s",
                      BUILD_DATE, BUILD_TIME, sbool(stdout_stderr_redirected));

   init_api_base();         // registers functions in RTTI table
   init_base_services();    // initializes tracing related modules
   init_ddc_services();     // initializes i2c, usb, ddc, vcp, dynvcp
#ifdef WATCH_DISPLAYS
   init_dw_services();      // initializes dw
#endif
   init_api_services();     // other files in directory libmain

   init_baseline_accumulated_sleep_ns();  // increase this value indicates a sleep has occurred

#ifdef TESTING_CLEANUP
   // int atexit_rc = atexit(done);   // TESTING CLEANUP
   // printf("(%s) atexit() returned %d\n", __func__, atexit_rc);
#endif

   DBGF(debug, "Done.");
}


//
// Profiling
//

void profiling_enable(bool enabled) {
   ptd_api_profiling_enabled = enabled;
}

void profiling_reset() {
   ptd_profile_reset_all_stats();
}

void profile_start_call(void * func) {
   ptd_profile_function_start(func);
}

void profile_end_call(void * func) {
   ptd_profile_function_end(func);
}

void profile_report(FILE * dest, bool by_thread) {
   if (dest) {
      rpt_push_output_dest(dest);
   }
   if (by_thread)
      ptd_profile_report_all_threads(0);
   ptd_profile_report_stats_summary(0);
   if (dest) {
      rpt_pop_output_dest();
   }
}


//
// Tracing
//

/** Collects all output that normally goes to the terminal and appends it in
 *  the specified file. The file is created if it does not already exist.
 *
 *  @param  library_trace_file  file in which to store the output.
 *  @param  debug               if true, issue debug messages
 *
 *  If the file name is not fully qualified, it is considered to be a
 *  subdirectory of the user's XDG state file, normally
 *  $HOME/.local/state/libddcutil.
 */
void
init_library_trace_file(char * library_trace_file, bool debug) {
   DBGF(debug, "library_trace_file = \"%s\"", library_trace_file);
   char * fq_trace_file = (library_trace_file[0] != '/')
          ? xdg_state_home_file("libddcutil", library_trace_file)
          : g_strdup(library_trace_file);

   fopen_mkdir(fq_trace_file, "a", stderr, &flog);
   if (flog) {
      DBGF(debug, "Writing %s trace output to %s", "libddcutil",fq_trace_file);
      syslog(LOG_NOTICE, "Trace destination: %s", fq_trace_file);
      time_t trace_start_time = time(NULL);
      char * trace_start_time_s = asctime(localtime(&trace_start_time));
      if (trace_start_time_s[strlen(trace_start_time_s)-1] == 0x0a)
           trace_start_time_s[strlen(trace_start_time_s)-1] = 0;
      fprintf(flog, "%s tracing started %s\n", "libddcutil", trace_start_time_s);

      set_default_thread_output_settings(flog, flog);
      set_fout(flog);
      set_ferr(flog);

      rpt_set_default_output_dest(flog);    // for future threads
      rpt_push_output_dest(flog);           // for this thread
   }
   else {
      fprintf(stderr, "Error opening libddcutil trace file %s: %s\n",
                      fq_trace_file, strerror(errno));
      syslog(LOG_ERR, "Error opening libddcutil trace file %s: %s",
                             fq_trace_file, strerror(errno));
   }
   free(fq_trace_file);
   DBGF(debug, "Done");
}


/** Cleanup at library termination
 *
 *  - Terminates thread that watches for display addition or removal.
 *  - Releases heap memory to avoid error reports from memory analyzers.
 */
void __attribute__ ((destructor))
_ddca_terminate(void) {
   bool debug = false;
   reset_current_traced_function_stack();  // ?? needed?
   DBGTRC_STARTING(debug, DDCA_TRC_API, "library_initialized = %s", SBOOL(library_initialized));
   if (library_initialized) {
      if (debug)
         dbgrpt_display_locks(2);
      if (dsa2_is_enabled())
         dsa2_save_persistent_stats();
      if (display_caching_enabled)
         ddc_store_displays_cache();
      ddc_discard_detected_displays();
      if (requested_stats)
         ddc_report_stats_main(requested_stats, per_display_stats, dsa_detail_stats, false, 0);
#ifdef WATCH_DISPLAYS
      DDCA_Display_Event_Class active_classes;
      if (dw_is_watch_displays_executing())
         dw_stop_watch_displays(/*wait=*/ true, &active_classes);   // in case it was started
      DBGTRC_NOPREFIX(debug, DDCA_TRC_API, "After ddc_stop_watch_displays");
      // sleep(5); // still needed?
      ldbus_stop_sleep_watch_thread();
      terminate_dw_services();
#endif
      terminate_ddc_services();
      terminate_base_services();
      free_regex_hash_table();

      library_initialized = false;
      if (flog)
         fclose(flog);
      DBGTRC_DONE(debug, DDCA_TRC_API, "library termination complete");
   }
   else {
      DBGTRC_DONE(debug, DDCA_TRC_API, "library was already terminated");   // should be impossible
   }
   // Frees the traced function stack for the main thread.
   // For created threads, is called at time of thread termination
   free_current_traced_function_stack();  // must come after last DBG... call

   // special handling for termination msg
   if (syslog_level > DDCA_SYSLOG_NEVER)
      syslog(LOG_NOTICE, "libddcutil terminating.");
   if (syslog_level > DDCA_SYSLOG_NEVER && !client_opened_syslog)
      closelog();
}


Error_Info *
set_master_errinfo_from_init_errors(
      GPtrArray * errs) // array of Error_Info *
{
   bool debug = false;
   DBGF(debug, "Starting. errs=%p", errs);
   Error_Info * master_error = NULL;
   if (errs && errs->len > 0) {
      master_error = errinfo_new(DDCRC_BAD_DATA, __func__, "Invalid configuration options");
      for (int ndx = 0; ndx < errs->len; ndx++) {
         Error_Info * cur = g_ptr_array_index(errs, ndx);
         errinfo_add_cause(master_error, cur);
      }
      g_ptr_array_free(errs, false);
   }
   DBGF(debug, "Done.  Returning %p", master_error);
   return master_error;
}


DDCA_Status
set_ddca_error_detail_from_init_errors(
      GPtrArray * errs) // array of Error_Info *
{
   bool debug = false;
   DDCA_Status ddcrc = 0;
   if (errs && errs->len > 0) {
      Error_Info * master_error = errinfo_new(DDCRC_BAD_DATA, __func__, "Invalid configuration options");
      ddcrc = DDCRC_BAD_DATA;
      for (int ndx = 0; ndx < errs->len; ndx++) {
         Error_Info * cur = g_ptr_array_index(errs, ndx);
         errinfo_add_cause(master_error, cur);
      }
      DDCA_Error_Detail * public_error_detail = error_info_to_ddca_detail(master_error);
      errinfo_free_with_report(master_error, debug, __func__);
      save_thread_error_detail(public_error_detail);
   }
   // clear if no errors?
   return ddcrc;
}


DDCA_Syslog_Level ddca_syslog_level_from_name(const char * name) {
   return syslog_level_name_to_value(name);
}


void report_parse_errors0(Error_Info * erec, int depth, int max_depth) {
   char * edesc = psc_text(erec->status_code);

   if (depth == 0)  {
      rpt_vstring(depth, "%s: %s", edesc, erec->detail);
   }
   else {
      rpt_vstring(depth, "%s", erec->detail);
   }
   if (depth < max_depth) {
      if (erec->cause_ct > 0) {
         for (int ndx = 0; ndx < erec->cause_ct; ndx++) {
            Error_Info * cur = erec->causes[ndx];
            report_parse_errors0(cur, depth+1, max_depth);
         }
      }
   }
}


void report_parse_errors(Error_Info * erec) {
   if (erec) {
      rpt_push_output_dest(ferr());
      report_parse_errors0(erec, 0, 3);
      rpt_pop_output_dest();
   }
}


DDCA_Status
ddci_init(const char *      libopts,
          DDCA_Syslog_Level syslog_level_arg,
          DDCA_Init_Options opts,
          char***           infomsg_loc)
{
   bool debug = false;
   char * s = getenv("DDCUTIL_DEBUG_LIBINIT");
   if (s && strlen(s) > 0)
      debug = true;

   char * s1 = g_strdup_printf("Starting. library built %s at %s, library_initialized=%s, libopts=|%s|",
         BUILD_DATE, BUILD_TIME, sbool(library_initialized), libopts);
   if (debug)
      DBG("%s", s1);
   else
      syslog(LOG_NOTICE, "%s", s1);
   free(s1);

   if (infomsg_loc)
      *infomsg_loc = NULL;

   execution_mode = MODE_LIBDDCUTIL;  // global in core.c
   Parsed_Cmd * parsed_cmd = NULL;
   Error_Info * master_error = NULL;
   DDCA_Status ddcrc = 0;
   enable_init_msgs = opts & DDCA_INIT_OPTIONS_ENABLE_INIT_MSGS;
   // enable_init_msgs = true;  // *** TEMP ***
   DBGF(debug, "enable_init_msgs=%s", SBOOL(enable_init_msgs));

   if (library_initialized) {
      master_error = ERRINFO_NEW(DDCRC_INVALID_OPERATION, "libddcutil already initialized");
      syslog(LOG_ERR, "libddcutil already initialized");
      goto bye;
   }

   client_opened_syslog = opts & DDCA_INIT_OPTIONS_CLIENT_OPENED_SYSLOG;
   DBGF(debug, "client_opened_syslog=%s, enable_syslog=%s",
         sbool(client_opened_syslog), sbool(enable_syslog));
   if (syslog_level_arg == DDCA_SYSLOG_NOT_SET)
      syslog_level_arg = DEFAULT_LIBDDCUTIL_SYSLOG_LEVEL;
   enable_syslog = (syslog_level_arg == DDCA_SYSLOG_NEVER) ? false : true;  // global in core.c
   DBGF(debug, "enable_syslog=%s", sbool(enable_syslog));

   if (enable_syslog) {
      if (!client_opened_syslog) {
         openlog("libddcutil",       // prepended to every log message
                 LOG_CONS | LOG_PID, // write to system console if error sending to system logger
                                     // include caller's process id
                 LOG_USER);          // generic user program, syslogger can use to determine how to handle
      }
      // special handling for start and termination msgs
      // always output if syslog is opened
      syslog(LOG_NOTICE, "Initializing libddcutil.  ddcutil version: %s, shared library: %s",
                   get_full_ddcutil_version(), ddci_libddcutil_filename());
      syslog(LOG_NOTICE, "                          library built %s at %s. stdout_stderr_redirected=%s",
                         BUILD_DATE, BUILD_TIME, sbool(stdout_stderr_redirected));
      syslog_level = syslog_level_arg;  // global in trace_control.h

      if (stdout_stderr_redirected) {
         // use tracing output for syslog for these functions:
         // add_traced_function("ddca_get_non_table_vcp_value");
         // add_traced_function("ddci_set_non_table_vcp_value_verify");
         add_traced_function("dw_start_watch_displays");
         add_traced_function("dw_stop_watch_displays");
         add_traced_function("dw_hotplug_change_handler");
         add_traced_function("dw_emit_display_status_record");
         add_traced_function("i2c_open_bus_basic");
      }
   }

   DBGF(debug, "syslog_level_arg = %s, syslog_level=%s, enable_syslog=%s",
               syslog_level_name(syslog_level_arg), syslog_level_name(syslog_level), sbool(enable_syslog));

   GPtrArray* infomsgs = g_ptr_array_new_with_free_func(g_free);

   if ((opts & DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE) && !libopts) {
      parsed_cmd = new_parsed_cmd();
   }
   else {
      master_error = get_parsed_libmain_config(
                        libopts,
                        opts & DDCA_INIT_OPTIONS_DISABLE_CONFIG_FILE,
                        infomsgs,
                        &parsed_cmd);
      ASSERT_IFF(master_error, !parsed_cmd);

      if (infomsgs->len > 0) {
         DBGF(debug, "emit infomsgs starting. enable_init_msgs=%s, stdout_stderr_redirected=%s, infomsgs->len=%d",
               sbool(enable_init_msgs), sbool(stdout_stderr_redirected), infomsgs->len);
         for (int ndx = 0; ndx < infomsgs->len; ndx++) {
            if (enable_init_msgs && !stdout_stderr_redirected)
               fprintf(fout(), "%s\n", (char*) g_ptr_array_index(infomsgs, ndx));
            // already done in emit_parse_info_msg():
            // syslog(LOG_NOTICE, "%s", (char*) g_ptr_array_index(infomsgs, ndx));
         }
         DBGF(debug, "emit infomsgs done");

         if (infomsg_loc) {
            *infomsg_loc = g_ptr_array_to_ntsa(infomsgs, /*duplicate=*/true);
         }
      }
   }
   g_ptr_array_free(infomsgs, true);
   DBGF(debug, "parsing complete");

   if (!master_error) {
      if (parsed_cmd->trace_destination) {
         DBGF(debug, "Setting library trace file: %s", parsed_cmd->trace_destination);
         init_library_trace_file(parsed_cmd->trace_destination, debug);
      }
      master_error = init_tracing(parsed_cmd);
      if (master_error) {
         DBGF(debug, "init_tracing failed");
        //  free_parsed_cmd(parsed_cmd);
      }
      else
         DBGF(debug, "init_tracing succeeded");
   }

   if (!master_error) {
      DBGF(true, "Calling ldbus_start_sleep_watch_thread...");
      bool ok = ldbus_start_sleep_watch_thread();   // right location?
      if (!ok) {
         DBGF(true, "ldbus_start_sleep_watch_thread() failed");
         // for now, don't set master_error
      }
   }

   if (!master_error) {
      requested_stats = parsed_cmd->stats_types;
      ptd_api_profiling_enabled = parsed_cmd->flags & CMD_FLAG_PROFILE_API;
      per_display_stats = parsed_cmd->flags & CMD_FLAG_VERBOSE_STATS;
      dsa_detail_stats = parsed_cmd->flags & CMD_FLAG_INTERNAL_STATS;
      Error_Info * submaster_status = submaster_initializer(parsed_cmd);
      if (submaster_status) {
         master_error = ERRINFO_NEW(DDCRC_UNINITIALIZED, "Initialization failed");
         errinfo_add_cause(master_error, submaster_status);
      }
   }

   assert(master_error || parsed_cmd);  // avoid null-dereference warning

   if (master_error) {
      syslog(LOG_CRIT, "Library initialization failed: %s", psc_desc(master_error->status_code));
      for (int ndx = 0; ndx < master_error->cause_ct; ndx++) {
         syslog(LOG_CRIT, "%s", master_error->causes[ndx]->detail);
      }

      if (enable_init_msgs) {
         DBGF(debug, "Calling report_parse_errors()");
         report_parse_errors(master_error);
      }
      // errinfo_free(master_error);
      library_initialization_failed = true;
      goto bye;
   }

   if (library_disabled) {
      DBGF(debug, "libddcutil disabled");
      master_error = ERRINFO_NEW(DDCRC_INVALID_OPERATION, "libddcutil disabled");
      syslog(LOG_ERR, "libddcutil disabled");
      library_initialization_failed = true;
      goto bye;
   }

    // i2c_detect_buses();  // needed by i2c_all_relevant_i2c_buses_rw()

   if (!enable_ddci_init_check_dev_i2c_devices_rw) {
      syslog(LOG_DEBUG, "Suppressing call to i2c_all_relevant_i2c_buses_rw()");
   }
   else {
      master_error = i2c_all_relevant_i2c_buses_rw();
      if (master_error) {
         library_initialization_failed = true;
         // syslog (LOG_ERR, "Not all relevant i2c buses rw");
         // errinfo_report_to_syslog(LOG_ERR, master_err, 1);
         goto bye;
      }
   }

   DBGF(debug, "performing display detection ...");

   ddc_ensure_displays_detected();  // calls i2c_detect_buses() if necessary
#ifdef OUT
      if (parsed_cmd->flags&CMD_FLAG_WATCH_DISPLAY_HOTPLUG_EVENTS) {
         dw_start_watch_displays(DDCA_EVENT_CLASS_DISPLAY_CONNECTION | DDCA_EVENT_CLASS_DPMS);
         SYSLOG2(DDCA_SYSLOG_NOTICE,
               "Started watch displays for DDCA_EVENT_CLASS_DISPLAY_CONNECTION | DDCA_EVENT_CLASS_DPMS");
   }
#endif
   library_initialized = true;
   library_initialization_failed = false;
   syslog(LOG_NOTICE, "Library initialization complete.");

bye:
   if (parsed_cmd)
      free_parsed_cmd(parsed_cmd);
   if (master_error) {
      SIMPLE_STD_FUNC_SYSLOG(LOG_ERR, "Returning %s", psc_name(master_error->status_code));
      errinfo_report_to_syslog(LOG_ERR, master_error, 2);
      ddcrc = master_error->status_code;
      DDCA_Error_Detail * public_error_detail = error_info_to_ddca_detail(master_error);
      save_thread_error_detail(public_error_detail);
      errinfo_free(master_error);
   }
   else {
      SIMPLE_STD_FUNC_SYSLOG(LOG_NOTICE, "Done.  Returning 0");
   }
   DBGF(debug, "Done.    Returning: %s", psc_desc(ddcrc));
   return ddcrc;
}


DDCA_Status
ddca_init(const char *      libopts,
          DDCA_Syslog_Level syslog_level_arg,
          DDCA_Init_Options opts)
{
   return ddci_init(libopts, syslog_level_arg, opts, NULL);
}


DDCA_Status
ddca_init2(const char *     libopts,
          DDCA_Syslog_Level syslog_level_arg,
          DDCA_Init_Options opts,
          char***           infomsg_loc
          )
{
   return ddci_init(libopts, syslog_level_arg, opts, infomsg_loc);
}


DDCA_Status
ddca_start_watch_displays(DDCA_Display_Event_Class enabled_classes) {
   bool debug = false;
   API_PROLOGX(debug, RESPECT_QUIESCE, "enabled_classes=0x%02x=%s",
                      enabled_classes, dw_event_classes_repr_t(enabled_classes));

   SYSLOG2(DDCA_SYSLOG_NOTICE, "Starting ddca_start_watch_displays()");

#ifdef WATCH_DISPLAYS
   DBGTRC_NOPREFIX(debug, DDCA_TRC_API, "all_video_adapters_implement_drm=%s",
         sbool(all_video_adapters_implement_drm));

#ifdef OUT
   if (enabled_classes == DDCA_EVENT_CLASS_ALL)
      enabled_classes = DDCA_EVENT_CLASS_DISPLAY_CONNECTION;
   else
#endif
	   if (enabled_classes&DDCA_EVENT_CLASS_DPMS) {
        // edet = new_ddca_error_detail(DDCRC_UNIMPLEMENTED, "Watching for DPMS state changes unimplemented");
  	   // DBGMSG("Watching for DPMS state changes unimplemented");
  	   MSG_W_SYSLOG(DDCA_SYSLOG_WARNING, "Watching for DPMS state changes unimplemented");
        enabled_classes &=~DDCA_EVENT_CLASS_DPMS;
     }

   DDCA_Error_Detail * edet = NULL;
#ifdef ENABLE_UDEV
   if (!all_video_adapters_implement_drm) {
      edet = new_ddca_error_detail(DDCRC_INVALID_OPERATION,
               "Display hotplug detection requires DRM enabled video drivers");
   }
   else if (enabled_classes == DDCA_EVENT_CLASS_NONE) {
      edet = new_ddca_error_detail(DDCRC_ARG, "No event class specified");
   }
   else if (enabled_classes&DDCA_EVENT_CLASS_DPMS) {
      edet = new_ddca_error_detail(DDCRC_UNIMPLEMENTED, "Watching for DPMS state changes unimplemented");
	  // MSG_W_SYSLOG(DDCA_SYSLOG_WARNING, "Watching for DPMS state changes unimplemented");
      //enabled_classes &=~DDCA_EVENT_CLASS_DPMS;
   }
   else if (enabled_classes != DDCA_EVENT_CLASS_DISPLAY_CONNECTION) {
      edet = new_ddca_error_detail (DDCRC_ARG, "Invalid event class specified");
   }
   else {
      Error_Info * erec = dw_start_watch_displays(enabled_classes);
      edet = error_info_to_ddca_detail(erec);
      ERRINFO_FREE(erec);
   }
#else
   edet = new_ddca_error_detail(DDCRC_INVALID_OPERATION, "Display change detection requires UDEV");
#endif

   DDCA_Status ddcrc = 0;
   if (edet) {
      ddcrc = edet->status_code;
      save_thread_error_detail(edet);
   }
#else
   DDCA_Status ddcrc = DDCRC_UNIMPLEMENTED;
#endif

   SYSLOG2(DDCA_SYSLOG_NOTICE,  "Done     ddca_start_watch_displays returning %s - %s", psc_name(ddcrc), psc_desc(ddcrc));

   API_EPILOG_RET_DDCRC(debug, RESPECT_QUIESCE, ddcrc, "");
}


DDCA_Status
ddca_stop_watch_displays(bool wait) {
   SYSLOG2(DDCA_SYSLOG_NOTICE, "Starting ddca_stop_watch_displays()");
   bool debug = false;
   API_PROLOGX(debug, NORESPECT_QUIESCE, "wait=%s", SBOOL(wait));
#ifdef WATCH_DISPLAYS
   DDCA_Display_Event_Class active_classes;
   DDCA_Status ddcrc = dw_stop_watch_displays(wait, &active_classes);
#else
   DDCA_Status ddcrc = DDCRC_UNIMPLEMENTED;
#endif
   SYSLOG2(DDCA_SYSLOG_NOTICE,  "Done     ddca_stop_watch_displays returning %s - %s", psc_name(ddcrc), psc_desc(ddcrc));
   API_EPILOG_RET_DDCRC(debug, NORESPECT_QUIESCE, ddcrc, "");
}


DDCA_Status
ddca_get_active_watch_classes(DDCA_Display_Event_Class * classes_loc) {
   bool debug = false;
   API_PROLOGX(debug, NORESPECT_QUIESCE, "Starting classes_loc=%p", classes_loc);
   DDCA_Status ddcrc = 0;
#ifdef WATCH_DISPLAYS
   if (classes_loc)
      ddcrc = dw_get_active_watch_classes(classes_loc);
   else
      ddcrc = DDCRC_ARG;
#else
   ddcrc = DDCRC_UNIMPLEMENTED;
#endif
   char * s = (classes_loc)
                 ? g_strdup_printf("*classes_loc=0x%02x=%s",
                     *classes_loc, (*classes_loc) ? dw_event_classes_repr_t(*classes_loc):"NULL" )
                 : g_strdup("");

   API_EPILOG_BEFORE_RETURN(debug, NORESPECT_QUIESCE, ddcrc, "%s", s);
   free(s);
   return ddcrc;
}

DDCA_Status
ddca_get_display_watch_settings(DDCA_DW_Settings * settings_buffer) {
   bool debug = false;
   API_PROLOGX(debug, NORESPECT_QUIESCE, "Starting");

#ifdef WATCH_DISPLAYS
   DDCA_Status ddcrc = DDCRC_OK;
   if (!settings_buffer)
      ddcrc = DDCRC_ARG;
   else
      dw_get_display_watch_settings(settings_buffer);
#else
   DDCA_Status ddcrc = DDCRC_UNIMPLEMENTED;
#endif

   API_EPILOG_RET_DDCRC(debug, NORESPECT_QUIESCE, ddcrc, "Done");
}


DDCA_Status
ddca_set_display_watch_settings(DDCA_DW_Settings * settings_buffer) {
   bool debug = false;
   API_PROLOGX(debug, NORESPECT_QUIESCE, "Starting");

#ifdef WATCH_DISPLAYS
   DDCA_Status ddcrc = DDCRC_ARG;
   if (settings_buffer)
      ddcrc = dw_set_display_watch_settings(settings_buffer);
#else
   DDCA_Status ddcrc = DDCRC_UNIMPLEMENTED;
#endif
   API_EPILOG_RET_DDCRC(debug, NORESPECT_QUIESCE, ddcrc, "Done");
}


//
// Error Detail
//

DDCA_Error_Detail *
ddca_get_error_detail() {
   bool debug = false;
   DBGMSF(debug, "Starting");

   DDCA_Error_Detail * result = dup_error_detail(get_thread_error_detail());

   if (debug) {
      DBGMSG("Done.     Returning: %p", result);
      if (result)
         report_error_detail(result, 2);
   }
   return result;
}


void
ddca_free_error_detail(DDCA_Error_Detail * ddca_erec) {
   free_error_detail(ddca_erec);
}


void
ddca_report_error_detail(DDCA_Error_Detail * ddca_erec, int depth) {
   report_error_detail(ddca_erec, depth);
}


// DDCA_Error_Detail * ddca_dup_error_detail(DDCA_Error_Detail * original) {
//     return dup_error_detail(original);
// }

// void ddci_error_detail_to_syslog() {
//    int depth = 2;
//    report_error_detail(get_thread_error_detail(), depth);
// }


//
// Status Code Management
//

const char *
ddca_rc_name(DDCA_Status status_code) {
   char * result = NULL;
   Status_Code_Info * code_info = find_status_code_info(status_code);
   if (code_info)
      result = code_info->name;
   return result;
}


const char *
ddca_rc_desc(DDCA_Status status_code) {
   char * result = "unknown status code";
   Status_Code_Info * code_info = find_status_code_info(status_code);
   if (code_info)
      result = code_info->description;
   return result;
}


//
// Output redirection
//

// Redirects output that normally would go to STDOUT
void
ddca_set_fout(FILE * fout) {
   // DBGMSG("Starting. fout=%p", fout);
   set_fout(fout);
}


void
ddca_set_fout_to_default(void) {
   set_fout_to_default();
}


// Redirects output that normally would go to STDERR
void
ddca_set_ferr(FILE * ferr) {
   set_ferr(ferr);
}


void
ddca_set_ferr_to_default(void) {
   set_ferr_to_default();
}


//
// Output capture - convenience functions
//

void
ddca_start_capture(DDCA_Capture_Option_Flags flags) {
   bool debug = false;
   DBGF(debug, "flags=0x%02x", flags);
   start_capture(flags);
}


char *
ddca_end_capture(void) {
   bool debug = false;

   char * result = end_capture();

   DBGF(debug, "Returning %p", result);
   return result;
}



//
// Message Control
//

DDCA_Output_Level
ddca_get_output_level(void) {
   return get_output_level();
}


DDCA_Output_Level
ddca_set_output_level(DDCA_Output_Level newval) {
     return set_output_level(newval);
}


char *
ddca_output_level_name(DDCA_Output_Level val) {
   return output_level_name(val);
}


//
// Global Settings
//

#ifdef REMOVED
int
ddca_max_max_tries(void) {
   return MAX_MAX_TRIES;
}


//  *** THIS IS FOR THE CURRENT THREAD
//  *** replace using function specifying display
//  *** for now, revert to old try_data_get_maxtries2()
int
ddca_get_max_tries(DDCA_Retry_Type retry_type) {
   // stats for multi part writes and reads are separate, but the
   // max tries for both are identical
// #ifndef NDEBUG
   Retry_Op_Value result3 = try_data_get_maxtries2((Retry_Operation) retry_type);
// #endif
   // // new way using retry_mgt
   // Retry_Op_Value result2 = trd_get_thread_max_tries((Retry_Operation) retry_type);
   // assert(result == result2);
   // assert(result2 == result3);
   return result3;
}


// ** THIS IS FOR CURRENT THREAD - FIX
DDCA_Status
ddca_set_max_tries(
      DDCA_Retry_Type retry_type,
      int             max_tries)
{
   DDCA_Status rc = 0;
   free_thread_error_detail();
   if (max_tries < 1 || max_tries > MAX_MAX_TRIES)
      rc = DDCRC_ARG;
   else {
      try_data_set_maxtries2((Retry_Operation) retry_type, max_tries);
      // for DDCA_MULTI_PART_TRIES, set both  MULTI_PART_WRITE_OP and MULTI_PART_READ_OP
      if (retry_type == DDCA_MULTI_PART_TRIES)
         try_data_set_maxtries2(MULTI_PART_WRITE_OP, max_tries);

      // new way, set in retry_mgt
#ifdef TRD
      trd_set_thread_max_tries((Retry_Operation) retry_type, max_tries);
      if (retry_type == DDCA_MULTI_PART_TRIES)
           trd_set_thread_max_tries(MULTI_PART_WRITE_OP, max_tries);
#endif
   }
   return rc;
}
#endif

bool
ddca_enable_verify(bool onoff) {
   return ddc_set_verify_setvcp(onoff);
}


bool
ddca_is_verify_enabled() {
   return ddc_get_verify_setvcp();
}


#ifdef REMOVED

// *** FOR CURRENT THREAD
double
ddca_set_default_sleep_multiplier(double multiplier)
{
   bool debug = false;
   DBGTRC_STARTING(debug, DDCA_TRC_API, "Setting multiplier = %6.3f", multiplier);

   double old_value = -1.0;
   if (multiplier >= 0.0 && multiplier <= 10.0) {
// #ifdef TSD
      old_value = pdd_get_default_sleep_multiplier_factor();
      pdd_set_default_sleep_multiplier_factor(multiplier, Reset);
// #endif
    }

   DBGTRC_DONE(debug, DDCA_TRC_API, "Returning: %6.3f", old_value);
   return old_value;
}


double
ddca_get_default_sleep_multiplier()
{
   bool debug = false;
   DBGTRC_STARTING(debug, DDCA_TRC_API, "");
   double result = pdd_get_default_sleep_multiplier_factor();
   DBGTRC(debug, DDCA_TRC_API, "Returning %6.3f", result);
   return result;
}


void
ddca_set_global_sleep_multiplier(double multiplier)
{
   ddca_set_default_sleep_multiplier(multiplier);
   return;
}

double
ddca_get_global_sleep_multiplier()
{
   return ddca_get_default_sleep_multiplier();
}
#endif



// for display on current thread
double
ddca_set_sleep_multiplier(double multiplier)
{
   bool debug = false;
   if (traced_function_stack_enabled)
      reset_current_traced_function_stack();
   DBGTRC_STARTING(debug, DDCA_TRC_API, "Setting multiplier = %6.3f", multiplier);

   double old_value = -1.0;
   if (multiplier >= 0.0 && multiplier <= 10.0) {
      Per_Thread_Data * ptd = ptd_get_per_thread_data();
      if (ptd->cur_dh) {
         Per_Display_Data * pdd = ptd->cur_dh->dref->pdd;
         old_value = pdd->user_sleep_multiplier;
         pdd_reset_multiplier(pdd, multiplier);
      }
   }

   DBGTRC_DONE(debug, DDCA_TRC_API, "Returning: %6.3f", old_value);
   return old_value;
}

double
ddca_get_sleep_multiplier()
{
   bool debug = false;
   if (traced_function_stack_enabled)
      reset_current_traced_function_stack();
   DBGTRC(debug, DDCA_TRC_API, "");

   Per_Thread_Data * ptd = ptd_get_per_thread_data();
   double result =  -1.0f;
   if (ptd->cur_dh) {
      Per_Display_Data * pdd = ptd->cur_dh->dref->pdd;
      result = pdd->user_sleep_multiplier;
   }
#ifdef TSD
   result = tsd_get_sleep_multiplier_factor();
#endif
   DBGTRC(debug, DDCA_TRC_API, "Returning %6.3f", result);
   return result;
}

#ifdef RELEASE_2_1_0
double
ddca_set_sleep_multiplier(double multiplier)
{
   bool debug = false;
   DBGTRC_STARTING(debug, DDCA_TRC_API, "Setting multiplier = %6.3f", multiplier);

   double old_value = -1.0;
   if (multiplier >= 0.0 && multiplier <= 10.0) {
      Per_Thread_Data * ptd = ptd_get_per_thread_data();
      old_value = ptd->sleep_multiplier;
      ptd->sleep_multiplier = multiplier;
   }

   DBGTRC_DONE(debug, DDCA_TRC_API, "Returning: %6.3f", old_value);
   return old_value;
}

double
ddca_get_sleep_multiplier()
{
   bool debug = false;
   DBGTRC(debug, DDCA_TRC_API, "");

   Per_Thread_Data * ptd = ptd_get_per_thread_data();
   double result = ptd->sleep_multiplier;

   DBGTRC(debug, DDCA_TRC_API, "Returning %6.3f", result);
   return result;
}
#endif

#ifdef FUTURE

/** Gets the I2C timeout in milliseconds for the specified timeout class.
 * @param timeout_type timeout type
 * @return timeout in milliseconds
 */
int
ddca_get_timeout_millis(
      DDCA_Timeout_Type timeout_type) {
   return 0;    // *** UNIMPLEMENTED ***
}

/** Sets the I2C timeout in milliseconds for the specified timeout class
 * @param timeout_type  timeout class
 * @param millisec      timeout to set, in milliseconds
 */
void
ddca_set_timeout_millis(
      DDCA_Timeout_Type timeout_type,
      int               millisec)
{
   // *** UNIMPLEMENTED
}
#endif


#ifdef REMOVED

/** Controls the force I2C slave address setting.
 *
 *  Normally, ioctl operation I2C_SLAVE is used to set the I2C slave address.
 *  If that returns EBUSY and this setting is in effect, slave address setting
 *  is retried using operation I2C_SLAVE_FORCE.
 *
 *  @param[in] onoff true/false
 *  @return  prior value
 *  @since 1.2.2
 */
bool
ddca_enable_force_slave_address(bool onoff);

/** Query the force I2C slave address setting.
 *
 *  @return true/false
 *  @since 1.2.2
 */
bool
ddca_is_force_slave_address_enabled(void);
#endif

#ifdef REMOVED
bool
ddca_enable_force_slave_address(bool onoff) {
   bool old = i2c_forceable_slave_addr_flag;
   i2c_forceable_slave_addr_flag = onoff;
   return old;
}


bool
ddca_is_force_slave_address_enabled(void) {
   return i2c_forceable_slave_addr_flag;
}
#endif


//
// Statistics
//

// TODO: Add functions to access ddcutil's runtime error statistics

void
ddca_reset_stats(void) {
   // DBGMSG("Executing");
   g_mutex_lock(&api_quiesced_mutex);
   g_mutex_lock(&active_calls_mutex);

   ddc_reset_stats_main();
   max_active_calls = 0;

   g_mutex_unlock(&active_calls_mutex);
   g_mutex_unlock(&api_quiesced_mutex);
}


// TODO: Functions that return stats in data structures
void
ddca_show_stats(
      DDCA_Stats_Type stats_types,
      bool            per_display_stats,
      int             depth)
{
   bool debug = false;
   API_PROLOG_NO_DISPLAY_IO(debug, "stats_types=0x%02x, per_display_stats=%s",
         stats_types, SBOOL(per_display_stats) );
   if (stats_types) {
      ddc_report_stats_main( stats_types, per_display_stats, per_display_stats, false, depth);
      rpt_nl();
   }

   rpt_vstring(0, "Max concurrent API calls: %d", max_active_calls);
#ifdef REDUNDANT
   if (stats_types & DDCA_STATS_API) {
      if (ptd_api_profiling_enabled) {
         rpt_nl();
         profile_report(NULL, false);  // redundant
      }
   }
#endif
   API_EPILOG_NO_RETURN(debug, NORESPECT_QUIESCE, "");
}

void
ddca_report_locks(
      int             depth)
{
   dbgrpt_display_locks(depth);
}


void init_api_base() {
   // DBGMSG("Executing");
   RTTI_ADD_FUNC(ddci_init);
   RTTI_ADD_FUNC(_ddca_terminate);
   RTTI_ADD_FUNC(ddca_start_watch_displays);
   RTTI_ADD_FUNC(ddca_stop_watch_displays);
   RTTI_ADD_FUNC(ddca_get_active_watch_classes);
   RTTI_ADD_FUNC(ddca_start_capture);
   RTTI_ADD_FUNC(ddca_end_capture);
   RTTI_ADD_FUNC(quiesce_api);
   RTTI_ADD_FUNC(unquiesce_api);
   RTTI_ADD_FUNC(increment_active_api_calls);
   RTTI_ADD_FUNC(decrement_active_api_calls);
#ifdef REMOVED
   RTTI_ADD_FUNC(ddca_set_sleep_multiplier);
   RTTI_ADD_FUNC(ddca_set_default_sleep_multiplier);
#endif
}

