#include "fsm.h"
#include "fsm_assistant.h"
#include "app_tuya_zigbee.h"
#include "app_timer.h"
#include "utils.h"

#include "log.h"


#if defined APP_TUYA_MODE_GENERAL || defined APP_TUYA_MODE_ZIGBEE
#error "Tuya General and Tuya ZigBee is not implemented now"
#endif


/*** Defines */

#define TUYA_SET_DP_HEADER_SIZE         4

#define TUYA_INITIALIZATION_MAX         3

#define TUYA_START_INTERVAL_MS          SEC_TO_MS(3)
#define TUYA_CMD_TIMEOUT_MS             SEC_TO_MS(3)

#define EVENT_LIST  E_START,                    \
                    E_STOP,                     \
                    E_TIMEOUT,                  \
                    E_HEARTBEAT_ACK,            \
                    E_QUERY_PRODUCT_ACK,        \
                    E_MCU_CONF_ACK,             \
                    E_NETWORK_STATUS,           \
                    E_NETWORK_STATUS_ACK,       \
                    E_SYNC_CLOCK_TIME,          \
                    E_SYNC_CLOCK_TIME_ACK       \

#define STATE_LIST  S_IDLE,             \
                    S_QUERY_PRODUCT,    \
                    S_READY,            \
                    S_NETWORK_STATUS,   \
                    S_SYNC_CLOCK_TIME

#define ACTION_LIST A_STOP,                             a_stop,                         \
                    A_INIT_FAIL,                        a_init_fail,                    \
                    A_SEND_QUERY_PRODUCT,               a_send_query_product,           \
                    A_INIT_SUCCESS,                     a_init_success,                 \
                    A_SEND_NETWORK_STATUS_SUCCESS,      a_send_network_status_success,  \
                    A_SEND_NETWORK_STATUS_FAIL,         a_send_network_status_fail


#define GUARD_LIST G_IS_INIT_LAST_ATTEMPT,              g_is_init_last_attempt


/*** Type definitions */

typedef enum {
    DECLARE_ENUM(EVENT_LIST)
} app_tuya_event_ids_t;

typedef enum {
    DECLARE_ENUM(STATE_LIST)
} app_tuya_state_ids_t;

typedef enum {
    DECLARE_ENUM_PAIR(ACTION_LIST)
} app_tuya_action_ids_t;

typedef enum {
    DECLARE_ENUM_PAIR(GUARD_LIST)
} app_tuya_guard_ids_t;


static const fsm_transition_t m_app_tuya_fsm_transition_table[] = {
    FSM_STATE(S_IDLE),
    FSM_TRANSITION(E_START,             FSM_ALWAYS,             A_SEND_QUERY_PRODUCT,           S_QUERY_PRODUCT),

    FSM_STATE(S_QUERY_PRODUCT),
    FSM_TRANSITION(E_TIMEOUT,           G_IS_INIT_LAST_ATTEMPT, A_INIT_FAIL,                    S_IDLE),
    FSM_TRANSITION(E_TIMEOUT,           FSM_ALWAYS,             A_SEND_QUERY_PRODUCT,           FSM_SAME_STATE),
    FSM_TRANSITION(E_QUERY_PRODUCT_ACK, FSM_ALWAYS,             A_INIT_SUCCESS,                 S_READY),
    FSM_TRANSITION(E_STOP,              FSM_ALWAYS,             A_STOP,                         S_IDLE),

    FSM_STATE(S_READY),
    FSM_TRANSITION(E_NETWORK_STATUS,    FSM_ALWAYS,             FSM_NO_ACTION,                  S_NETWORK_STATUS),
    FSM_TRANSITION(E_STOP,              FSM_ALWAYS,             A_STOP,                         S_IDLE),

    FSM_STATE(S_NETWORK_STATUS),
    FSM_TRANSITION(E_TIMEOUT,                   FSM_ALWAYS,     A_SEND_NETWORK_STATUS_FAIL,     S_READY),
    FSM_TRANSITION(E_NETWORK_STATUS_ACK,        FSM_ALWAYS,     A_SEND_NETWORK_STATUS_SUCCESS,  S_READY),
    FSM_TRANSITION(E_STOP,                      FSM_ALWAYS,     A_STOP,                         S_IDLE),
};


