/******************************************************************************
* Copyright 2013-2014 Espressif Systems (Wuxi)
*
* FileName: hw_timer.c
*
* Description: hw_timer driver
*
* Modification history:
*     2014/5/1, v1.0 create this file.
*
* Adapted for NodeMCU 2016
*
* The owner parameter should be a unique value per module using this API
* It could be a pointer to a bit of data or code
* e.g.   #define OWNER    ((os_param_t) module_init)
* where module_init is a function. For builtin modules, it might be
* a small numeric value that is known not to clash.
*******************************************************************************/
#include "platform.h"
#include <stdio.h>
#include <stdlib.h>
#include "ets_sys.h"
#include "os_type.h"
#include "osapi.h"

#include "hw_timer.h"

//#define DEBUG_HW_TIMER
//#undef NODE_DBG
//#define NODE_DBG dbg_printf

#define FRC1_ENABLE_TIMER  BIT7
#define FRC1_AUTO_LOAD  BIT6

//TIMER PREDIVIDED MODE
typedef enum {
    DIVIDED_BY_1 = 0,		//timer clock
    DIVIDED_BY_16 = 4,	//divided by 16
    DIVIDED_BY_256 = 8,	//divided by 256
} TIMER_PREDIVIDED_MODE;

typedef enum {			//timer interrupt mode
    TM_LEVEL_INT = 1,	// level interrupt
    TM_EDGE_INT   = 0,	//edge interrupt
} TIMER_INT_MODE;

/*
 * This represents a single user of the timer functionality. It is keyed by the owner
 * field.
 */
typedef struct _timer_user {
  struct _timer_user *next;
  bool autoload;
  int32_t delay; // once on the active list, this is difference in delay from the preceding element
  int32_t autoload_delay;
  uint32_t expected_interrupt_time;
  os_param_t owner;
  os_param_t callback_arg;
  void (* user_hw_timer_cb)(os_param_t);
#ifdef DEBUG_HW_TIMER
  int cb_count;
#endif
} timer_user;

/*
 * There are two lists of timer_user blocks. The active list are those which are waiting
 * for timeouts to happen, and the inactive list contains idle blocks. Unfortunately
 * there isn't a way to clean up the inactive blocks as some modules call the
 * close method from interrupt level.
 */
static timer_user *active;
static timer_user *inactive;

/*
 * There are a fair number of places when interrupts need to be disabled as many of
 * the methods can be called from interrupt level. The lock/unlock calls support
 * multiple LOCKs and then the same number of UNLOCKs are required to re-enable
 * interrupts. This is imolemeted by counting the number of times that lock is called.
 */
static uint8_t lock_count;
static uint8_t timer_running;

static uint32_t time_next_expiry;
static int32_t last_timer_load;

#define LOCK()   do { ets_intr_lock(); lock_count++; } while (0)
#define UNLOCK()   if (--lock_count == 0) ets_intr_unlock()

/*
 * It is possible to reserve the timer exclusively, for one module alone.
 * This way the interrupt overhead is minimal.
 * Drawback is that no other module can use the timer at same time.
 * If flag if true, indicates someone reserved the timer exclusively.
 * Unline shared used (default), only one client can reserve exclusively.
 */
static bool reserved_exclusively = false;

/*
 * To start a timer, you write to FRCI_LOAD_ADDRESS, and that starts the counting
 * down. When it reaches zero, the interrupt fires -- but the counting continues.
 * The counter is 23 bits wide. The current value of the counter can be read
 * at FRC1_COUNT_ADDRESS. The unit is 200ns, and so it takes somewhat over a second
 * to wrap the counter.
 */

#ifdef DEBUG_HW_TIMER
void ICACHE_RAM_ATTR hw_timer_debug() {
  dbg_printf("timer_running=%d\n", timer_running);
  timer_user *tu;
  for (tu = active; tu; tu = tu->next) {
    dbg_printf("Owner: 0x%x, delay=%d, autoload=%d, autoload_delay=%d, cb_count=%d\n",
        tu->owner, tu->delay, tu->autoload, tu->autoload_delay, tu->cb_count);
  }
}
#endif

