/* 
 * This file is a part of the ESPShell Arduino library (Espressif's ESP32-family CPUs)
 *
 * Latest source code can be found at Github: https://github.com/vvb333007/espshell/
 * Stable releases: https://github.com/vvb333007/espshell/tags
 *
 * Feel free to use this code as you wish: it is absolutely free for commercial and 
 * non-commercial, education purposes.  Credits, however, would be greatly appreciated.
 *
 * Author: Viacheslav Logunov <vvb333007@gmail.com>
 */

// -- Tasks --
// Along with "qlib", the "task" module contains OS Abstraction Layer, implemented as thin wrappers
// This allows for easier porting to other architectures
//
// Thin wrapper for FreeRTOS' task functions, command handlers related to tasks
// Main purpose is to move all FreeRTOS specific code into one or two files while rest of the code
// will use its own functions or/and defines
//
// Overhead, usually associated with wrappers is minimal or zero
//
// There are 3 way espshell start tasks:
// 1. via amp_helper_task()   : every command with "&" at the end, i.e. "background" commands
// 2. via alias_helper_task() : background alias execution ("if" and "every" commands)
// 3. generic task_new() call
//
// Cases 1 and 2 use so-called "helper argument" or helper_arg to pass parameters to the task (see ha_get())
// to initialize their environment (Context, keywords and Cwd). Case 3 does not do any setup and as a result, these
// generic tasks must not access Context, keywords or Cwd thread-local variables
//
//

#if COMPILING_ESPSHELL



#define task_t     TaskHandle_t
#define taskfunc_t TaskFunction_t

#define TASK_MAX_PRIO 24 // use macro from IDF/FreeRTOS
#define TASK_MIN_PRIO 0

// forwards
static const char *files_set_cwd(const char *);
static inline const char *files_get_cwd();

extern task_t  loopTaskHandle; // task handle of a task which calls Arduino's loop().
static task_t  shell_task = 0; // Main espshell task handle
static uint8_t shell_prio = 0; // Default shell priority (Inherited by spawned tasks,
                               // adjusted to be higher than loopTask priority on shell startup)

// CPU core number ESPShell is running on. For a single core system it is always 0.
// Tasks, that are created by ESPShell are pinned to the "shell_core"
// Plus workaround for USB-CDC (see https://github.com/espressif/arduino-esp32/issues/11959)
#if (portNUM_PROCESSORS > 1)
#  if SERIAL_IS_USB
#    define CHANGE_CORE 0
#  else
#    define CHANGE_CORE 1
#  endif
static uint8_t shell_core = ARDUINO_RUNNING_CORE ^ CHANGE_CORE;
#else
static uint8_t shell_core = 0;
#endif


// Signals for use in task_signal(), task_signal_from_isr() and task_wait_for_signal() functions
// TODO: make typedefed enum
#define SIGNAL_TERM 0  // Request to terminate. (Must be zero, DO NOT CHANGE its default value)
#define SIGNAL_GPIO 1  // "Pin interrupt" signal. Generated by GPIO ISR.
#define SIGNAL_KILL 2  // Force task deletion. This value can be sent but can't be received
#define SIGNAL_HUP  3  // "Reinitialize/Re-read configuration" (Unused, for future extensions)

//
// void taskid_remember(task_t handle);
// void taskid_forget(task_t handle);
//
// Remember & Forget task IDs. We only need these if we are running
// on old Arduino Core version, where TraceFaclity is disabled: so we maintain the list
// of running tasks.
//
#if CONFIG_FREERTOS_USE_TRACE_FACILITY  // New Arduino Core
#  define taskid_remember(value) {}     //  do nothing
#  define taskid_forget(value) {}       //  do nothing
#else                                   // Old Arduino Core
// TODO: remove by 1.0.0
static vsa_t *task_list = 0;            //  variable-sized array to hold active tasks list
#  warning "TraceFacility is disabled in ESP-IDF: Limited task module functionality"
#  define taskid_remember(value) \
       vsa_find_slot(&task_list, NULL, value, true) 

#  define taskid_forget(value) \
     { \
       vsa_t *_vsa; \
       int slot = 0; \
       if ((_vsa = vsa_find_slot(&task_list, &slot, value, false)) != NULL) \
         _vsa->values[slot] = 0; \
     }
#endif // !CONFIG_FREERTOS_USE_TRACE_FACILITY

// task_t taskid_self();
//
// Get current task id
//
#define taskid_self() \
  xTaskGetCurrentTaskHandle()