#if FSM_DEBUG
static const char * m_action_lookup_table[] =
{
    DECLARE_STRING_PAIR(ACTION_LIST)
};

static const char * m_guard_lookup_table[] =
{
    DECLARE_STRING_PAIR(GUARD_LIST)
};

static const char * m_event_lookup_table[] =
{
    DECLARE_STRING(EVENT_LIST)
};

static const char * m_state_lookup_table[] =
{
    DECLARE_STRING(STATE_LIST)
};
#endif  /* FSM_DEBUG */



/*** Forward declarations */

typedef void (* app_tuya_fsm_action_t)(void *);
typedef bool (* app_tuya_fsm_guard_t)(void *);

DECLARE_ACTION_PROTOTYPE(ACTION_LIST)
DECLARE_GUARD_PROTOTYPE(GUARD_LIST)

static void app_tuya_fsm_action(fsm_action_id_t action_id, void * p_data);
static bool app_tuya_fsm_guard(fsm_guard_id_t guard_id, void * p_data);



/*** Static variables */

static app_tuya_t *mp_app = NULL;

static const fsm_const_descriptor_t m_app_tuya_fsm_descriptor =
{
    .transition_table = m_app_tuya_fsm_transition_table,
    .transitions_count = ARRAY_SIZE(m_app_tuya_fsm_transition_table),
    .initial_state = S_IDLE,
    .guard = app_tuya_fsm_guard,
    .action = app_tuya_fsm_action,
#if FSM_DEBUG
    .fsm_name = "app_tuya_fsm",
    .action_lookup = m_action_lookup_table,
    .event_lookup = m_event_lookup_table,
    .guard_lookup = m_guard_lookup_table,
    .state_lookup = m_state_lookup_table
#endif  /* FSM_DEBUG */
};


static const app_tuya_fsm_action_t app_tuya_fsm_actions[] =
{
    DECLARE_HANDLER(ACTION_LIST)
};

static const app_tuya_fsm_guard_t app_tuya_fsm_guards[] =
{
    DECLARE_HANDLER(GUARD_LIST)
};


/*** Implementation */

static inline void app_tuya_event(app_tuya_t *p_app, app_tuya_evt_t event)
{
    if (p_app->evt_cb != NULL) {
        p_app->evt_cb(p_app, event);
    }
}



static void app_tuya_fsm_action(fsm_action_id_t action_id, void * p_data)
{
    app_tuya_fsm_actions[action_id](p_data);
}


/* FSM guards */

static bool app_tuya_fsm_guard(fsm_guard_id_t guard_id, void * p_data)
{
    return app_tuya_fsm_guards[guard_id](p_data);
}


/* FSM actions */

static void a_stop(void *p_data)
{
    app_tuya_t *p_app = (app_tuya_t *)p_data;

    app_tuya_event(p_app, APP_TUYA_EVT_STOP);
}


static void a_init_fail(void *p_data)
{
    app_tuya_t *p_app = (app_tuya_t *)p_data;

    if (p_app->evt_cb != NULL) {
        p_app->evt_cb(p_app, APP_TUYA_EVT_INIT_FAIL);
    }
}


static void a_send_query_product(void *p_data)
{
    app_tuya_t *p_app = (app_tuya_t *)p_data;

    UNUSED_VARIABLE(tuya_send_query_product());
    APP_ERROR_CHECK(app_timer_start(*p_app->p_timer_id,
                                    APP_TIMER_TICKS(TUYA_START_INTERVAL_MS),
                                    p_app));
}