static void ICACHE_RAM_ATTR set_timer(int delay, const char *caller) {
  if (delay < 1) {
    delay = 1;
  }
  int32_t time_left = (RTC_REG_READ(FRC1_COUNT_ADDRESS)) & ((1 << 23) - 1);
  RTC_REG_WRITE(FRC1_LOAD_ADDRESS, delay);

  if (time_left > last_timer_load) {
    // We have missed the interrupt
    time_left -= 1 << 23;
  }
  NODE_DBG("%s(%x): time_next=%d, left=%d (load=%d), delay=%d  => %d\n", caller, active->owner, time_next_expiry, time_left, last_timer_load, delay, time_next_expiry - time_left + delay);
  time_next_expiry = time_next_expiry - time_left + delay;
  last_timer_load = delay;

  timer_running = 1;
}

static void ICACHE_RAM_ATTR adjust_root() {
  // Can only ge called with interrupts disabled
  // change the initial active delay so that relative stuff still works
  // Also, set the last_timer_load to be now
  int32_t time_left = (RTC_REG_READ(FRC1_COUNT_ADDRESS)) & ((1 << 23) - 1);
  if (time_left > last_timer_load) {
    // We have missed the interrupt
    time_left -= 1 << 23;
  }

  if (active && timer_running) {
    active->delay = time_left;
  }

  if (active) {
    NODE_DBG("adjust(%x): time_left=%d (last_load=%d)\n", active->owner, time_left, last_timer_load);
  } else {
    NODE_DBG("adjust: time_left=%d (last_load=%d)\n", time_left, last_timer_load);
  }

  last_timer_load = time_left;
}
/*
 * Find the timer_user block for this owner. This just returns
 * a pointer to the block, or NULL.
 */
static timer_user * ICACHE_RAM_ATTR find_tu(os_param_t owner) {
  // Try the inactive chain first
  timer_user **p;

  LOCK();

  for (p = &inactive; *p; p = &((*p)->next)) {
    if ((*p)->owner == owner) {
      timer_user *result = *p;

      UNLOCK();

      return result;
    }
  }

  for (p = &active; *p; p = &((*p)->next)) {
    if ((*p)->owner == owner) {
      timer_user *result = *p;

      UNLOCK();

      return result;
    }
  }

  UNLOCK();
  return NULL;
}

/*
 * Find the timer_user block for this owner. This just returns
 * a pointer to the block, or NULL. If it finds the block, then it is
 * removed from whichever chain it is on. Note that this may require
 * triggering a timer.
 */
static timer_user * ICACHE_RAM_ATTR find_tu_and_remove(os_param_t owner) {
  // Try the inactive chain first
  timer_user **p;

  LOCK();

  for (p = &inactive; *p; p = &((*p)->next)) {
    if ((*p)->owner == owner) {
      timer_user *result = *p;
      *p = result->next;
      result->next = NULL;

      UNLOCK();

      return result;
    }
  }

  for (p = &active; *p; p = &((*p)->next)) {
    if ((*p)->owner == owner) {
      timer_user *result = *p;

      bool need_to_reset = (result == active) && result->next;

      if (need_to_reset) {
        adjust_root();
      }

      // Increase the delay on the next element
      if (result->next) {
        result->next->delay += result->delay;
      }

      // Cut out of chain
      *p = result->next;
      result->next = NULL;

      if (need_to_reset) {
        set_timer(active->delay, "find_tu");
      }

      UNLOCK();
      return result;
    }
  }

  UNLOCK();
  return NULL;
}

/*
 * This inserts a timer_user block into the active chain. This is a sightly
 * complex process as it can involve triggering a timer load.
 */