// task_t taskid_arduino_sketch()
//
// Get task handle for Arduino's loop task
//
#define taskid_arduino_sketch() \
  loopTaskHandle

// task_t taskid_shell_repl()
//
// Get task handle for ESPShell main (REPL) task
//
#define taskid_shell_repl() \
  shell_task

// unsigned int task_get_priority(task_t handle);
//
// Get task priority
//
#define task_get_priority(_TaskID) \
  ((unsigned int)uxTaskPriorityGet(_TaskID))

// unsigned int task_set_priority(task_t handle, uint8_t priority);
//
// Set task priority
//
#define task_set_priority(_TaskID, _Prio) \
  vTaskPrioritySet(_TaskID, _Prio)

// void task_resume(task_t handle);
//
// Resume
#define task_resume(_TaskID) \
  vTaskResume(_TaskID)

// void task_suspend(task_t handle);
//
// Suspend
#define task_suspend(_TaskID) \
  vTaskSuspend(_TaskID)

// void task_kill(task_t handle);
//
// Destroy task. 
//
#define task_kill(_TaskID) \
  vTaskDelete(_TaskID)

// void task_kill_self();
// 
// Must be called by every task when task finishes its execution:
// In FreeRTOS simple "return" from a task is not allowed. Instead task_kill_self() must be called.
// This function does not return (has __attribute__((noreturn)))
//
#define task_kill_self() \
  vTaskDelete(NULL)

// task_t task_by_name(const char *name);
//
// Find a task handle
//
#define task_by_name(_Name) \
  xTaskGetHandle(_Name)


// task_t task_new(TaskFunction_t func, void *arg_for_func, const char *task_name, uint8_t core);
//
// Start a new thread on the same core espshell is running, remember the task_id.
// With trace facility enabled, this and other macros here have zero overhead comparing to plain FreeRTOS API
//
// _Core can be 0, 1 or tskNO_AFFINITY
#define task_new(_Func, _Arg, _Name, _Core) \
  ({ \
    task_t handle = NULL; \
    if (pdPASS == xTaskCreatePinnedToCore((TaskFunction_t)_Func, \
                                          _Name, \
                                          STACKSIZE, \
                                          _Arg, \
                                          shell_prio, /*TODO: uxTaskPriorityGet(NULL) ?*/ \
                                          &handle, \
                                          _Core)) \
      taskid_remember(handle); \
    handle; \
  })

// Must be called by a task to finish its execution:
// FreeRTOS can not handle "return" from the task function. Instead, vTaskDelete must be called.
// Dispose all per-thread variables here
//
#define task_finished() \
  { \
    taskid_forget(taskid_self()); \
    task_return_memory(); \
    task_kill_self(); \
  }

// Task signalling wrappers. qlib is used in a few different projects so I try to keep it easily convertible to other API
// (POSIX for example). 

// Send a signal (uin32_t arbitrary value) to the task.If task was blocking on task_signal_wait() the task will unblock
// and receive signal value
#define task_signal(_Handle, _Signal) \
  xTaskNotify((task_t)(_Handle), _Signal, eSetValueWithOverwrite)

// Same as above but ISR-safe
#define task_signal_from_isr(_Handle, _Signal) \
  { \
    BaseType_t force_yield; \
    xTaskNotifyFromISR((task_t)(_Handle), _Signal, eSetValueWithOverwrite, &force_yield); \
    if (force_yield) \
      portYIELD_FROM_ISR(); \
  }

// When task is created, there are some memory buffers get allocated: CWD for the filesystem as an example.
// When task is destroyed we release that allocated memory to the system. This one called implicitly by task_finished()
// 
static void task_return_memory() {
#if WITH_FS  
  files_set_cwd(NULL);
#endif
}

// Block until any signal is received but not longer than /timeout/ milliseconds. Value of 0xffffffff (DELAY_INFINITE) means "infinite timeout":
// function will block until it receives ANY signal; the signal value is returned in /*sig/, pointer can be NULL;
// 
// Returns /true/ if signal was received
// Returns /false/ if timeout has fired before any signal was received
//
static bool task_wait_for_signal(uint32_t *sig, uint32_t timeout_ms) {

  bool loop = false;
  uint32_t sig0;

  // portMAX_DELAY is not an infinite value, it is 0xffffffff which is about 1200 hours
  // thats why we have /loop/ flag here
  if (timeout_ms == DELAY_INFINITE) {
    timeout_ms = portMAX_DELAY;
    loop = true;
  } else 
    timeout_ms = pdMS_TO_TICKS(timeout_ms); 
 
  if (NULL == sig)
    sig = &sig0;

  // block until any notification or timeout
  if (!loop)
    return xTaskNotifyWait(0, 0xffffffff, sig, timeout_ms ) == pdPASS;

  // block until any notification
  while( xTaskNotifyWait(0, 0xffffffff, sig, timeout_ms ) != pdPASS ) {}

  return true;
}