static void a_init_success(void * p_data)
{
    app_tuya_t *p_app = (app_tuya_t *)p_data;

    app_tuya_event(p_app, APP_TUYA_EVT_INIT_SUCCESS);

    UNUSED_VARIABLE(tuya_send_query_state());
}


static void a_send_network_status_success(void * p_data)
{
    app_tuya_t *p_app = (app_tuya_t *)p_data;

    app_tuya_event(p_app, APP_TUYA_EVT_NETWORK_STATUS_SUCCESS);
}


static void a_send_network_status_fail(void * p_data)
{
    app_tuya_t *p_app = (app_tuya_t *)p_data;

    app_tuya_event(p_app, APP_TUYA_EVT_NETWORK_STATUS_FAIL);
}



static bool g_is_init_last_attempt(void * p_data)
{
    app_tuya_t *p_app = (app_tuya_t *)p_data;

    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "%s(): initialization_cnt=%d\n", __func__, p_app->initialization_cnt);

    p_app->initialization_cnt++;
    return p_app->initialization_cnt >= TUYA_INITIALIZATION_MAX;
}



static void tuya_event_handler(tuya_evt_type_t evt, const tuya_msg_t *p_msg)
{
    app_tuya_t *p_app = mp_app;

    switch (evt) {
    case TUYA_MSG:
        switch (p_msg->cmd) {
        case TUYA_CMD_QUERY_PRODUCT:
#if NRF_MESH_LOG_ENABLE == 1
            {
                char str[256];
                strncpy(str, (char *)p_msg->data, p_msg->len);
                str[p_msg->len] = '\0';
                __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "MCU Product: %s\n", str);
            }
#endif /* NRF_MESH_LOG_ENABLE */
            fsm_event_post(&p_app->fsm, E_QUERY_PRODUCT_ACK, p_app);
            break;

        case TUYA_CMD_REPORT_STATUS:
            if (p_app->dp_set_cb != NULL) {
                tuya_msg_dp_t dp_msg;
                if (tuya_parse_dp(p_msg->data, p_msg->len, &dp_msg) == NRF_SUCCESS) {
                    p_app->dp_set_cb(p_app, &dp_msg);
                } else {
                    __LOG(LOG_SRC_APP, LOG_LEVEL_ERROR, "Failed to parse Tuya message\n");
                }
                UNUSED_VARIABLE(tuya_resp_report_status(p_msg->seq, true));
            }
            break;

        case TUYA_CMD_NETWORK_STATUS:
            __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "TUYA_CMD_NETWORK_STATUS\n");
            fsm_event_post(&p_app->fsm, E_NETWORK_STATUS_ACK, p_app);
            break;

        case TUYA_CMD_MODULE_CONFIGURE:
            __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "TUYA_CMD_MODULE_CONFIGURE %02x\n", p_msg->data[0]);
            tuya_resp_module_configure(p_msg->seq);
            if (p_msg->data[0] == TUYA_MODULE_CONFIGURE_NETWORK_RESET) {
                app_tuya_event(p_app, APP_TUYA_EVT_RESET);
            }
            break;

        case TUYA_CMD_SYNC_CLOCK_TIME:
            __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "TUYA_CMD_SYNC_CLOCK_TIME, seq=0x%x\n", p_msg->seq);
            p_app->sync_clock_time_seq = p_msg->seq;
            app_tuya_event(p_app, APP_TUYA_EVT_SYNC_CLOCK_TIME);
            break;

        default:
#if NRF_MESH_LOG_ENABLE == 1
            {
                int i;
                __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Unsupported Tuya command 0x%02x, size %d\n",
                      p_msg->cmd, p_msg->len);
                for (i = 0; i < p_msg->len; i++) {
                    __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "%02x\n", p_msg->data[i]);
                }
            }
