/*
 * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include "lua_module_ir.h"

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

#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"

#include "cap_lua.h"
#include "driver/gpio.h"
#include "driver/rmt_rx.h"
#include "driver/rmt_tx.h"
#include "esp_board_manager.h"
#include "esp_check.h"
#include "esp_log.h"
#include "ir_encoder.h"
#include "lauxlib.h"
#include "sdkconfig.h"

#define LUA_MODULE_IR_METATABLE      "ir.device"
#define LUA_MODULE_IR_DEFAULT_NAME   "ir_blaster"
#define LUA_MODULE_IR_MAX_NAME_LEN   64

#define LUA_MODULE_IR_TX_QUEUE_DEPTH 4
#define LUA_MODULE_IR_TX_MEM_BLOCK   64
#define LUA_MODULE_IR_RX_MEM_BLOCK   128
#define LUA_MODULE_IR_RX_MIN_NS      1250
#define LUA_MODULE_IR_RX_MAX_NS      32000000

/*
 * NEC protocol timings (microseconds) - leader and bit windows.
 */
#define LUA_IR_NEC_LEADING_HIGH_US   9000
#define LUA_IR_NEC_LEADING_LOW_US    4500
#define LUA_IR_NEC_BIT_HIGH_US       560
#define LUA_IR_NEC_ZERO_LOW_US       560
#define LUA_IR_NEC_ONE_LOW_US        1690
#define LUA_IR_NEC_ENDING_HIGH_US    560

static const char *TAG = "lua_module_ir";

typedef struct {
    int tx_gpio_num;
    int rx_gpio_num;
    int ctrl_gpio_num;
    int ctrl_active_level;
    uint32_t carrier_hz;
    uint32_t tx_resolution_hz;
    uint32_t rx_resolution_hz;
    int rx_max_symbols;
} lua_ir_resolved_cfg_t;

typedef struct {
    rmt_channel_handle_t tx_channel;
    rmt_channel_handle_t rx_channel;
    rmt_encoder_handle_t encoder;
    QueueHandle_t        rx_queue;
    rmt_symbol_word_t   *rx_buffer;
    int                  tx_gpio_num;
    int                  rx_gpio_num;
    int                  ctrl_gpio_num;
    int                  ctrl_active_level;
    uint32_t             carrier_hz;
    uint32_t             tx_resolution_hz;
    uint32_t             rx_resolution_hz;
    int                  rx_max_symbols;
    bool                 tx_enabled;
    bool                 rx_enabled;
} lua_module_ir_handle_t;

typedef struct {
    lua_module_ir_handle_t *handle;
    char device_name[LUA_MODULE_IR_MAX_NAME_LEN];
} lua_module_ir_ud_t;

/*
 * Local mirror of the dev_custom_ir_blaster_config_t struct generated by the
 * ESP Board Manager from board_devices.yaml. Keep this layout in sync with the
 * generator output so custom IR device defaults are read with the correct
 * field offsets on every board. When the board does not declare such a device,
 * `lua_module_ir_load_board_defaults` simply returns ESP_ERR_NOT_FOUND and the
 * caller-supplied Lua options are used instead.
 */
typedef struct {
    const char *name;
    const char *type;
    const char *chip;
    int8_t      tx_gpio_num;
    int8_t      rx_gpio_num;
    int8_t      ctrl_gpio_num;
    int8_t      ctrl_active_level;
    int32_t     carrier_hz;
} lua_ir_board_cfg_t;

/* -------------------------------------------------------------------------- */
/* Helpers                                                                    */
/* -------------------------------------------------------------------------- */

static lua_module_ir_ud_t *lua_module_ir_get_ud(lua_State *L, int idx)
{
    lua_module_ir_ud_t *ud =
        (lua_module_ir_ud_t *)luaL_checkudata(L, idx, LUA_MODULE_IR_METATABLE);
    if (!ud || !ud->handle) {
        luaL_error(L, "ir: invalid or closed handle");
    }
    return ud;
}

static bool IRAM_ATTR lua_ir_rx_done_cb(rmt_channel_handle_t channel,
                                        const rmt_rx_done_event_data_t *edata,
                                        void *user_ctx)
{
    BaseType_t high_task_wakeup = pdFALSE;
    QueueHandle_t queue = (QueueHandle_t)user_ctx;
    xQueueSendFromISR(queue, edata, &high_task_wakeup);
    return high_task_wakeup == pdTRUE;
}

