/**
 * @file    hello_ic.c
 * @brief   hello_ic 教學步驟 4：在步驟 3 之上加一個 Widget 面板（一個數值＋一顆按鈕）。
 *
 * 新概念只有一個：**Widget 模式的面板**。Plugin 只「宣告」要哪些控件
 * （這裡是一個顯示最後讀值的 VALUE 與一顆 Read 按鈕），不管座標、不管畫法，
 * TUI 與 GUI 兩個主程式各自負責把它畫出來。控件宣告寫在 `hello_ic_panel.nxlay`，
 * `init()` 用 `LoadLayout()` 把它讀進來；`hello_ic_panel.h` 由 `nxlayout` 工具從它產生
 * （只有控件編號），**不要手改**。
 *
 * @see   docs/NEXUS_TUI_PLUGIN_GUIDE.md 第 6 章（Widget 模式）
 */

#include <stdio.h>
#include <string.h>

#include "nexus_plugin_api.h"
#include "plugin_version.h"

/**
 * `hello_ic_panel.h` 是 `nxlayout --emit-header` 從 `hello_ic_panel.nxlay`
 * 產生的檔案：裡面有控件個數 `HELLO_IC_PANEL_WIDGET_COUNT`，以及每個控件的編號
 * `WIDGET_ID_LASTREAD`、`WIDGET_ID_READ`。
 * 把 `hello_ic_panel.h` 與 `hello_ic_panel.nxlay` 跟 `hello_ic.c` 放在
 * 同一個 `src/` 目錄，建置的 include 路徑就找得到 `.h`；`.nxlay` 本身則放在
 * 編好的 DLL 旁邊（見 `HELLO_IC_LAYOUT`）。
 */
#include "hello_ic_panel.h"

/**
 * @brief 本 Plugin 對外的名稱。
 * @note  使用者在指令列打 `plugin list` 會看到它，打 `p hello_ic` 就掛載它。
 */
#define HELLO_IC_NAME   "hello_ic"

/**
 * @brief 面板版面檔的檔名：`LoadLayout()` 以 `NX_LAYOUT_F_FILE` 讀它，相對於 DLL 所在目錄。
 * @note  這是「檔案模式」，最容易上手：把 `hello_ic_panel.nxlay` 跟 `hello_ic.dll` 放在同一個目錄。
 *        要出貨成單一 DLL，可改成把版面當成 `RCDATA` 資源編進去，然後以資源名稱載入
 *        （`LoadLayout(ctx, 0, "HELLO_IC_PANEL", 0)`）。
 */
#define HELLO_IC_LAYOUT "hello_ic_panel.nxlay"

/**
 * @brief 你的 IC 的 7-bit I2C 位址。
 * @note  **請改成你自己的 IC 的位址。** 不知道位址時，在主程式指令列打 `scan`，
 *        它會列出匯流排上有回應的所有 7-bit 位址。本教學預設 0x58
 *        （模擬後端 mock 預設就有一顆 0x58）。
 */
#define HELLO_IC_ADDR   0x58

/**
 * @brief 本 Plugin 對外宣告的 slave 位址表（至少要有一列，否則 Core 拒載）。
 *
 * @note  欄位依序是：位址、位址模式（8-bit 暫存器位移）、bank 模式（無 bank）、
 *        bank 暫存器、bank 遮罩、旗標、保留、顯示名稱。
 * @warning 必須是靜態儲存期：Core 只讀不釋放，所以不能是函式內的區域變數。
 */
static const NxSlaveDecl s_slaves[] =
{
    { HELLO_IC_ADDR, NX_ADDR_MODE_8BIT, NX_BANK_MODE_NONE, 0, 0, 0, { 0, 0 }, "hello_ic" }
};

/**
 * @brief Core 提供的服務函式表，`init()` 時存下來，之後的 callback 才用得到。
 * @note  生命期涵蓋整個行程；不得修改其內容、不得釋放。
 */
