/*
 * Dauerhaft-CM3 WiFi curtain motor
 *
 * (C)2026 St(u)dio of Computer Games
 */

#include <stdint.h>
#include <string.h>


/* core */
#include "boards.h"
#include "nrf_mesh_config_core.h"
#include "nrf_mesh_gatt.h"
#include "nrf_mesh_configure.h"
#include "nrf_mesh_events.h"
#include "nrf_mesh.h"
#include "mesh_stack.h"
#include "device_state_manager.h"
#include "access_config.h"
#include "proxy.h"
#include "deferrer_flag.h"

/* provisioning and configuration */
#include "mesh_provisionee_no_oob.h"
#include "mesh_app_utils.h"

/* models */
#include "model_config_file.h"

/* application specific includes */
#include "nordic_common.h"
#include "nrf_mesh_config_app.h"
#include "nrf.h"
#include "example_common.h"
#include "nrf_drv_pwm.h"
#include "nrf_mesh_config_examples.h"
#include "ble_softdevice_support.h"
#include "app_timer.h"
#include "app_curtain_motor.h"
#include "app_scene.h"
#include "app_tuya.h"
#include "app_config.h"
#include "fsm.h"
#include "fsm_assistant.h"

/* logging */
#include "log.h"


/*** Definitions ***/

#define SCHED_MAX_EVENT_DATA_SIZE       (APP_TIMER_SCHED_EVENT_DATA_SIZE + sizeof(uint16_t))
#define SCHED_QUEUE_SIZE                (10)

/* Controls if the model instance should force all mesh messages to be segmented messages. */
#define APP_FORCE_SEGMENTATION          (false)
/* Controls the MIC size used by the model instance for sending the mesh messages. */
#define APP_MIC_SIZE                    (NRF_MESH_TRANSMIC_SIZE_SMALL)
/** The maximum message size in bytes. */
#define APP_MAX_MESSAGE_BYTES           (256)

#define APP_CURTAIN_MOTOR_ELEMENT_INDEX (0)

/* Tuya DP IDs */
#define TUYA_DPID_CMD                   (1)
#define TUYA_DPID_LEVEL                 (2)
#define TUYA_DPID_MOTOR_DIRECTION       (5)
#define TUYA_DPID_AUTO_POWER            (6)



/*** Type declaration */

typedef enum tuya_cm_cmd_e {
    TUYA_CM_CMD_OPEN = 0,
    TUYA_CM_CMD_STOP = 1,
    TUYA_CM_CMD_CLOSE = 2
} tuya_cm_cmd_t;



/*** Forward declarations */

/* models behavior */
static void app_curtain_motor_server_set_cb(const app_curtain_motor_server_t *p_app,
                                            uint16_t level);
static void app_curtain_motor_server_get_cb(const app_curtain_motor_server_t *p_app,
                                            uint16_t *present_level);
static void app_curtain_motor_transition_cb(const app_curtain_motor_server_t *p_app,
                                            uint32_t transition_time_ms, uint16_t target_level);


#if SCENE_SETUP_SERVER_INSTANCES_MAX > 0
static void app_curtain_motor_scene_transition_cb(const app_scene_setup_server_t *p_app,
                                                  uint32_t transition_time_ms,
                                                  uint16_t target_scene);
#endif


/* Tuya module */

static void app_tuya_evt_cb(app_tuya_t *p_app, app_tuya_evt_t evt);
static void app_tuya_dp_set_cb(app_tuya_t *p_app, const tuya_msg_dp_t *p_dp_msg);


/* proivisioning */
static void provisioning_complete_cb(void);
static void provisioning_abort_cb(void);

/* mesh basic */
static void mesh_events_handle(const nrf_mesh_evt_t *p_evt);
static void node_reset(void);



/*** Static variables */

APP_TUYA_DEF(m_tuya,
             app_tuya_evt_cb,
             app_tuya_dp_set_cb);
static bool m_tuya_initialize;
static tuya_cm_cmd_t m_tuya_cm_cmd;
static uint32_t m_tuya_cm_present_level;
static uint32_t m_tuya_cm_target_level;


/* Curtains Controller server structure definition and initialization */
APP_CURTAIN_MOTOR_SERVER_DEF(m_curtain_motor_server,
                             APP_FORCE_SEGMENTATION,
                             APP_MIC_SIZE,
                             app_curtain_motor_server_set_cb,
                             app_curtain_motor_server_get_cb,
                             app_curtain_motor_transition_cb
);

#if SCENE_SETUP_SERVER_INSTANCES_MAX > 0
/* Scene Setup server structure definition and initialization */
APP_SCENE_SETUP_SERVER_DEF(m_scene_server,
                           APP_FORCE_SEGMENTATION,
                           APP_MIC_SIZE,
                           app_curtain_motor_scene_transition_cb,
                           &m_curtain_motor_server.dtt_server);