/*
 * Drive the IR module enable pin (if any) into its active state. On boards
 * like the ESP32-S3-BOX-3 this single ctrl GPIO powers BOTH the IR
 * transmitter LED *and* the IR receiver front-end, so it must be asserted
 * regardless of whether the user is about to send or receive. The pin is
 * idempotent; calling this multiple times is safe.
 */
static void lua_module_ir_assert_ctrl(lua_module_ir_handle_t *h)
{
    if (h->ctrl_gpio_num < 0) {
        return;
    }
    gpio_config_t io_conf = {
        .pin_bit_mask = BIT64(h->ctrl_gpio_num),
        .mode = GPIO_MODE_OUTPUT,
        .pull_up_en = GPIO_PULLUP_ENABLE,
        .pull_down_en = GPIO_PULLDOWN_DISABLE,
        .intr_type = GPIO_INTR_DISABLE,
    };
    (void)gpio_config(&io_conf);
    (void)gpio_set_level(h->ctrl_gpio_num, h->ctrl_active_level ? 1 : 0);
}

static esp_err_t lua_module_ir_ensure_tx(lua_module_ir_handle_t *h)
{
    if (h->tx_channel != NULL) {
        return ESP_OK;
    }

    rmt_tx_channel_config_t tx_cfg = {
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = h->tx_resolution_hz,
        .mem_block_symbols = LUA_MODULE_IR_TX_MEM_BLOCK,
        .trans_queue_depth = LUA_MODULE_IR_TX_QUEUE_DEPTH,
        .gpio_num = h->tx_gpio_num,
    };
    ESP_RETURN_ON_ERROR(rmt_new_tx_channel(&tx_cfg, &h->tx_channel),
                        TAG, "Failed to create RMT TX channel");

    rmt_carrier_config_t carrier_cfg = {
        .duty_cycle = 0.33f,
        .frequency_hz = h->carrier_hz,
    };
    ESP_RETURN_ON_ERROR(rmt_apply_carrier(h->tx_channel, &carrier_cfg),
                        TAG, "Failed to apply IR carrier");

    ir_encoder_config_t enc_cfg = {
        .resolution = h->tx_resolution_hz,
    };
    ESP_RETURN_ON_ERROR(ir_encoder_new(&enc_cfg, &h->encoder),
                        TAG, "Failed to create IR encoder");

    ESP_RETURN_ON_ERROR(rmt_enable(h->tx_channel), TAG, "Failed to enable TX channel");
    h->tx_enabled = true;

    lua_module_ir_assert_ctrl(h);
    return ESP_OK;
}

static esp_err_t lua_module_ir_ensure_rx(lua_module_ir_handle_t *h)
{
    if (h->rx_channel != NULL) {
        return ESP_OK;
    }

    /*
     * Power up the IR module BEFORE creating the RMT channel so the analog
     * front-end has a chance to settle while we configure the peripheral.
     * On boards where ctrl_gpio is shared between TX and RX (e.g. the
     * ESP32-S3-BOX-3 uses GPIO44 as a single enable), skipping this on the
     * RX path made the very first dev:receive() always time out: the
     * receiver was simply unpowered until something later called ensure_tx.
     */
    lua_module_ir_assert_ctrl(h);

    rmt_rx_channel_config_t rx_cfg = {
        .clk_src = RMT_CLK_SRC_DEFAULT,
        .resolution_hz = h->rx_resolution_hz,
        .mem_block_symbols = LUA_MODULE_IR_RX_MEM_BLOCK,
        .gpio_num = h->rx_gpio_num,
    };
    ESP_RETURN_ON_ERROR(rmt_new_rx_channel(&rx_cfg, &h->rx_channel),
                        TAG, "Failed to create RMT RX channel");

    h->rx_queue = xQueueCreate(1, sizeof(rmt_rx_done_event_data_t));
    if (h->rx_queue == NULL) {
        return ESP_ERR_NO_MEM;
    }

    rmt_rx_event_callbacks_t cbs = {
        .on_recv_done = lua_ir_rx_done_cb,
    };
    ESP_RETURN_ON_ERROR(rmt_rx_register_event_callbacks(h->rx_channel, &cbs, h->rx_queue),
                        TAG, "Failed to register RMT RX callback");

    h->rx_buffer = calloc(h->rx_max_symbols, sizeof(rmt_symbol_word_t));
    if (h->rx_buffer == NULL) {
        return ESP_ERR_NO_MEM;
    }

    ESP_RETURN_ON_ERROR(rmt_enable(h->rx_channel), TAG, "Failed to enable RX channel");
    h->rx_enabled = true;

    /*
     * Right after enabling the RX channel the GPIO transitions from a normal
     * input to RMT-controlled state, which can generate a single spurious
     * edge. RMT then treats the following 12 ms of silence as a "frame" and
     * delivers a bogus done event with num_symbols=1 and duration=0. Drain
     * any such pending event so the very first user-visible receive() call
     * does not return that artifact.
     */
    vTaskDelay(pdMS_TO_TICKS(20));
    (void)xQueueReset(h->rx_queue);
    return ESP_OK;
}