static void ICACHE_RAM_ATTR insert_active_tu(timer_user *tu) {
  timer_user **p;

  LOCK();

  tu->expected_interrupt_time = time_next_expiry - last_timer_load + tu->delay;

  for (p = &active; *p; p = &((*p)->next)) {
    if ((*p)->delay >= tu->delay) {
      break;
    }
    tu->delay -= (*p)->delay;
  }

  if (*p) {
    (*p)->delay -= tu->delay;
  }
  tu->next = *p;
  *p = tu;

  if (tu == active) {
    // We have a new leader
    set_timer(active->delay, "insert_active");
  }
  UNLOCK();
}

/******************************************************************************
* FunctionName : platform_hw_timer_arm_ticks
* Description  : set a trigger timer delay for this timer.
* Parameters   : os_param_t owner
*                uint32 ticks :
* Returns      : true if it worked
*******************************************************************************/
bool ICACHE_RAM_ATTR platform_hw_timer_arm_ticks(os_param_t owner, uint32_t ticks)
{
  if (reserved_exclusively) return false;

  timer_user *tu = find_tu_and_remove(owner);

  if (!tu) {
    return false;
  }

  tu->delay = ticks;
  tu->autoload_delay = ticks;

  NODE_DBG("arm(%x): ticks=%d\n", owner, ticks);

  LOCK();
  adjust_root();
  insert_active_tu(tu);
  UNLOCK();

  return true;
}

/******************************************************************************
* FunctionName : platform_hw_timer_arm_us
* Description  : set a trigger timer delay for this timer.
* Parameters   : os_param_t owner
*                uint32 microseconds :
* in autoload mode
*                         50 ~ 0x7fffff;  for FRC1 source.
*                         100 ~ 0x7fffff;  for NMI source.
* in non autoload mode:
*                         10 ~ 0x7fffff;
* Returns      : true if it worked
*******************************************************************************/
bool ICACHE_RAM_ATTR platform_hw_timer_arm_us(os_param_t owner, uint32_t microseconds)
{
  return platform_hw_timer_arm_ticks(owner, US_TO_RTC_TIMER_TICKS(microseconds));
}

/******************************************************************************
* FunctionName : platform_hw_timer_set_func
* Description  : set the func, when trigger timer is up.
* Parameters   : os_param_t owner
*                void (* user_hw_timer_cb_set)(os_param_t):
                        timer callback function
*	         os_param_t arg
* Returns      : true if it worked
*******************************************************************************/
bool platform_hw_timer_set_func(os_param_t owner, void (* user_hw_timer_cb_set)(os_param_t), os_param_t arg)
{
  if (reserved_exclusively) return false;

  timer_user *tu = find_tu(owner);
  if (!tu) {
    return false;
  }
  tu->callback_arg = arg;
  tu->user_hw_timer_cb = user_hw_timer_cb_set;
  NODE_DBG("set-CB(%x): %x, %x\n", tu->owner, tu->user_hw_timer_cb, tu->callback_arg);
  return true;
}

/*
 * This is the timer ISR. It has to find the timer that was running and trigger the callback
 * for that timer. By this stage, the next timer may have expired as well, and so the process
 * iterates. Note that if there is an autoload timer, then it should be restarted immediately.
 * Also, the callbacks typically do re-arm the timer, so we have to be careful not to
 * assume that nothing changes during the callback.
 */