#endif

static bool m_device_provisioned;
static nrf_mesh_evt_handler_t m_event_handler = {
    .evt_cb = mesh_events_handle,
};


static deferrer_flag_t m_app_tuya_init_flag;
static deferrer_flag_t m_app_tuya_reset_flag;
static deferrer_flag_t m_app_tuya_set_cmd_flag;
static deferrer_flag_t m_app_tuya_set_level_flag;
static deferrer_flag_t m_app_cm_set_level_flag;



/*** Helpers section ***/

static void unicast_address_print(void)
{
    dsm_local_unicast_address_t node_address;
    dsm_local_unicast_addresses_get(&node_address);
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "Node Address: 0x%04x\n", node_address.address_start);
}


static uint32_t cm_level_to_tuya_level(uint16_t cm_level_value)
{
    uint32_t tuya_level_value_inv = ((uint32_t)cm_level_value * 100 + (UINT16_MAX / 2)) / UINT16_MAX;
    return 100 - tuya_level_value_inv;
}


static uint16_t tuya_level_to_cm_level(uint32_t tuya_level_value)
{
    uint32_t tuya_level_value_inv = 100 - tuya_level_value;
    if (tuya_level_value_inv >= 99) return UINT16_MAX;
    if (tuya_level_value_inv <= 1) return 0;
    return (uint16_t)((tuya_level_value_inv * UINT16_MAX + 50) / 100);
}



/* Curtains Motor App */

static void app_curtain_motor_server_set_cb(const app_curtain_motor_server_t *p_app,
                                            uint16_t level)
{
    /* do nothing */
}


static void app_curtain_motor_server_get_cb(const app_curtain_motor_server_t *p_app,
                                            uint16_t *present_level)
{
    *present_level = tuya_level_to_cm_level(m_tuya_cm_present_level);
}


static void app_curtain_motor_transition_cb(const app_curtain_motor_server_t *p_app,
                                            uint32_t transition_time_ms, uint16_t target_level)
{
    m_tuya_cm_target_level = cm_level_to_tuya_level(target_level);
    deferrer_flag_set(m_app_cm_set_level_flag);
}


static void app_curtain_motor_scene_transition_cb(const app_scene_setup_server_t *p_app,
                                                  uint32_t transition_time_ms,
                                                  uint16_t target_scene)
{
    /* do nothing */
}



/* Tuya App */

static void app_tuya_evt_cb(app_tuya_t *p_app, app_tuya_evt_t evt)
{
    switch (evt) {
    case APP_TUYA_EVT_INIT_SUCCESS:
        m_tuya_initialize = true;
        deferrer_flag_set(m_app_tuya_init_flag);
        break;

    case APP_TUYA_EVT_WIFI_STATE_FAIL:
        deferrer_flag_set(m_app_tuya_init_flag);
        break;

#if 0
    case APP_TUYA_EVT_HEARTBEAT_SUCCESS:
        UNUSED_VARIABLE(app_tuya_query_state(&m_tuya));
        break;
#endif

    case APP_TUYA_EVT_RESET:
        deferrer_flag_set(m_app_tuya_reset_flag);
        break;

    default:
        break;
    }
}


static void app_tuya_dp_set_cb(app_tuya_t *p_app, const tuya_msg_dp_t *p_dp_msg)
{
    if (p_dp_msg->id == TUYA_DPID_CMD && p_dp_msg->type == TUYA_DP_TYPE_ENUM) {
        m_tuya_cm_cmd = (uint8_t)p_dp_msg->val.enum_value;
        deferrer_flag_set(m_app_tuya_set_cmd_flag);
        __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Tuya DPID CMD %d\n", m_tuya_cm_cmd);
    } else if (p_dp_msg->id == TUYA_DPID_LEVEL && p_dp_msg->type == TUYA_DP_TYPE_VALUE) {
        m_tuya_cm_present_level = p_dp_msg->val.value;
        deferrer_flag_set(m_app_tuya_set_level_flag);
        __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Tuya DPID LEVEL %d\n", m_tuya_cm_present_level);
#if NRF_MESH_LOG_ENABLE == 1
    } else if (p_dp_msg->id == TUYA_DPID_MOTOR_DIRECTION && p_dp_msg->type == TUYA_DP_TYPE_ENUM) {
        __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Tuya DPID MOTOR_DIRECTION %d\n", p_dp_msg->val.enum_value);
    } else if (p_dp_msg->id == TUYA_DPID_AUTO_POWER && p_dp_msg->type == TUYA_DP_TYPE_BOOL) {
        __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Tuya DPID AUTO_POWER %d\n", p_dp_msg->val.boolean);
    } else if (p_dp_msg->id == 7 && p_dp_msg->type == TUYA_DP_TYPE_ENUM) {
        __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Tuya DPID 7(Unk) %d\n", p_dp_msg->val.enum_value);
    } else {
        __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Unknown Tuya DPID %d, type %d\n", p_dp_msg->id, p_dp_msg->type);
#endif /* NRF_MESH_LOG_ENABLE */
    }
}