static const NxHostApi *s_host = (const NxHostApi *)0;

/**
 * @brief 更新「最後讀值」那個 VALUE 控件的文字。
 *
 * @param[in] ctx  本次掛載的上下文
 * @param[in] text 要顯示的字串；不得為 `NULL`
 * @return 無
 *
 * @note  控件的狀態只存在 Core，Plugin 只送出新值、自己不留副本。
 * @note  `SetWidgetProp()` 第二個參數是面板編號；本 Plugin 只有一個面板，傳 0。
 *        `VALUE` 控件的值是字串（`NX_WKIND_STR`）；Plugin 自己設值不會產生事件。
 */
static void hello_ic_show(NxPluginCtx ctx, const char *text)
{
    NxPropVal v;

    memset(&v, 0, sizeof(v));
    v.SizeOfStruct = (NxDword)sizeof(v);
    v.kind         = (NxWord)NX_WKIND_STR;
    v.u.str.ptr    = text;
    v.u.str.len    = (NxDword)strlen(text);

    (void)s_host->SetWidgetProp(ctx, 0, WIDGET_ID_LASTREAD, NX_WP_VALUE, &v);
}

/**
 * @brief 讀 IC 的暫存器 0x00，把結果寫進 Log 並更新面板。`read` 指令與 Read 按鈕共用。
 *
 * @param[in] ctx 本次掛載的上下文
 * @return `ERR_OK`，或 `ExecSync()` 失敗時的錯誤碼（同時已寫進 Log 並顯示在面板上）。
 *
 * @warning 只能在 **I2C Worker Thread** 上呼叫：`ExecSync()` 在 UI Thread 上
 *          一律回 `ERR_WRONG_THREAD`（-85）。本檔的兩個呼叫端
 *          （自訂指令、`on_widget_event`）剛好都在 Worker Thread。
 * @note    `memset` 清成全 0 就是全部的設定：位址 0 代表「用這次掛載當下的位址」，
 *          bank 與後端也沿用掛載的設定。
 */
static NxLong hello_ic_do_read(NxPluginCtx ctx)
{
    NxJobRequest req;
    NxByte       value;
    NxLong       rc;
    char         line[64];

    memset(&req, 0, sizeof(req));
    req.size_of_struct = (NxDword)sizeof(req);
    req.op     = NX_JOB_READ;
    req.offset = 0x00;
    req.length = 1;
    rc = s_host->ExecSync(ctx, &req, &value, (NxWord)sizeof(value));
    if (rc != ERR_OK)
    {
        sprintf(line, "hello_ic: read 0x00 failed, rc=%ld", (long)rc);
        s_host->LogWrite(ctx, NX_LOG_WARN, line);
        hello_ic_show(ctx, "error");
        return rc;
    }

    sprintf(line, "0x%02X", (unsigned)value);
    hello_ic_show(ctx, line);
    sprintf(line, "hello_ic: reg 0x00 = 0x%02X", (unsigned)value);
    s_host->LogWrite(ctx, NX_LOG_INFO, line);
    return ERR_OK;
}

/**
 * @brief Plugin 掛載時的初始化，Core 在 **UI Thread** 上呼叫。
 *
 * @param[in] ctx  本次掛載的上下文
 * @param[in] host HostAPI 函式表
 * @return `ERR_OK` 表示成功；載入版面失敗時回傳其錯誤碼，這次掛載中止。
 *
 * @warning 在 UI Thread 上執行，不得做 I2C 存取或任何會阻塞的事。
 * @note    版面只在這裡載入**一次**：內容真的不同時重新載入會丟掉各控件的值，
 *          所以不要每幀重載。第二個參數 0 是面板編號。
 */