static void ICACHE_RAM_ATTR hw_timer_isr_cb(void *arg)
{
  bool keep_going = true;
  adjust_root();
  timer_running = 0;

  while (keep_going && active) {
    keep_going = false;

    timer_user *fired = active;
    active = fired->next;
    if (fired->autoload) {
      fired->expected_interrupt_time += fired->autoload_delay;
      fired->delay = fired->expected_interrupt_time - (time_next_expiry - last_timer_load);
      insert_active_tu(fired);
      if (active->delay <= 0) {
        keep_going = true;
      }
    } else {
      fired->next = inactive;
      inactive = fired;
      if (active) {
        active->delay += fired->delay;
        if (active->delay <= 0) {
          keep_going = true;
        }
      }
    }
    if (fired->user_hw_timer_cb) {
#ifdef DEBUG_HW_TIMER
      fired->cb_count++;
#endif
      NODE_DBG("CB(%x): %x, %x\n", fired->owner, fired->user_hw_timer_cb, fired->callback_arg);
      (*(fired->user_hw_timer_cb))(fired->callback_arg);
    }
  }
  if (active && !timer_running) {
    set_timer(active->delay, "isr");
  }
}

static void ICACHE_RAM_ATTR hw_timer_nmi_cb(void)
{
  hw_timer_isr_cb(NULL);
}

/******************************************************************************
* FunctionName : platform_hw_timer_get_delay_ticks
* Description  : figure out how long since th last timer interrupt
* Parameters   : os_param_t owner
* Returns      : the number of ticks
*******************************************************************************/
uint32_t ICACHE_RAM_ATTR platform_hw_timer_get_delay_ticks(os_param_t owner)
{
  if (reserved_exclusively) return 0;

  timer_user *tu = find_tu(owner);
  if (!tu) {
    return 0;
  }

  LOCK();
  adjust_root();
  UNLOCK();
  int ret = (time_next_expiry - last_timer_load) - tu->expected_interrupt_time;

  if (ret < 0) {
    NODE_DBG("delay ticks = %d, last_timer_load=%d, tu->expected_int=%d, next_exp=%d\n", ret, last_timer_load, tu->expected_interrupt_time, time_next_expiry);
  }

  return ret < 0 ? 0 : ret;
}

/******************************************************************************
* FunctionName : platform_hw_timer_init
* Description  : initialize the hardware isr timer for shared use i.e. multiple owners.
* Parameters   : os_param_t owner
* FRC1_TIMER_SOURCE_TYPE source_type:
*                         FRC1_SOURCE,    timer use frc1 isr as isr source.
*                         NMI_SOURCE,     timer use nmi isr as isr source.
* bool autoload:
*                         0,  not autoload,
*                         1,  autoload mode,
* Returns      : true if it worked
*******************************************************************************/
bool platform_hw_timer_init(os_param_t owner, FRC1_TIMER_SOURCE_TYPE source_type, bool autoload)
{
  if (reserved_exclusively) return false;

  timer_user *tu = find_tu_and_remove(owner);

  if (!tu) {
    tu = (timer_user *) malloc(sizeof(*tu));
    if (!tu) {
      return false;
    }
    memset(tu, 0, sizeof(*tu));
    tu->owner = owner;
  }

  tu->autoload = autoload;

  if (!active && !inactive) {
    RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
		  DIVIDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);
    ETS_FRC_TIMER1_INTR_ATTACH(hw_timer_isr_cb, NULL);

    TM1_EDGE_INT_ENABLE();
    ETS_FRC1_INTR_ENABLE();
  }

  LOCK();
  tu->next = inactive;
  inactive = tu;
  UNLOCK();

  return true;
}

/******************************************************************************
* FunctionName : platform_hw_timer_close
* Description  : ends use of the hardware isr timer.
* Parameters   : os_param_t owner.
* Returns      : true if it worked
*******************************************************************************/
bool ICACHE_RAM_ATTR platform_hw_timer_close(os_param_t owner)
{
  if (reserved_exclusively) return false;

  timer_user *tu = find_tu_and_remove(owner);

  if (tu) {
    if (tu == inactive) {
      inactive == NULL;
    } else {
      LOCK();
      tu->next = inactive;
      inactive = tu;
      UNLOCK();
    }
  }

  // This will never actually run....
  if (!active && !inactive) {
    /* Set no reload mode */
    RTC_REG_WRITE(FRC1_CTRL_ADDRESS,
                  DIVIDED_BY_16 | TM_EDGE_INT);

    TM1_EDGE_INT_DISABLE();
    ETS_FRC1_INTR_DISABLE();
  }

  return true;
}

