#ifndef R_CLI_PROGRESS_H
#define R_CLI_PROGRESS_H

#include <R_ext/Rdynload.h>
#include <stdarg.h>

#ifdef __cplusplus
extern "C" {
#endif

// ----------------------------------------------------------------------
// Public API
// ----------------------------------------------------------------------

//' ### `CLI_SHOULD_TICK`
//'
//' A macro that evaluates to (int) 1 if a cli progress bar update is due,
//' and to (int) 0 otherwise. If the timer hasn't been initialized in this
//' compilation unit yet, then it is always 0. To initialize the timer,
//' call `cli_progress_init_timer()` or create a progress bar with
//' `cli_progress_bar()`.

#define CLI_SHOULD_TICK

//' ### `cli_progress_add()`
//'
//' ```c
//' void cli_progress_add(SEXP bar, double inc);
//' ```
//'
//' Add a number of progress units to the progress bar. It will also
//' trigger an update if an update is due.
//'
//' * `bar`: progress bar object.
//' * `inc`: progress increment.

static R_INLINE void cli_progress_add(SEXP bar, double inc);

//' ### `cli_progress_bar()`
//'
//' ```c
//' SEXP cli_progress_bar(double total, SEXP config);
//' ```
//'
//'  Create a new progress bar object. The returned progress bar object
//'  must be `PROTECT()`-ed.
//'
//' * `total`: Total number of progress units. Use `NA_REAL` if it is not
//'   known.
//' * `config`: R named list object of additional parameters. May be `NULL`
//'   (the C `NULL~) or `R_NilValue` (the R `NULL`) for the defaults.
//'
//' `config` may contain the following entries:
//'
//' * `name`: progress bar name.
//' * `status`: (initial) progress bar status.
//' * `type`: progress bar type.
//' * `total`: total number of progress units.
//' * `show_after`: show the progress bar after the specified number of
//'    seconds. This overrides the global `show_after` option.
//' * `format`: format string, must be specified for custom progress bars.
//' * `format_done`: format string for successful termination.
//' * `format_failed`: format string for unsuccessful termination.
//' * `clear`: whether to remove the progress bar from the screen after
//'   termination.
//' * `auto_terminate`: whether to terminate the progress bar when the
//'   number of current units equals the number of total progress units.
//'
//' #### Example
//'
//' ```c
//' #include <cli/progress.h>
//' SEXP progress_test1() {
//'   int i;
//'   SEXP bar = PROTECT(cli_progress_bar(1000, NULL));
//'   for (i = 0; i < 1000; i++) {
//'     cli_progress_sleep(0, 4 * 1000 * 1000);
//'     if (CLI_SHOULD_TICK) cli_progress_set(bar, i);
//'   }
//'   cli_progress_done(bar);
//'   UNPROTECT(1);
//'   return Rf_ScalarInteger(i);
//' }
//' ```

static R_INLINE SEXP cli_progress_bar(double total, SEXP config);

//' ### `cli_progress_done()`
//'
//' ```c
//' void cli_progress_done(SEXP bar);
//' ```
//'
//' Terminate the progress bar.
//'
//' * `bar`: progress bar object.

static R_INLINE void cli_progress_done(SEXP bar);

//' ### `cli_progress_init_timer()`
//'
//' ```c
//' void cli_progress_init_timer();
//' ```
//'
//' Initialize the cli timer without creating a progress bar.

static R_INLINE void cli_progress_init_timer();

//' ### `cli_progress_num()`
//'
//' ```c
//' int cli_progress_num();
//' ```
//'
//' Returns the number of currently active progress bars.

static R_INLINE int cli_progress_num();

//' ### `cli_progress_set()`
//'
//' ```c
//' void cli_progress_set(SEXP bar, double set);
//' ```
//'
//' Set the progress bar to the specified number of progress units.
//'
//' * `bar`: progress bar object.
//' * `set`: number of current progress progress units.

static R_INLINE void cli_progress_set(SEXP bar, double set);

//' ### `cli_progress_set_clear()`
//'
//' ```c
//' void cli_progress_set_clear(SEXP bar, int clear);
//' ```
//'
//' Set whether to remove the progress bar from the screen. You can call
//' this any time before `cli_progress_done()` is called.
//'
//' * `bar`: progress bar object.
//' * `clear`: whether to remove the progress bar from the screen, zero or
//'   one.

static R_INLINE void cli_progress_set_clear(SEXP bar, int clear);

//' ### `cli_progress_set_format()`
//'
//' ```c
//' void cli_progress_set_format(SEXP bar, const char *format, ...);
//' ```
//'
//' Set a custom format string for the progress bar. This call does not
//' try to update the progress bar. If you want to request an update,
//' call `cli_progress_add()`, `cli_progress_set()` or
//' `cli_progress_update()`.
//'
//' * `bar`: progress bar object.
//' * `format`: format string.
//' * `...`: values to substitute into `format`.
//'
//' `format` and `...` are passed to `vsnprintf()` to create a format
//' string.
//'
//' Format strings may contain glue substitutions, referring to
//' [progress variables][progress-variables], pluralization, and cli
//' styling.
//'
//' [progress-variables]: https://cli.r-lib.org/dev/reference/progress-variables.html

static R_INLINE void cli_progress_set_format(SEXP bar, const char *format, ...);

//' ### `cli_progress_set_name()`
//'
//' ```c
//' void cli_progress_set_name(SEXP bar, const char *name);
//' ```
//'
//' Set the name of the progress bar.
//'
//' * `bar`; progress bar object.
//' * `name`: progress bar name.

static R_INLINE void cli_progress_set_name(SEXP bar, const char *name);

//' ### `cli_progress_set_status()`
//'
//' ```c
//' void cli_progress_set_status(SEXP bar, const char *status);
//' ```
//'
//' Set the status of the progress bar.
//'
//' * `bar`: progress bar object.
//' * `status `: progress bar status.

static R_INLINE void cli_progress_set_status(SEXP bar, const char *status);

//' ### `cli_progress_set_type()`
//'
//' ```c
//' void cli_progress_set_type(SEXP bar, const char *type);
//' ```
//'
//' Set the progress bar type. Call this function right after creating
//' the progress bar with `cli_progress_bar()`. Otherwise the behavior is
//' undefined.
//'
//' * `bar`: progress bar object.
//' * `type`: progress bar type. Possible progress bar types:
//'   `iterator`, `tasks`, `download` and `custom`.

static R_INLINE void cli_progress_set_type(SEXP bar, const char *type);

//' ### `cli_progress_update()`
//'
//' ```c
//' void cli_progress_update(SEXP bar, double set, double inc, int force);
//' ```
//'
//' Update the progress bar. Unlike the simpler `cli_progress_add()` and
//' `cli_progress_set()` function, it can force an update if `force` is
//' set to 1.
//'
//' * `bar`: progress bar object.
//' * `set`: the number of current progress units. It is ignored if
//'   negative.
//' * `inc`: increment to add to the current number of progress units.
//'   It is ignored if `set` is not negative.
//' * `force`: whether to force an update, even if no update is due.
//'
//' To force an update without changing the current number of progress units,
//' supply `set = -1`, `inc = 0` and `force = 1`.

static R_INLINE void cli_progress_update(SEXP bar, double set, double inc, int force);

// ----------------------------------------------------------------------
// Internals
// ----------------------------------------------------------------------

typedef volatile int vint;

static vint cli__false = 0;
static vint *cli__should_tick = &cli__false;

#ifndef __has_builtin         // Optional of course.
  #define __has_builtin(x) 0  // Compatibility with non-clang compilers.
#endif

#if __has_builtin (__builtin_expect)
#   define CLI_UNLIKELY(a) __builtin_expect((a), 0)
#   define CLI_LIKELY(a)   __builtin_expect((a), 1)
# else
#   define CLI_UNLIKELY(a) a
#   define CLI_LIKELY(a)   a
#endif

#undef CLI_SHOULD_TICK
#define CLI_SHOULD_TICK (CLI_UNLIKELY(*cli__should_tick))

static R_INLINE void cli_progress_done(SEXP bar) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP)) R_GetCCallable("cli", "cli_progress_done");
  }
  ptr(bar);
}