static NxLong NX_CALL hello_ic_init(NxPluginCtx ctx, const NxHostApi *host)
{
    NxLong rc;

    s_host = host;

    rc = host->LoadLayout(ctx, 0, HELLO_IC_LAYOUT, NX_LAYOUT_F_FILE);
    if (rc != ERR_OK)
    {
        return rc;
    }

    hello_ic_show(ctx, "--");
    host->LogWrite(ctx, NX_LOG_INFO, "hello_ic: loaded");
    return ERR_OK;
}

/**
 * @brief Plugin 卸載時的清理。**強制實作，不得為 `NULL`。**
 *
 * @param[in] ctx 本次掛載的上下文
 * @return 無
 *
 * @warning 在 **I2C Worker Thread** 上呼叫，返回後 `ctx` 立即失效。
 * @note    把 `s_host` 清掉：卸載之後任何遲到的呼叫都不該再碰 HostAPI。
 */
static void NX_CALL hello_ic_cleanup(NxPluginCtx ctx)
{
    NX_UNUSED_ARG(ctx);
    s_host = (const NxHostApi *)0;
}

/**
 * @brief Widget 互動事件：使用者按下 Read 按鈕時，Core 會呼叫這裡。
 *
 * @param[in] ctx       本次掛載的上下文
 * @param[in] panel_id  發生事件的面板編號
 * @param[in] widget_id 發生事件的控件編號（`hello_ic_panel.h` 裡的 `WIDGET_ID_*`）
 * @param[in] evt       事件，只在本次呼叫期間有效；按鈕被按下是 `evt->kind == NX_WEVT_ACTIVATED`
 *                      （`evt->val` 是點擊位置，這裡用不到）
 * @return 無
 *
 * @warning 在 **I2C Worker Thread** 上呼叫——這正是可以在這裡直接讀 I2C 的原因，
 *          畫面不會被卡住。
 */
static void NX_CALL hello_ic_on_widget_event(NxPluginCtx ctx, NxByte panel_id,
                                             NxWord widget_id, const NxWidgetEvent *evt)
{
    NxLong rc;

    NX_UNUSED_ARG(panel_id);

    if (widget_id == WIDGET_ID_READ && evt->kind == NX_WEVT_ACTIVATED)
    {
        /* 這個 callback 沒有回傳值可以交給主程式，失敗只能自己記 Log。 */
        rc = hello_ic_do_read(ctx);
        if (rc != ERR_OK)
        {
            s_host->LogWrite(ctx, NX_LOG_WARN, "hello_ic: Read button failed");
        }
    }
}

/**
 * @brief 自訂指令 `hello` 的處理函式：寫一行 Log，回 `ERR_OK`。
 *
 * @param[in] ctx  本次掛載的上下文
 * @param[in] argc 參數個數（含指令本身）
 * @param[in] argv 參數陣列；只在本次呼叫期間有效
 * @return `ERR_OK`。指令處理函式自己的回傳值就是這次指令的結果：
 *         回 `ERR_OK` 或錯誤碼；非 `ERR_OK` 時主程式會顯示錯誤。
 *         （batch 模式下，非 ERR_OK 的回傳值會成為 Exit Code 的絕對值。）
 *
 * @warning 在 **I2C Worker Thread** 上呼叫。
 */
static NxLong NX_CALL hello_ic_cmd_hello(NxPluginCtx ctx, int argc, const char *const *argv)
{
    NX_UNUSED_ARG(argc);
    NX_UNUSED_ARG(argv);
    s_host->LogWrite(ctx, NX_LOG_INFO, "hello_ic: hello!");
    return ERR_OK;
}

/**
 * @brief 自訂指令 `read` 的處理函式：等同按下面板上的 Read 按鈕。
 *
 * @param[in] ctx  本次掛載的上下文
 * @param[in] argc 參數個數（含指令本身）
 * @param[in] argv 參數陣列；只在本次呼叫期間有效
 * @return `ERR_OK`，或 `ExecSync()` 失敗時的錯誤碼；非 `ERR_OK` 時主程式會顯示錯誤。
 *         （batch 模式下，非 ERR_OK 的回傳值會成為 Exit Code 的絕對值。）
 *
 * @warning 在 **I2C Worker Thread** 上呼叫，所以可以直接讀 I2C。
 */