// Can we perform commands on this taskid?
// The task must be not the espshell's main task AND taskid must be in a valid address range
//
static bool is_taskid_good(task_t taskid) {

  if (!is_valid_address((void *)taskid,sizeof(task_t))) {
    HELP(q_print("% Task ID you entered seems to be invalid\r\n"
                 "% Task ID is a <i>hex number</>, something like \"3ffb0030\" or \"0x40005566\"\r\n"));
    return false;
  }
  
  // Ignore attempts to manipulate the main espshell task
  if (shell_task == taskid) {
    HELP(q_printf("%% Task <i>%p</> is the main espshell task, access denied :)\r\n",taskid));
    return false;
  }

  return true;
}

// check if *this* task (a caller) is executed as separate (background) task
// or it is executed in ESPShell's task context
//
static INLINE bool is_foreground_task() {
  return shell_task == taskid_self();
}

static INLINE bool is_background_task() {
  return shell_task != taskid_self();
}


// A task argument structure passed to newly created tasks (e.g., amp_helper_task(void *arg)
// or alias_helper_task(void *arg))
//
// The last three members (keywords, cwd, and context) hold the current keyword tree, the Context value, 
// and the filesystem's current working directory (freed by task_finished()).
//
// These members are populated by ha_get(), which copies the current values of /Context/, /keywords/, 
// and /Cwd/ into the corresponding fields of helper_arg. 
//
// The newly spawned task reads these values and assigns them to the thread-local /Context/, /keywords/, and /Cwd/ variables. 
// (They are declared as "static _Thread_local", so they are per-thread, not truly global)
//
// Different fields are used depending on the task type:
//
// 1. amp_helper_task() (used to execute shell commands in the background) uses /.aa/.
// 2. alias_helper_task() (used to run aliases in the background) uses /.al/ and /.delay_ms/
//
// Although /.next/, /.aa/, and /.al/ are never used at the same time, putting them into a union is discouraged. 
// Using a union would technically work but defeats the idea of having "permanent pointers"
//
struct helper_arg {
    struct helper_arg       *next;     // TODO: remove. mb_get()/mb_put() manage this
    struct alias            *al;
    argcargv_t              *aa;
    uint32_t                 delay_ms;
    __typeof__(Context)      context;  // Task must copy this into the /Context/ thread variable
    const struct keywords_t *keywords; // Task must copy this into the /keywords/ thread variable
    char                    *cwd;      // Task must call files_set_cwd( ha->cwd )
};


// Once allocated, structures never freed.
//
// Instead they are put on the "unused" list. Since alias_exec_in_background() is called everytime condition 
// is triggered, we want our alloc/free to be as fast as possible - thats why ha_get() and ha_put() instead 
// of malloc() and free(). Under load espshell simply preallocates as many entries on its "unused" as needed 
// reducing suffering from OOM events
//
// Second reason is to keep pointers persistent - so any stored pointer always points 
// to a valid memory region
//
static struct mb_pool ha_pool = MB_POOL(sizeof(struct helper_arg),0);

// Allocate new helper_arg, copy current Context, keywords and the CWD
//
static struct helper_arg *ha_get() {

  struct helper_arg *ret;

  // Populate common fields: /Context/, /keywords/ and /Cwd/. These fields will be used by a spawned task,
  // to set its corresponding "global" (actually, _Thread_local) variables. The rest is populated by the caller
  //
  if (NULL != (ret = mb_get(&ha_pool))) {
    ret->context = context_get();
    ret->keywords = keywords_get();
    ret->cwd = q_strdup(files_get_cwd(), MEM_TMP); // its ok if strdup() return NULL; files_get_cwd() never return NULL
  }

  return ret;
}

// Put /ha/ on unused list.
//
static inline void ha_put(struct helper_arg *ha) {

    mb_put(&ha_pool, ha);
}