static void lua_module_ir_destroy_handle(lua_module_ir_handle_t *h)
{
    if (h == NULL) {
        return;
    }

    if (h->tx_enabled && h->tx_channel) {
        (void)rmt_disable(h->tx_channel);
        h->tx_enabled = false;
    }
    if (h->rx_enabled && h->rx_channel) {
        (void)rmt_disable(h->rx_channel);
        h->rx_enabled = false;
    }
    if (h->encoder) {
        (void)rmt_del_encoder(h->encoder);
        h->encoder = NULL;
    }
    if (h->tx_channel) {
        (void)rmt_del_channel(h->tx_channel);
        h->tx_channel = NULL;
    }
    if (h->rx_channel) {
        (void)rmt_del_channel(h->rx_channel);
        h->rx_channel = NULL;
    }
    if (h->rx_queue) {
        vQueueDelete(h->rx_queue);
        h->rx_queue = NULL;
    }
    if (h->rx_buffer) {
        free(h->rx_buffer);
        h->rx_buffer = NULL;
    }
    free(h);
}

static esp_err_t lua_module_ir_create_handle(const lua_ir_resolved_cfg_t *cfg,
                                             lua_module_ir_handle_t **out_handle)
{
    lua_module_ir_handle_t *h = calloc(1, sizeof(*h));
    if (h == NULL) {
        return ESP_ERR_NO_MEM;
    }

    h->tx_gpio_num       = cfg->tx_gpio_num;
    h->rx_gpio_num       = cfg->rx_gpio_num;
    h->ctrl_gpio_num     = cfg->ctrl_gpio_num;
    h->ctrl_active_level = cfg->ctrl_active_level;
    h->carrier_hz        = cfg->carrier_hz;
    h->tx_resolution_hz  = cfg->tx_resolution_hz;
    h->rx_resolution_hz  = cfg->rx_resolution_hz;
    h->rx_max_symbols    = cfg->rx_max_symbols;

    *out_handle = h;
    ESP_LOGI(TAG, "IR device created: tx=%d rx=%d ctrl=%d carrier=%" PRIu32 "Hz",
             cfg->tx_gpio_num, cfg->rx_gpio_num, cfg->ctrl_gpio_num, cfg->carrier_hz);
    return ESP_OK;
}

/* -------------------------------------------------------------------------- */
/* NEC encoding                                                               */
/* -------------------------------------------------------------------------- */

static int lua_ir_build_nec_symbols(rmt_symbol_word_t *out, uint16_t address, uint16_t command)
{
    int n = 0;
    out[n++] = (rmt_symbol_word_t){
        .level0 = 1, .duration0 = LUA_IR_NEC_LEADING_HIGH_US,
        .level1 = 0, .duration1 = LUA_IR_NEC_LEADING_LOW_US,
    };
    uint32_t payload = ((uint32_t)address & 0xFFFF) | ((uint32_t)command << 16);
    for (int i = 0; i < 32; i++) {
        bool one = (payload >> i) & 0x1;
        out[n++] = (rmt_symbol_word_t){
            .level0 = 1, .duration0 = LUA_IR_NEC_BIT_HIGH_US,
            .level1 = 0, .duration1 = one ? LUA_IR_NEC_ONE_LOW_US : LUA_IR_NEC_ZERO_LOW_US,
        };
    }
    out[n++] = (rmt_symbol_word_t){
        .level0 = 1, .duration0 = LUA_IR_NEC_ENDING_HIGH_US,
        .level1 = 0, .duration1 = 0,
    };
    return n;
}

