/*
 * Copyright (c) 2016 Zibin Zheng <znbin@qq.com>
 * All rights reserved
 * https://github.com/0x1abin/MultiButton
 */

#include "multi_button.h"

#define CONFIG_SYS_LOG_LEVEL SYS_LOG_LEVEL_ERR
#define SYS_LOG_DOMAIN "MAIN"
#include "sys_log.h"

#define EVENT_CB(ev)                        \
    do                                      \
    {                                       \
        if ((1U << ev) & handle->event_cb)  \
            handle->cb((void *)handle, ev); \
    } while (0)

#define PRESS_REPEAT_MAX_NUM 15 /*!< The maximum value of the repeat counter */

// button handle list head.
static button_hdl_t *head_handle = NULL;

static void button_handler(button_hdl_t *handle);

/**
 * @brief  Initializes the button struct handle.
 * @param  handle: the button handle struct.
 * @param  pin_level: read the HAL GPIO of the connected button level.
 * @param  active_level: pressed GPIO level.
 * @param  button_id: the button id.
 * @retval None
 */
void button_init(button_hdl_t *handle, uint8_t (*pin_level)(uint8_t button_id), uint8_t active_level, uint8_t button_id)
{
    memset(handle, 0, sizeof(button_hdl_t));
    handle->event = (uint8_t)NONE_PRESS;
    handle->hal_button_Level = pin_level;
    handle->button_level = !active_level;
    handle->active_level = active_level;
    handle->button_id = button_id;
}

/**
 * @brief  Attach the button event callback function.
 * @param  handle: the button handle struct.
 * @param  event_mask: trigger event type. @ref EVENT_MASK
 * @param  cb: callback function.
 * @retval None
 */
void button_attach(button_hdl_t *handle, uint8_t event_mask, button_callback_fn cb)
{
    handle->cb = cb;
    handle->event_cb = event_mask;
}

/**
 * @brief  Inquire the button event happen.
 * @param  handle: the button handle struct.
 * @retval button event.
 */
press_event_t get_button_event(button_hdl_t *handle)
{
    return (press_event_t)(handle->event);
}

/**
 * @brief  Button driver core function, driver state machine.
 * @param  handle: the button handle struct.
 * @retval None
 */
static void button_handler(button_hdl_t *handle)
{
    uint8_t read_gpio_level = handle->hal_button_Level(handle->button_id);

    // ticks counter working..
    if ((handle->state) > 0)
        handle->ticks++;

    /*------------button debounce handle---------------*/
    if (read_gpio_level != handle->button_level)
    { // not equal to prev one
        // continue read 3 times same new level change
        if (++(handle->debounce_cnt) >= DEBOUNCE_TICKS)
        {
            handle->button_level = read_gpio_level;
            handle->debounce_cnt = 0;
        }
    }
    else
    { // level not change ,counter reset.
        handle->debounce_cnt = 0;
    }

    /*-----------------State machine-------------------*/
    switch (handle->state)
    {
    case 0:
        if (handle->button_level == handle->active_level)
        { // start press down
            handle->event = (uint8_t)PRESS_DOWN;
            SYS_LOG_INF("PRESS_DOWN");
            EVENT_CB(PRESS_DOWN);
            handle->ticks = 0;
            handle->repeat = 1;
            handle->state = 1;
        }
        else
        {
            handle->event = (uint8_t)NONE_PRESS;
        }
        break;

    case 1:
        if (handle->button_level != handle->active_level)
        { // released press up
            handle->event = (uint8_t)PRESS_UP;
            SYS_LOG_INF("PRESS_UP");
            EVENT_CB(PRESS_UP);
            handle->ticks = 0;
            handle->state = 2;
        }
        else if (handle->ticks > LONG_TICKS)
        {
            handle->event = (uint8_t)LONG_PRESS_START;
            SYS_LOG_INF("LONG_PRESS_START");
            EVENT_CB(LONG_PRESS_START);
            handle->state = 5;
        }
        break;

    case 2:
        if (handle->button_level == handle->active_level)
        { // press down again
            handle->event = (uint8_t)PRESS_DOWN;
            SYS_LOG_INF("PRESS_DOWN");
            EVENT_CB(PRESS_DOWN);
            if (handle->repeat != PRESS_REPEAT_MAX_NUM)
            {
                handle->repeat++;
            }
            SYS_LOG_INF("PRESS_REPEAT");
            EVENT_CB(PRESS_REPEAT); // repeat hit
            handle->ticks = 0;
            handle->state = 3;
        }
        else if (handle->ticks > SHORT_TICKS)
        { // released timeout
            if (handle->repeat == 1)
            {
                handle->event = (uint8_t)SINGLE_CLICK;
                SYS_LOG_INF("SINGLE_CLICK");
                EVENT_CB(SINGLE_CLICK);
            }
            else if (handle->repeat == 2)
            {
                handle->event = (uint8_t)DOUBLE_CLICK;
                SYS_LOG_INF("DOUBLE_CLICK");
                EVENT_CB(DOUBLE_CLICK); // repeat hit
            }
            handle->state = 0;
        }
        break;

    case 3:
        if (handle->button_level != handle->active_level)
        { // released press up
            handle->event = (uint8_t)PRESS_UP;
            SYS_LOG_INF("PRESS_UP");
            EVENT_CB(PRESS_UP);
            if (handle->ticks < SHORT_TICKS)
            {
                handle->ticks = 0;
                handle->state = 2; // repeat press
            }
            else
            {
                handle->state = 0;
            }
        }
        else if (handle->ticks > SHORT_TICKS)
        { // SHORT_TICKS < press down hold time < LONG_TICKS
            handle->state = 1;
        }
        break;

    case 5:
        if (handle->button_level == handle->active_level)
        {
            // continue hold trigger
            handle->event = (uint8_t)LONG_PRESS_HOLD;
            SYS_LOG_INF("LONG_PRESS_HOLD");
            EVENT_CB(LONG_PRESS_HOLD);
        }
        else
        { // released
            handle->event = (uint8_t)PRESS_UP;
            SYS_LOG_INF("PRESS_UP");
            EVENT_CB(PRESS_UP);
            handle->state = 0; // reset
        }
        break;
    default:
        handle->state = 0; // reset
        break;
    }
}

/**
 * @brief  Start the button work, add the handle into work list.
 * @param  handle: target handle struct.
 * @retval 0: succeed. -1: already exist.
 */
int button_start(button_hdl_t *handle)
{
    button_hdl_t *target = head_handle;
    while (target)
    {
        if (target == handle)
            return -1; // already exist.
        target = target->next;
    }
    handle->next = head_handle;
    head_handle = handle;
    return 0;
}

/**
 * @brief  Stop the button work, remove the handle off work list.
 * @param  handle: target handle struct.
 * @retval None
 */
void button_stop(button_hdl_t *handle)
{
    button_hdl_t **curr;
    for (curr = &head_handle; *curr;)
    {
        button_hdl_t *entry = *curr;
        if (entry == handle)
        {
            *curr = entry->next;
            return; // glacier add 2021-8-18
        }
        else
        {
            curr = &entry->next;
        }
    }
}

/**
 * @brief  background ticks, timer repeat invoking interval 5ms.
 * @param  None.
 * @retval None
 */
void button_ticks(void)
{
    button_hdl_t *target;
    for (target = head_handle; target; target = target->next)
    {
        button_handler(target);
    }
}