/******************************************************************************
* FunctionName : platform_hw_timer_init_exclusive
* Description  : initialize the hardware isr timer for exclusive use by the caller.
* Parameters   : FRC1_TIMER_SOURCE_TYPE source_type:
*                         FRC1_SOURCE,    timer use frc1 isr as isr source.
*                         NMI_SOURCE,     timer use nmi isr as isr source.
* bool autoload:
*                         0,  not autoload,
*                         1,  autoload mode,
* void (* frc1_timer_cb)(os_param_t): timer callback function when FRC1_SOURCE is being used
*	os_param_t arg                    : argument passed to frc1_timer_cb or NULL
* void (* nmi_timer_cb)(void)       : timer callback function when NMI_SOURCE is being used
* Returns      : true if it worked, false if the timer is already served for shared or exclusive use
*******************************************************************************/
bool platform_hw_timer_init_exclusive(
  FRC1_TIMER_SOURCE_TYPE source_type,
  bool autoload,
  void (* frc1_timer_cb)(os_param_t),
  os_param_t arg,
  void (*nmi_timer_cb)(void)
  )
{
  if (active || inactive) return false;
  if (reserved_exclusively) return false;
  reserved_exclusively = true;

  RTC_REG_WRITE(FRC1_CTRL_ADDRESS, (autoload ? FRC1_AUTO_LOAD : 0) | DIVIDED_BY_16 | FRC1_ENABLE_TIMER | TM_EDGE_INT);

  if (source_type == NMI_SOURCE) {
    ETS_FRC_TIMER1_NMI_INTR_ATTACH(nmi_timer_cb);
  } else {
    ETS_FRC_TIMER1_INTR_ATTACH((void (*)(void *))frc1_timer_cb, (void*)arg);
  }

  TM1_EDGE_INT_ENABLE();
  ETS_FRC1_INTR_ENABLE();

  return true;
}

/******************************************************************************
* FunctionName : platform_hw_timer_close_exclusive
* Description  : ends use of the hardware isr timer in exclusive mode.
* Parameters   :
* Returns      : true if it worked
*******************************************************************************/
bool ICACHE_RAM_ATTR platform_hw_timer_close_exclusive()
{
  if (!reserved_exclusively) return true;
  reserved_exclusively = false;

  /* Set no reload mode */
  RTC_REG_WRITE(FRC1_CTRL_ADDRESS, DIVIDED_BY_16 | TM_EDGE_INT);

  TM1_EDGE_INT_DISABLE();
  ETS_FRC1_INTR_DISABLE();

  return true;
}

/******************************************************************************
* FunctionName : platform_hw_timer_arm_ticks_exclusive
* Description  : set a trigger timer delay for this timer.
* Parameters   : uint32 ticks :
* Returns      : true if it worked
*******************************************************************************/
bool ICACHE_RAM_ATTR platform_hw_timer_arm_ticks_exclusive(uint32_t ticks)
{
  RTC_REG_WRITE(FRC1_LOAD_ADDRESS, ticks);
  return true;
}

/******************************************************************************
* FunctionName : platform_hw_timer_arm_us_exclusive
* Description  : set a trigger timer delay for this timer.
* Parameters   : uint32 microseconds :
* in autoload mode
*                         50 ~ 0x7fffff;  for FRC1 source.
*                         100 ~ 0x7fffff;  for NMI source.
* in non autoload mode:
*                         10 ~ 0x7fffff;
* Returns      : true if it worked
*******************************************************************************/
bool ICACHE_RAM_ATTR platform_hw_timer_arm_us_exclusive(uint32_t microseconds)
{
  RTC_REG_WRITE(FRC1_LOAD_ADDRESS, US_TO_RTC_TIMER_TICKS(microseconds));
  return true;
}