/* -------------------------------------------------------------------------- */
/* Board-defaults & Lua-options resolution                                    */
/* -------------------------------------------------------------------------- */

static esp_err_t lua_module_ir_load_board_defaults(const char *device_name,
                                                   lua_ir_resolved_cfg_t *out)
{
    void *raw = NULL;
    esp_err_t err = esp_board_manager_get_device_config(device_name, &raw);
    if (err != ESP_OK || raw == NULL) {
        return ESP_ERR_NOT_FOUND;
    }

    const lua_ir_board_cfg_t *board = (const lua_ir_board_cfg_t *)raw;

    if (board->tx_gpio_num >= 0) {
        out->tx_gpio_num = board->tx_gpio_num;
    }
    if (board->rx_gpio_num >= 0) {
        out->rx_gpio_num = board->rx_gpio_num;
    }
    if (board->ctrl_gpio_num >= 0) {
        out->ctrl_gpio_num = board->ctrl_gpio_num;
        out->ctrl_active_level = board->ctrl_active_level;
    }
    if (board->carrier_hz > 0) {
        out->carrier_hz = (uint32_t)board->carrier_hz;
    }

    return ESP_OK;
}

static void lua_ir_apply_lua_overrides(lua_State *L, int opts_idx,
                                       lua_ir_resolved_cfg_t *cfg)
{
    if (opts_idx == 0 || lua_type(L, opts_idx) != LUA_TTABLE) {
        return;
    }

#define APPLY_INT(field, key) do { \
    lua_getfield(L, opts_idx, key); \
    if (lua_isnumber(L, -1)) { (cfg)->field = (int)lua_tointeger(L, -1); } \
    lua_pop(L, 1); \
} while (0)

#define APPLY_U32(field, key) do { \
    lua_getfield(L, opts_idx, key); \
    if (lua_isnumber(L, -1)) { (cfg)->field = (uint32_t)lua_tointeger(L, -1); } \
    lua_pop(L, 1); \
} while (0)

    APPLY_INT(tx_gpio_num,       "tx_gpio");
    APPLY_INT(rx_gpio_num,       "rx_gpio");
    APPLY_INT(ctrl_gpio_num,     "ctrl_gpio");
    APPLY_INT(ctrl_active_level, "ctrl_active_level");
    APPLY_U32(carrier_hz,        "carrier_hz");
    APPLY_U32(tx_resolution_hz,  "tx_resolution_hz");
    APPLY_U32(rx_resolution_hz,  "rx_resolution_hz");
    APPLY_INT(rx_max_symbols,    "rx_max_symbols");

#undef APPLY_INT
#undef APPLY_U32
}

/* -------------------------------------------------------------------------- */
/* Lua method implementations                                                 */
/* -------------------------------------------------------------------------- */

static int lua_module_ir_close_impl(lua_State *L, lua_module_ir_ud_t *ud)
{
    (void)L;
    if (ud->handle) {
        lua_module_ir_destroy_handle(ud->handle);
        ud->handle = NULL;
    }
    ud->device_name[0] = '\0';
    return 0;
}

static int lua_module_ir_gc(lua_State *L)
{
    lua_module_ir_ud_t *ud =
        (lua_module_ir_ud_t *)luaL_testudata(L, 1, LUA_MODULE_IR_METATABLE);
    if (ud && ud->handle) {
        return lua_module_ir_close_impl(L, ud);
    }
    return 0;
}

static int lua_module_ir_close(lua_State *L)
{
    lua_module_ir_ud_t *ud =
        (lua_module_ir_ud_t *)luaL_checkudata(L, 1, LUA_MODULE_IR_METATABLE);
    if (ud->handle) {
        return lua_module_ir_close_impl(L, ud);
    }
    return 0;
}

static int lua_module_ir_name(lua_State *L)
{
    lua_module_ir_ud_t *ud = lua_module_ir_get_ud(L, 1);
    lua_pushstring(L, ud->device_name);
    return 1;
}