static void app_tuya_init_flag_cb(void *p_data)
{
    ERROR_CHECK(app_tuya_wifi_state(&m_tuya,
                mesh_stack_is_device_provisioned() ?
                    TUYA_WIFI_STATE_CONNECTED_TO_CLOUD :
                    TUYA_WIFI_STATE_NOT_CONNECTED));
}


static void app_tuya_reset_flag_cb(void *p_data)
{
    node_reset();
}


static void app_tuya_set_cmd_flag_cb(void *p_data)
{
    if ((m_tuya_cm_cmd == TUYA_CM_CMD_OPEN && m_tuya_cm_target_level > 0) ||
        (m_tuya_cm_cmd == TUYA_CM_CMD_CLOSE && m_tuya_cm_target_level < 100))
    {
        UNUSED_VARIABLE(app_tuya_set_dp_uint32(&m_tuya, TUYA_DPID_LEVEL, m_tuya_cm_target_level));
        __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Send Level %d\n", m_tuya_cm_target_level);
    } else if (m_tuya_cm_cmd == TUYA_CM_CMD_STOP) {
        UNUSED_VARIABLE(app_tuya_query_state(&m_tuya));
    }
}


static void app_tuya_set_level_flag_cb(void *p_data)
{
    UNUSED_VARIABLE(app_curtain_motor_current_value_publish(&m_curtain_motor_server));
}


static void app_cm_set_level_flag_cb(void *p_data)
{
    if (m_tuya_cm_cmd == TUYA_CM_CMD_STOP) {
        if (m_tuya_cm_target_level < m_tuya_cm_present_level) {
            UNUSED_VARIABLE(app_tuya_set_dp_enum(&m_tuya, TUYA_DPID_CMD, TUYA_CM_CMD_OPEN));
            __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Send CMD Open\n");
        } else if (m_tuya_cm_target_level > m_tuya_cm_present_level) {
            UNUSED_VARIABLE(app_tuya_set_dp_enum(&m_tuya, TUYA_DPID_CMD, TUYA_CM_CMD_CLOSE));
            __LOG(LOG_SRC_APP, LOG_LEVEL_DBG1, "Send CMD Close\n");
        }
    }
}



/*** Provisionoing section */

static void provisioning_complete_cb(void)
{
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "Successfully provisioned\n");

#if MESH_FEATURE_GATT_ENABLED
    /* Restores the application parameters after switching from the Provisioning
     * service to the Proxy  */
    gap_params_init();
    conn_params_init();
#endif

    ERROR_CHECK(app_tuya_wifi_state(&m_tuya, TUYA_WIFI_STATE_CONNECTED_TO_CLOUD));

    unicast_address_print();
}


static void provisioning_abort_cb(void)
{
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "Provisioning abort\n");

    ERROR_CHECK(app_tuya_wifi_state(&m_tuya, TUYA_WIFI_STATE_NOT_CONNECTED));
}



/*** mesh basic section ***/

static void mesh_events_handle(const nrf_mesh_evt_t * p_evt)
{
    if (p_evt->type == NRF_MESH_EVT_ENABLED) {
        __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "----- NRF_MESH_EVT_ENABLED  -----\n");

        APP_ERROR_CHECK(app_curtain_motor_binding_setup(&m_curtain_motor_server));

        /* start TUYA module */
        UNUSED_VARIABLE(app_tuya_start(&m_tuya));
    }
}


static void node_reset(void)
{
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "----- NODE RESET ----- \n");

#if MESH_FEATURE_GATT_PROXY_ENABLED
    proxy_stop();
#endif
    mesh_stack_config_clear();

    /* this function may return if there are ongoing flash operations */
    model_config_file_clear();
    mesh_stack_device_reset();
}


static void config_server_evt_cb(const config_server_evt_t * p_evt)
{
    if (p_evt->type == CONFIG_SERVER_EVT_NODE_RESET) {
        node_reset();
    }
}