#endif /* NRF_MESH_LOG_ENABLE */
            break;
        }
        break;

    case TUYA_WAKEUP:
        app_tuya_event(p_app, APP_TUYA_EVT_WAKEUP);
        break;

    case TUYA_SLEEP:
        app_tuya_event(p_app, APP_TUYA_EVT_SLEEP);
        break;

    case TUYA_INVALID_SIZE:
        __LOG(LOG_SRC_APP, LOG_LEVEL_ERROR, "invalid size of Tuya message, size %d\n", p_msg->len);
        break;

    case TUYA_INVALID_CRC:
        __LOG(LOG_SRC_APP, LOG_LEVEL_ERROR, "invalid CRC of Tuya message\n");
        break;
    }
}


static void tuya_timeout_handler(void * p_context)
{
    app_tuya_t *p_app = (app_tuya_t *)p_context;
    fsm_event_post(&p_app->fsm, E_TIMEOUT, p_app);
}



/*** Interface functions */

uint32_t app_tuya_init(app_tuya_t *p_app)
{
    uint32_t status;

    if (mp_app != NULL) {
        return NRF_ERROR_NO_MEM;
    }
    mp_app = p_app;

    /* create Tuya heartbead timer */
    status = app_timer_create(p_app->p_timer_id,
                              APP_TIMER_MODE_SINGLE_SHOT,
                              tuya_timeout_handler);
    if (status != NRF_SUCCESS) {
        return status;
    }

    p_app->initialization_cnt = 0;
    p_app->sync_clock_time_seq = TUYA_SEQ_MAX;

    fsm_init(&p_app->fsm, &m_app_tuya_fsm_descriptor);

    return tuya_init(tuya_event_handler);
}


uint32_t app_tuya_start(app_tuya_t *p_app)
{
    if (p_app->fsm.current_state != S_IDLE) {
        return NRF_ERROR_INVALID_STATE;
    }

    p_app->initialization_cnt = 0;
    fsm_event_post(&p_app->fsm, E_START, p_app);

    return NRF_SUCCESS;
}


uint32_t app_tuya_stop(app_tuya_t *p_app)
{
    fsm_event_post(&p_app->fsm, E_STOP, p_app);

    return NRF_SUCCESS;
}


uint32_t app_tuya_query_state(app_tuya_t *p_app)
{
    if (p_app->fsm.current_state != S_READY) {
        return NRF_ERROR_INVALID_STATE;
    }

    return tuya_send_query_state();
}


uint32_t app_tuya_network_status(app_tuya_t *p_app, uint8_t network_status_value)
{
    uint32_t status;

    if (p_app->fsm.current_state != S_READY) {
        return NRF_ERROR_INVALID_STATE;
    }

    fsm_event_post(&p_app->fsm, E_NETWORK_STATUS, p_app);
    status = tuya_send_network_status(network_status_value);
    if (status != NRF_SUCCESS) {
        return status;
    }
    return app_timer_start(*p_app->p_timer_id,
                           APP_TIMER_TICKS(TUYA_CMD_TIMEOUT_MS),
                           p_app);
}


uint32_t app_tuya_sync_clock_time(app_tuya_t *p_app, uint32_t time, uint32_t local_time)
{
    uint32_t status;

    if (p_app->fsm.current_state != S_READY || p_app->sync_clock_time_seq == TUYA_SEQ_MAX) {
        return NRF_ERROR_INVALID_STATE;
    }

    __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "%s: send time %u to MCU, seq 0x%x\n",
          __func__, local_time, p_app->sync_clock_time_seq);

    status = tuya_resp_sync_clock_time(p_app->sync_clock_time_seq, time, local_time);
    if (status == NRF_SUCCESS) {
        p_app->sync_clock_time_seq = TUYA_SEQ_MAX;
    }
    return status;
}