static int lua_module_ir_send_symbols_internal(lua_State *L,
                                               lua_module_ir_handle_t *h,
                                               const rmt_symbol_word_t *symbols,
                                               size_t symbol_count)
{
    esp_err_t err = lua_module_ir_ensure_tx(h);
    if (err != ESP_OK) {
        return luaL_error(L, "ir: TX init failed: %s", esp_err_to_name(err));
    }

    rmt_transmit_config_t tx_cfg = { .loop_count = 0 };
    err = rmt_transmit(h->tx_channel, h->encoder, symbols,
                       symbol_count * sizeof(rmt_symbol_word_t), &tx_cfg);
    if (err != ESP_OK) {
        return luaL_error(L, "ir: rmt_transmit failed: %s", esp_err_to_name(err));
    }
    err = rmt_tx_wait_all_done(h->tx_channel, 5000);
    if (err != ESP_OK) {
        return luaL_error(L, "ir: tx wait failed: %s", esp_err_to_name(err));
    }
    return 0;
}

static int lua_module_ir_send_nec(lua_State *L)
{
    lua_module_ir_ud_t *ud = lua_module_ir_get_ud(L, 1);
    uint16_t address = (uint16_t)luaL_checkinteger(L, 2);
    uint16_t command = (uint16_t)luaL_checkinteger(L, 3);

    if (ud->handle->tx_gpio_num < 0) {
        return luaL_error(L, "ir: TX GPIO not configured");
    }

    rmt_symbol_word_t symbols[34];
    int n = lua_ir_build_nec_symbols(symbols, address, command);
    (void)lua_module_ir_send_symbols_internal(L, ud->handle, symbols, (size_t)n);
    lua_pushboolean(L, 1);
    return 1;
}

static int lua_module_ir_send_raw(lua_State *L)
{
    lua_module_ir_ud_t *ud = lua_module_ir_get_ud(L, 1);
    luaL_checktype(L, 2, LUA_TTABLE);

    if (ud->handle->tx_gpio_num < 0) {
        return luaL_error(L, "ir: TX GPIO not configured");
    }

#if LUA_VERSION_NUM >= 502
    lua_Integer count = luaL_len(L, 2);
#else
    lua_Integer count = lua_objlen(L, 2);
#endif
    if (count <= 0) {
        return luaL_error(L, "ir.send_raw: empty symbol list");
    }

    rmt_symbol_word_t *symbols = calloc((size_t)count, sizeof(rmt_symbol_word_t));
    if (symbols == NULL) {
        return luaL_error(L, "ir.send_raw: out of memory");
    }

    for (lua_Integer i = 0; i < count; i++) {
        lua_rawgeti(L, 2, i + 1);
        if (!lua_istable(L, -1)) {
            free(symbols);
            return luaL_error(L, "ir.send_raw: symbol[%d] is not a table", (int)(i + 1));
        }
        lua_getfield(L, -1, "level0");
        symbols[i].level0 = (uint32_t)luaL_optinteger(L, -1, 1) & 0x1;
        lua_pop(L, 1);
        lua_getfield(L, -1, "duration0");
        symbols[i].duration0 = (uint32_t)luaL_optinteger(L, -1, 0) & 0x7FFF;
        lua_pop(L, 1);
        lua_getfield(L, -1, "level1");
        symbols[i].level1 = (uint32_t)luaL_optinteger(L, -1, 0) & 0x1;
        lua_pop(L, 1);
        lua_getfield(L, -1, "duration1");
        symbols[i].duration1 = (uint32_t)luaL_optinteger(L, -1, 0) & 0x7FFF;
        lua_pop(L, 1);
        lua_pop(L, 1);
    }

    int rc = lua_module_ir_send_symbols_internal(L, ud->handle, symbols, (size_t)count);
    free(symbols);
    if (rc != 0) {
        return rc;
    }
    lua_pushboolean(L, 1);
    return 1;
}