// Older versions of  ESP-IDF had FreeRTOS Trace Facility disabled, so we had to use an ugly workaround 
// (see taskid_remember(), taskid_forget())
// 
// Workaround is ok, but has it disadvantages:
//   1. system tasks become invisible to ESPShell (Tmr Svc, ipc0, ipc1, IDLE0, IDLE1, esp_timer)
//   2. "show tasks" will not display anything except task ID. Even names will be inaccessible.
//
// Better approach is to stick to trace utility API, which somehow became available in latest Arduino Core's ESP-IDF.
// ESP-IDF that is used in Arduino Core is lagging well behind the main branch, so when I checked it last year, 
// Arduino Core of that time had Trace Facility disabled (to reenable it one have to recompile esp-idf libs which 
// may be tricky)
//
// I missed the moment when Espressif had added Trace Utility support, so instead of "#if esp_idf_version < x.x.x" 
// we use macro CONFIG_FREERTOS_USE_TRACE_FACILITY from the IDF config
//

// Proper version:
#if CONFIG_FREERTOS_USE_TRACE_FACILITY

// FreeRTOS task state names (eTaskState)
// These are fetched as part of TaskState_t structure in cmd_show_tasks()
//
static const char *task_state_name[] = { 
  "Running  ",
  "Ready    ",
  "Waiting  ",
  "Suspended",
  "Deleted  ",
  "Invalid  "
};

//"show tasks"
// Shows task ID FreeRTOS is aware of
// Use FreeRTOS Trace Utility to access list of kernel tasks
// Totally ignore task ids from locally managed VSA
//
static int cmd_show_tasks(int argc, char **argv) {
  int j = 0,nt;

  if ((nt = uxTaskGetNumberOfTasks()) > 0) {

    //TODO: allocate on heap, stack is limited
    TaskStatus_t tasks[nt]; 
    memset(tasks,0,sizeof(TaskStatus_t) * nt);

    // Fetch all tasks states & other information.
    // Creates a lag, because disables scheduling completely during information gathering
    uxTaskGetSystemState( tasks, nt, NULL);

    q_print("%<r>  # |  Task  ID  |        Name      | Prio |   State   | Stack/(HighWM)   | Core</>\r\n"
            "%----+------------+------------------+------+-----------+------------------+-----\r\n");

    for (j = 0; j < nt; j++) {
      // just in case. you never know when they decide to update their eStatus enum
      if (tasks[j].eCurrentState > 5)
        tasks[j].eCurrentState = 5;

      const char *warn = "<m>!";
      if (tasks[j].usStackHighWaterMark > 1023)
        warn = " ";

      q_printf("%% %3u| %p | %16s |  %2u  | %s |%s%p /%5u|</>",
        j + 1,
        tasks[j].xHandle,
        tasks[j].pcTaskName,
        tasks[j].uxCurrentPriority,
        task_state_name[tasks[j].eCurrentState],
        warn,
        tasks[j].pxStackBase,
        (unsigned int)tasks[j].usStackHighWaterMark);

      const char *affinity = NULL;

#if (( configNUMBER_OF_CORES > 1 ) && ( configTASKLIST_INCLUDE_COREID == 1 ))
      if (tasks[j].xCoreID == 0)
        affinity = " CPU0";
      else if (tasks[j].xCoreID == 1)
        affinity = " CPU1";
      else
        affinity = "  Any";
#endif
      q_print(affinity ? affinity : " Main");
      q_print(CRLF);
    }
  }
  q_printf("%%----+------------+------------------+------+-----------+------------------+-----\r\n"
           "%% Total: %u tasks. <m>low HighWM</> values MAY indicate stack overflow risks\r\n",j);
  return 0;
}
#else //!CONFIG_FREERTOS_USE_TRACE_FACILITY
//
//"show tasks"
// Shows task ID espshell is aware of (loopTask + all task ids recorded via taskid_remember())
// No system tasks will be displayed (i.e. IDLE0, ipc0, Tmr Svc, etc)
//
static int cmd_show_tasks(int argc, char **argv) {
  int j = 0;

  // Source task ID's from locally maintained variable-sized array of task IDs
  vsa_t *v = task_list;
  MUST_NOT_HAPPEN(task_list == NULL); // there must be at least 1 task, espshell task

  q_print("% Running tasks (user tasks):\r\n");

  q_print("%<r>  # |  Task  ID  |        Name      | Prio |   State   </>\r\n"
          "%----+------------+------------------+------+-----------\r\n");
  while(v) {
    for (int i = 0; i < VSASIZE/sizeof(vsaval_t); i++)
      if (v->values[i])
          q_printf("%% %3u| %p | %16s |  %2u  | %s\r\n",++j,v->values[i], "N/A", 0, 0);
    v = v->next;
  }
  q_printf("%%----+------------+------------------+------+-----------\r\n"
           "%% Total: %u tasks\r\n",j);
  return 0;
}
#endif