uint32_t app_tuya_set_dp_bool(app_tuya_t *p_app, uint8_t dpid, bool value)
{
    uint8_t payload_buffer[TUYA_SET_DP_HEADER_SIZE + sizeof(uint8_t)];

    if (p_app->fsm.current_state != S_READY) {
        return NRF_ERROR_INVALID_STATE;
    }

    payload_buffer[0] = dpid;
    payload_buffer[1] = TUYA_DP_TYPE_BOOL;
    payload_buffer[2] = 0x00;                   /* length H */
    payload_buffer[3] = sizeof(uint8_t);        /* length L */
    payload_buffer[4] = (uint8_t)value;

    return tuya_send_cmd(TUYA_CMD_SET_DP, payload_buffer, sizeof(payload_buffer));
}


uint32_t app_tuya_set_dp_enum(app_tuya_t *p_app, uint8_t dpid, uint8_t value)
{
    uint8_t payload_buffer[TUYA_SET_DP_HEADER_SIZE + sizeof(uint8_t)];

    if (p_app->fsm.current_state != S_READY) {
        return NRF_ERROR_INVALID_STATE;
    }

    payload_buffer[0] = dpid;
    payload_buffer[1] = TUYA_DP_TYPE_ENUM;
    payload_buffer[2] = 0x00;                   /* length H */
    payload_buffer[3] = sizeof(uint8_t);        /* length L */
    payload_buffer[4] = value;

    return tuya_send_cmd(TUYA_CMD_SET_DP, payload_buffer, sizeof(payload_buffer));
}


uint32_t app_tuya_set_dp_uint8(app_tuya_t *p_app, uint8_t dpid, uint8_t value)
{
    uint8_t payload_buffer[TUYA_SET_DP_HEADER_SIZE + sizeof(uint8_t)];

    if (p_app->fsm.current_state != S_READY) {
        return NRF_ERROR_INVALID_STATE;
    }

    payload_buffer[0] = dpid;
    payload_buffer[1] = TUYA_DP_TYPE_VALUE;
    payload_buffer[2] = 0x00;                   /* length H */
    payload_buffer[3] = sizeof(uint8_t);        /* length L */
    payload_buffer[4] = value;

    return tuya_send_cmd(TUYA_CMD_SET_DP, payload_buffer, sizeof(payload_buffer));
}


uint32_t app_tuya_set_dp_uint16(app_tuya_t *p_app, uint8_t dpid, uint16_t value)
{
    uint8_t payload_buffer[TUYA_SET_DP_HEADER_SIZE + sizeof(uint16_t)];

    if (p_app->fsm.current_state != S_READY) {
        return NRF_ERROR_INVALID_STATE;
    }

    payload_buffer[0] = dpid;
    payload_buffer[1] = TUYA_DP_TYPE_VALUE;
    payload_buffer[2] = 0x00;                   /* length H */
    payload_buffer[3] = sizeof(uint16_t);       /* length L */
    payload_buffer[4] = (value >> 8) & 0xff;
    payload_buffer[5] = (value >> 0) & 0xff;

    return tuya_send_cmd(TUYA_CMD_SET_DP, payload_buffer, sizeof(payload_buffer));
}


uint32_t app_tuya_set_dp_uint32(app_tuya_t *p_app, uint8_t dpid, uint32_t value)
{
    uint8_t payload_buffer[TUYA_SET_DP_HEADER_SIZE + sizeof(uint32_t)];

    if (p_app->fsm.current_state != S_READY) {
        return NRF_ERROR_INVALID_STATE;
    }

    payload_buffer[0] = dpid;
    payload_buffer[1] = TUYA_DP_TYPE_VALUE;
    payload_buffer[2] = 0x00;                   /* length H */
    payload_buffer[3] = sizeof(uint32_t);       /* length L */
    payload_buffer[4] = (value >> 24) & 0xff;
    payload_buffer[5] = (value >> 16) & 0xff;
    payload_buffer[6] = (value >> 8) & 0xff;
    payload_buffer[7] = (value >> 0) & 0xff;

    return tuya_send_cmd(TUYA_CMD_SET_DP, payload_buffer, sizeof(payload_buffer));
}