static int lua_module_ir_receive(lua_State *L)
{
    lua_module_ir_ud_t *ud = lua_module_ir_get_ud(L, 1);
    int timeout_ms = (int)luaL_optinteger(L, 2, 5000);

    if (ud->handle->rx_gpio_num < 0) {
        return luaL_error(L, "ir: RX GPIO not configured");
    }

    esp_err_t err = lua_module_ir_ensure_rx(ud->handle);
    if (err != ESP_OK) {
        return luaL_error(L, "ir: RX init failed: %s", esp_err_to_name(err));
    }

    rmt_receive_config_t rcv_cfg = {
        .signal_range_min_ns = LUA_MODULE_IR_RX_MIN_NS,
        .signal_range_max_ns = LUA_MODULE_IR_RX_MAX_NS,
    };

    /*
     * Loop until we either receive a frame that contains at least one symbol
     * with non-zero duration, or the user-supplied timeout elapses. This
     * filters out the empty/zero-duration "frames" the RMT driver sometimes
     * emits when the line is briefly disturbed (e.g. right after rmt_enable
     * or by short noise spikes shorter than signal_range_min_ns).
     */
    rmt_rx_done_event_data_t evt;
    TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
    bool got_real_frame = false;
    while (!got_real_frame) {
        err = rmt_receive(ud->handle->rx_channel, ud->handle->rx_buffer,
                          ud->handle->rx_max_symbols * sizeof(rmt_symbol_word_t), &rcv_cfg);
        if (err != ESP_OK) {
            return luaL_error(L, "ir: rmt_receive failed: %s", esp_err_to_name(err));
        }

        TickType_t now = xTaskGetTickCount();
        TickType_t wait = (deadline > now) ? (deadline - now) : 0;
        if (xQueueReceive(ud->handle->rx_queue, &evt, wait) != pdPASS) {
            lua_pushnil(L);
            lua_pushstring(L, "timeout");
            return 2;
        }

        if (evt.num_symbols == 0) {
            continue;
        }
        /*
         * Reject obvious noise bursts: any real IR remote frame lasts at
         * least ~10ms (NEC repeat alone is 13.5ms, regular NEC ~67ms). A
         * stray edge captured right after rmt_enable typically yields a
         * 1-2 symbol "frame" with sub-millisecond total duration.
         */
        uint32_t total_us = 0;
        for (size_t i = 0; i < evt.num_symbols; i++) {
            total_us += evt.received_symbols[i].duration0;
            total_us += evt.received_symbols[i].duration1;
        }
        if (total_us >= 3000) {
            got_real_frame = true;
        } else {
            ESP_LOGD(TAG, "ignored noise burst: %u symbols, %" PRIu32 "us total",
                     (unsigned)evt.num_symbols, total_us);
        }
    }

    lua_newtable(L);
    for (size_t i = 0; i < evt.num_symbols; i++) {
        const rmt_symbol_word_t *s = &evt.received_symbols[i];
        lua_newtable(L);
        lua_pushinteger(L, s->level0 ? 0 : 1);
        lua_setfield(L, -2, "level0");
        lua_pushinteger(L, s->duration0);
        lua_setfield(L, -2, "duration0");
        lua_pushinteger(L, s->level1 ? 0 : 1);
        lua_setfield(L, -2, "level1");
        lua_pushinteger(L, s->duration1);
        lua_setfield(L, -2, "duration1");
        lua_rawseti(L, -2, (lua_Integer)(i + 1));
    }
    return 1;
}

static int lua_module_ir_info(lua_State *L)
{
    lua_module_ir_ud_t *ud = lua_module_ir_get_ud(L, 1);
    lua_module_ir_handle_t *h = ud->handle;

    lua_newtable(L);
    lua_pushstring(L, ud->device_name);
    lua_setfield(L, -2, "name");
    lua_pushinteger(L, h->tx_gpio_num);
    lua_setfield(L, -2, "tx_gpio");
    lua_pushinteger(L, h->rx_gpio_num);
    lua_setfield(L, -2, "rx_gpio");
    lua_pushinteger(L, h->ctrl_gpio_num);
    lua_setfield(L, -2, "ctrl_gpio");
    lua_pushinteger(L, h->carrier_hz);
    lua_setfield(L, -2, "carrier_hz");
    lua_pushinteger(L, h->tx_resolution_hz);
    lua_setfield(L, -2, "tx_resolution_hz");
    lua_pushinteger(L, h->rx_resolution_hz);
    lua_setfield(L, -2, "rx_resolution_hz");
    return 1;
}