#ifdef R_CLEANCALL_SUPPORT
static void cli_progress_done2(SEXP bar) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP)) R_GetCCallable("cli", "cli_progress_done");
  }
  ptr(bar);
}
#endif

static R_INLINE void cli_progress_init_timer() {
  static void (*ptr)(vint **) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(vint **)) R_GetCCallable("cli", "cli_progress_init_timer");
  }
  ptr(&cli__should_tick);
}

static R_INLINE SEXP cli_progress_bar(double total, SEXP config) {
  static SEXP (*ptr)(vint **, double, SEXP) = NULL;
  if (ptr == NULL) {
    ptr = (SEXP (*)(vint **, double, SEXP)) R_GetCCallable("cli", "cli_progress_bar");
  }

  SEXP bar = PROTECT(ptr(&cli__should_tick, total, config));

#ifdef R_CLEANCALL_SUPPORT
  if (r_cleancall_is_active()) {
    r_call_on_early_exit((void (*)(void *)) cli_progress_done2, (void*) bar);
  }
#endif

  UNPROTECT(1);
  return bar;
}

static R_INLINE void cli_progress_set_name(SEXP bar, const char *name) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP, const char*) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP, const char*))
      R_GetCCallable("cli", "cli_progress_set_name");
  }
  ptr(bar, name);
}