static void models_init_cb(void)
{
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "Initializing and adding models\n");

    /* instantiate onoff server on element index APP_ONOFF_ELEMENT_INDEX */
    ERROR_CHECK(app_curtain_motor_init(&m_curtain_motor_server, APP_CURTAIN_MOTOR_ELEMENT_INDEX));
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "    App OnOff Model Handle: %d\n", m_curtain_motor_server.onoff_server.model_handle);
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "    App Level Model Handle: %d\n", m_curtain_motor_server.level_server.model_handle);
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "    App Motor DTT Model Handle: %d\n", m_curtain_motor_server.dtt_server.model_handle);

#if SCENE_SETUP_SERVER_INSTANCES_MAX > 0
    /* Instantiate scene server and register onoff server to have scene support */
    ERROR_CHECK(app_scene_model_init(&m_scene_server, APP_CURTAIN_MOTOR_ELEMENT_INDEX));
    ERROR_CHECK(app_scene_model_add(&m_scene_server, &m_curtain_motor_server.scene_if));
    ERROR_CHECK(app_curtain_motor_scene_context_set(&m_curtain_motor_server, &m_scene_server));
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "    App Scene Model Handle: %d\n", m_scene_server.scene_setup_server.model_handle);
#endif
}


static void mesh_init(void)
{
    /* initialize the application storage for models */
    model_config_file_init();

    mesh_stack_init_params_t init_params = {
        .core.irq_priority       = NRF_MESH_IRQ_PRIORITY_LOWEST,
        .core.lfclksrc           = DEV_BOARD_LF_CLK_CFG,
        .core.p_uuid             = NULL,
        .models.models_init_cb   = models_init_cb,
        .models.config_server_cb = config_server_evt_cb
    };

    uint32_t status = mesh_stack_init(&init_params, &m_device_provisioned);

    if (status == NRF_SUCCESS) {
        /* check if application stored data is valid, if not clear all data and use default values */
        status = model_config_file_config_apply();
    }

    switch (status) {
    case NRF_ERROR_INVALID_DATA:
        /* clear model config file as loading failed */
        model_config_file_clear();
        __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "Data in the persistent memory was corrupted. Device starts as unprovisioned.\n");
        __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "Reboot device before starting of the provisioning process.\n");
        mesh_stack_device_reset();
        break;
    case NRF_SUCCESS:
        break;
    default:
        ERROR_CHECK(status);
    }
}


static void initialize(void)
{

    __LOG_INIT(LOG_SRC_APP, LOG_LEVEL_DBG1, LOG_CALLBACK_DEFAULT);
    __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "----- %s -----\n", GAP_DEVICE_NAME);

    ERROR_CHECK(app_timer_init());

    deferrer_flag_init();
    m_app_tuya_init_flag = deferrer_flag_add(app_tuya_init_flag_cb, NULL);
    m_app_tuya_reset_flag = deferrer_flag_add(app_tuya_reset_flag_cb, NULL);
    m_app_tuya_set_cmd_flag = deferrer_flag_add(app_tuya_set_cmd_flag_cb, NULL);
    m_app_tuya_set_level_flag = deferrer_flag_add(app_tuya_set_level_flag_cb, NULL);
    m_app_cm_set_level_flag = deferrer_flag_add(app_cm_set_level_flag_cb, NULL);

    m_tuya_initialize = false;
    m_tuya_cm_cmd = TUYA_CM_CMD_STOP;
    m_tuya_cm_present_level = m_tuya_cm_target_level = 0;
    app_tuya_init(&m_tuya);

    ble_stack_init();

#if MESH_FEATURE_GATT_ENABLED
    gap_params_init();
    conn_params_init();
#endif

    mesh_init();
}


static void start(void)
{
    if (!m_device_provisioned) {
        __LOG(LOG_SRC_APP, LOG_LEVEL_INFO, "Device is unprovisioned\n");

        mesh_provisionee_start_params_t prov_start_params = {
            .prov_sd_ble_opt_set_cb = NULL,
            .prov_complete_cb = provisioning_complete_cb,
            .prov_device_identification_start_cb = NULL,
            .prov_device_identification_stop_cb = NULL,
            .prov_abort_cb = provisioning_abort_cb,
            .prov_output_request_cb = NULL,
            .p_device_uri = EX_URI_DM_SERVER
        };
        ERROR_CHECK(mesh_provisionee_prov_start(&prov_start_params));
    } else {
        unicast_address_print();
    }

    mesh_app_uuid_print(nrf_mesh_configure_device_uuid_get());

    /* NRF_MESH_EVT_ENABLED is triggered in the mesh IRQ context after the stack is fully enabled.
     * This event is used to call Model APIs for establishing bindings and publish a model state information. */
    nrf_mesh_evt_handler_add(&m_event_handler);
    ERROR_CHECK(mesh_stack_start());
}



/*** Entry point */

int main(void)
{
    initialize();
    start();

    for (;;) {
        (void)sd_app_evt_wait();
    }
}