static int lua_module_ir_new(lua_State *L)
{
    const char *device_name = LUA_MODULE_IR_DEFAULT_NAME;
    int opts_idx = 0;

    if (lua_isstring(L, 1)) {
        device_name = lua_tostring(L, 1);
        if (lua_istable(L, 2)) {
            opts_idx = 2;
        }
    } else if (lua_istable(L, 1)) {
        opts_idx = 1;
        lua_getfield(L, 1, "device");
        if (lua_isstring(L, -1)) {
            device_name = lua_tostring(L, -1);
        }
        lua_pop(L, 1);
    }

    if (strlen(device_name) >= LUA_MODULE_IR_MAX_NAME_LEN) {
        return luaL_error(L, "ir device name too long");
    }

    lua_ir_resolved_cfg_t cfg = {
        .tx_gpio_num       = -1,
        .rx_gpio_num       = -1,
        .ctrl_gpio_num     = -1,
        .ctrl_active_level = 0,
        .carrier_hz        = CONFIG_LUA_MODULE_IR_DEFAULT_CARRIER_HZ,
        .tx_resolution_hz  = CONFIG_LUA_MODULE_IR_DEFAULT_TX_RESOLUTION_HZ,
        .rx_resolution_hz  = CONFIG_LUA_MODULE_IR_DEFAULT_RX_RESOLUTION_HZ,
        .rx_max_symbols    = CONFIG_LUA_MODULE_IR_RX_MAX_SYMBOLS,
    };

    (void)lua_module_ir_load_board_defaults(device_name, &cfg);
    lua_ir_apply_lua_overrides(L, opts_idx, &cfg);

    if (cfg.tx_gpio_num < 0 && cfg.rx_gpio_num < 0) {
        return luaL_error(L, "ir.new: missing tx_gpio/rx_gpio for device '%s'", device_name);
    }
    if (cfg.carrier_hz == 0) {
        return luaL_error(L, "ir.new: invalid carrier_hz");
    }
    if (cfg.rx_max_symbols <= 0) {
        cfg.rx_max_symbols = CONFIG_LUA_MODULE_IR_RX_MAX_SYMBOLS;
    }

    lua_module_ir_handle_t *handle = NULL;
    esp_err_t err = lua_module_ir_create_handle(&cfg, &handle);
    if (err != ESP_OK || handle == NULL) {
        return luaL_error(L, "ir.new failed: %s",
                          esp_err_to_name(err != ESP_OK ? err : ESP_FAIL));
    }

    lua_module_ir_ud_t *ud =
        (lua_module_ir_ud_t *)lua_newuserdata(L, sizeof(*ud));
    memset(ud, 0, sizeof(*ud));
    ud->handle = handle;
    snprintf(ud->device_name, sizeof(ud->device_name), "%s", device_name);

    luaL_getmetatable(L, LUA_MODULE_IR_METATABLE);
    lua_setmetatable(L, -2);
    return 1;
}

/* -------------------------------------------------------------------------- */
/* Module registration                                                        */
/* -------------------------------------------------------------------------- */

int luaopen_ir(lua_State *L)
{
    if (luaL_newmetatable(L, LUA_MODULE_IR_METATABLE)) {
        lua_pushcfunction(L, lua_module_ir_gc);
        lua_setfield(L, -2, "__gc");
        lua_pushvalue(L, -1);
        lua_setfield(L, -2, "__index");
        lua_pushcfunction(L, lua_module_ir_send_nec);
        lua_setfield(L, -2, "send_nec");
        lua_pushcfunction(L, lua_module_ir_send_raw);
        lua_setfield(L, -2, "send_raw");
        lua_pushcfunction(L, lua_module_ir_receive);
        lua_setfield(L, -2, "receive");
        lua_pushcfunction(L, lua_module_ir_info);
        lua_setfield(L, -2, "info");
        lua_pushcfunction(L, lua_module_ir_name);
        lua_setfield(L, -2, "name");
        lua_pushcfunction(L, lua_module_ir_close);
        lua_setfield(L, -2, "close");
    }
    lua_pop(L, 1);

    lua_newtable(L);
    lua_pushcfunction(L, lua_module_ir_new);
    lua_setfield(L, -2, "new");
    return 1;
}

esp_err_t lua_module_ir_register(void)
{
    return cap_lua_register_module("ir", luaopen_ir);
}