static R_INLINE void cli_progress_set_status(SEXP bar, const char *status) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP, const char*) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP, const char*))
      R_GetCCallable("cli", "cli_progress_set_status");
  }
  ptr(bar, status);
}

static R_INLINE void cli_progress_set_type(SEXP bar, const char *type) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP, const char*) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP, const char*))
      R_GetCCallable("cli", "cli_progress_set_type");
  }
  ptr(bar, type);
}

static R_INLINE void cli_progress_set_clear(SEXP bar, int clear) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP, int) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP, int))
      R_GetCCallable("cli", "cli_progress_set_clear");
  }
  ptr(bar, clear);
}

static R_INLINE void cli_progress_set(SEXP bar, double set) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP, double) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP, double)) R_GetCCallable("cli", "cli_progress_set");
  }
  ptr(bar, set);
}

static R_INLINE void cli_progress_set_format(SEXP bar, const char *format, ...) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP, const char*) = NULL;
  static char str[1024];
  if (ptr == NULL) {
    ptr = (void (*)(SEXP, const char*))
      R_GetCCallable("cli", "cli_progress_set_format");
  }

  va_list ap;
  va_start(ap, format);
  vsnprintf(str, sizeof(str) / sizeof(char), format, ap);

  ptr(bar, str);
}

static R_INLINE void cli_progress_add(SEXP bar, double inc) {
  if (Rf_isNull(bar)) return;
  static void (*ptr)(SEXP, double) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP, double)) R_GetCCallable("cli", "cli_progress_add");
  }
  ptr(bar, inc);
}

static R_INLINE int cli_progress_num() {
  static int (*ptr)() = NULL;
  if (ptr == NULL) {
    ptr = (int (*)()) R_GetCCallable("cli", "cli_progress_num");
  }
  return ptr();
}

static R_INLINE void cli_progress_sleep(int s, long ns) {
  static void (*ptr)(int, long) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(int, long)) R_GetCCallable("cli", "cli_progress_sleep");
  }
  ptr(s, ns);
}

static R_INLINE void cli_progress_update(SEXP bar,
                                         double set,
                                         double inc,
                                         int force) {
  static void (*ptr)(SEXP, double, double, int) = NULL;
  if (ptr == NULL) {
    ptr = (void (*)(SEXP, double, double, int)) R_GetCCallable("cli", "cli_progress_update");
  }
  ptr(bar, set, inc, force);
}

#ifdef __cplusplus
}
#endif

#endif