//"suspend [TASK_ID | TASK_NAME]"
// suspends main Arduino task (i.e loop()) or any other task
//
static int cmd_suspend(int argc, char **argv) {

  task_t taskid;
  task_t sus = loopTaskHandle;
  if (argc > 1) {
    if (q_isnumeric(argv[1]))
      taskid = (task_t)hex2uintptr(argv[1]);
    else
      taskid = task_by_name(argv[1]);
    if (is_taskid_good(taskid))
      sus = taskid; 
    else 
      return 1;
  }
  task_suspend(sus);
  
  return 0;
}

//"resume"
// Resume previously suspended task
//
static int cmd_resume(int argc, char **argv) {
  task_t taskid;
  task_t sus = loopTaskHandle;
  if (argc > 1) {
    if (q_isnumeric(argv[1]))
      taskid = (task_t)hex2uintptr(argv[1]);
    else
      taskid = task_by_name(argv[1]);
    if (is_taskid_good(taskid))
      sus = taskid;
    else
      return 1;
  }
  task_resume(sus);
  return 0;
}

//"kill [-term|-kill|-9|-15] TASK_ID|TASK_NAME"
// 1. Stop a background command
// 2. Terminate arbitrary FreeRTOS task
// 
static int cmd_kill(int argc, char **argv) {

  unsigned int sig = SIGNAL_TERM, i = 1;
  task_t taskid;
  if (argc < 2)
    return CMD_MISSING_ARG;

  if (argv[i][0] == '-' && argv[i][1]) { // an option, task id follows
    q_tolower(argv[i]);
#if 0    
    if (!q_strcmp(argv[i],"-term") || !q_strcmp(argv[i],"-15")) sig = SIGNAL_TERM; else
    if (!q_strcmp(argv[i],"-hup")  ||  !q_strcmp(argv[i],"-1")) sig = SIGNAL_HUP;  else
    if (!q_strcmp(argv[i],"-kill") || !q_strcmp(argv[i],"-9"))  sig = SIGNAL_KILL; else return 1;
#else
    if (argv[i][1] == 't' || (argv[i][1] == '1' && argv[i][2] == '5')) sig = SIGNAL_TERM; else
    if (argv[i][1] == 'h' || (argv[i][1] == '1' && argv[i][2] == 0)) sig = SIGNAL_HUP; else
    if (argv[i][1] == 'k' || (argv[i][1] == '9' && argv[i][2] == 0)) sig = SIGNAL_KILL; else return 1;
#endif    
    i++;
  }

  if (i >= argc)
    return CMD_MISSING_ARG;

  if (q_isnumeric(argv[i]))
    taskid = (task_t)hex2uintptr(argv[i]);
  else
    taskid = task_by_name(argv[i]);

  if (is_taskid_good(taskid)) {
    // SIGNAL_KILL is never sent to a task. Instead, task is deleted.
    if (sig == SIGNAL_KILL) {
      task_suspend((task_t )taskid);
      q_delay(1);
      task_kill((task_t )taskid);
      taskid_forget((task_t )taskid);
      HELP(q_printf("%% Killed: \"%p\"\r\n", taskid));
    } else
      // -term, -hup and other signals are sent directly to the task
      task_signal(taskid, sig);

  } else
    return i;
  return 0;
}

// Set task priority (0..24)
// "prio (0..24) [TASK_ID]"
// Can be used in aliases to raise priority during exec (in this case use "prio NUM", 
// as task id is not yet known and cant be provided to the command)
// 
static int cmd_priority(int argc, char **argv) {

  task_t taskid;
  unsigned char prio;

  if (argc < 2)
    return CMD_MISSING_ARG;
  
  if ((prio = q_atoi(argv[1],configMAX_PRIORITIES)) >= configMAX_PRIORITIES) {
    HELP(q_printf("%% Out of range. Range is [0..%u] (0 is the lowest priority)\r\n", configMAX_PRIORITIES - 1));
    return 1;
  }

  if (argc > 2) {

    if (q_isnumeric(argv[2]))
      taskid = (task_t)hex2uintptr(argv[2]);
    else
      taskid = task_by_name(argv[2]);

    if (!is_taskid_good(taskid))
      return 2;
  } else
    taskid = NULL; // self
  
  // Save priority value if it was for ESPShell main task: new tasks will inherit this priority
  // Double check: shell task id can be passed as an argument, causing taskid != NULL
  if ((!taskid && is_foreground_task()) || taskid == shell_task)
    shell_prio = prio;

  task_set_priority(taskid, (UBaseType_t )prio);

  return 0;
}


#endif // COMPILING_ESPSHELL