static NxLong NX_CALL hello_ic_cmd_read(NxPluginCtx ctx, int argc, const char *const *argv)
{
    NX_UNUSED_ARG(argc);
    NX_UNUSED_ARG(argv);
    return hello_ic_do_read(ctx);
}

/**
 * @brief 本 Plugin 的自訂指令表：名稱、`help` 說明、用法、處理函式。
 *
 * @note  靜態生命期，Core 只讀不釋放。
 * @warning 說明字串請用純 ASCII 收尾（VC6 以 CP950 掃描原始碼，中文結尾的
 *          字串會讓引號無法關閉）；這裡整段用英文最保險。
 */
static const NxCommandDef s_commands[] =
{
    { "hello", "say hello in the log", "hello", hello_ic_cmd_hello },
    { "read",  "read register 0x00 and log it", "read", hello_ic_cmd_read }
};

/**
 * @brief 本 Plugin 的能力宣告，Core 只讀不釋放。
 *
 * @note  必須是靜態儲存期：靜態變數一開始全是 0，而 0 對每一個選用 callback
 *        而言正是「我不提供」。
 */
static IcPlugin s_plugin;

/**
 * @brief Plugin DLL 的進入點：Core 載入 DLL 之後第一個呼叫的函式。
 *
 * @param[in] host_abi_major Host 端的 `NX_PLUGIN_ABI_MAJOR`
 * @param[in] host_abi_minor Host 端的 `NX_PLUGIN_ABI_MINOR`（本函式刻意不比較：minor 不同不是拒載的理由）
 * @return 指向靜態生命期 `IcPlugin` 的指標；ABI major 不相容時回傳 `NULL`。
 *
 * @warning 在 UI Thread 上呼叫，不得做 I2C 存取或阻塞操作。
 * @note    欄位一律用**具名賦值**填寫，沒填的欄位維持 0（＝不提供該 callback）。
 *          這一步把 `panel_mode` 改成 `NX_PANEL_MODE_WIDGET` 並掛上 `on_widget_event`。
 */
NX_PLUGIN_EXPORT struct IcPlugin * NX_CALL NexusPluginGetInterface(NxDword host_abi_major,
                                                                   NxDword host_abi_minor)
{
    NX_UNUSED_ARG(host_abi_minor);

    if (host_abi_major != NX_PLUGIN_ABI_MAJOR)
    {
        return (struct IcPlugin *)0;
    }

    s_plugin.abi_major       = NX_PLUGIN_ABI_MAJOR;
    s_plugin.abi_minor       = NX_PLUGIN_ABI_MINOR;
    s_plugin.size_of_struct  = sizeof(IcPlugin);

    s_plugin.name            = HELLO_IC_NAME;
    s_plugin.description     = "hello_ic tutorial plugin";

    s_plugin.slaves          = s_slaves;
    s_plugin.slave_count     = (NxByte)(sizeof(s_slaves) / sizeof(s_slaves[0]));
    s_plugin.slave_decl_size = (NxDword)sizeof(NxSlaveDecl);
    s_plugin.panel_mode      = NX_PANEL_MODE_WIDGET;

    s_plugin.init            = hello_ic_init;
    s_plugin.cleanup         = hello_ic_cleanup;
    s_plugin.on_widget_event = hello_ic_on_widget_event;

    s_plugin.commands        = s_commands;
    s_plugin.command_count   = (NxWord)(sizeof(s_commands) / sizeof(s_commands[0]));

    s_plugin.version_major   = (NxWord)PLUGIN_VER_MAJOR;
    s_plugin.version_minor   = (NxWord)PLUGIN_VER_MINOR;
    s_plugin.version_patch   = (NxWord)PLUGIN_VER_PATCH;

    return &s_plugin;
}
