/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2019 William Wilgus
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

/* lua events from rockbox *****************************************************
 * This library allows events to be subscribed / recieved within a lua script
 * most events in rb are synchronous so flags are set and later checked by a
 * secondary thread to make them (semi?) asynchronous.
 *
 * There are a few caveats to be aware of:
 * FIRST, The main lua state is halted till the lua callback(s) are finished
 *     Yielding will not return control to your script from within a callback
 *     Also, subsequent callbacks may be delayed by the code in your lua callback
 * SECOND, You must store the value returned from the event_register function
 *         you might get away with it for a bit but gc will destroy your callback
 *         eventually if you do not store the event
 * THIRD, You only get one cb per event type
 *     ["action", "button", "custom", "playback", "timer"]
 * (Re-registration of an event overwrites the previous one)
 *
 * Usage:
 * possible events =["action", "button", "custom", "playback", "timer"]
 *
 * local ev = rockev.register("event", cb_function, [timeout / flags])
 *    cb_function([id] [, data]) ... end
 *
 *
 * rockev.trigger("event", [true/false], [id])
 * sets an event to triggered,
 * NOTE!, CUSTOM_EVENT must be unset manually
 * id is only passed to callback by custom and playback events
 *
 * rockev.suspend(["event"/nil][true/false]) passing nil suspends all events.
 * stops event from executing, any event before re-enabling will be lost.
 * Passing false, unregistering or re-registering an event will clear the suspend
 *
 * rockev.unregister(evX)
 * Use unregister(evX) to remove an event
 * Unregistering is not necessary before script end, it will be
 * cleaned up on script exit
 *
 *******************************************************************************
 * *
 */

#define LUA_LIB

#define _ROCKCONF_H_ /* We don't need strcmp() etc. wrappers */
#include "lua.h"
#include "lauxlib.h"
#include "plugin.h"
#include "rocklib_events.h"

#define EVENT_METATABLE "event metatable"

#define EVENT_THREAD LUA_ROCKEVENTSNAME ".thread"
#define EV_STACKSZ (DEFAULT_STACK_SIZE * 2)

#define LUA_SUCCESS 0

#define EV_TIMER_TICKS 5 /* timer resolution */
#define EV_TIMER_FREQ ((TIMER_FREQ / HZ) * EV_TIMER_TICKS)
#define EV_TICKS (HZ / 5)

#define ENABLE_LUA_HOOK  (LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT)
#define DISABLE_LUA_HOOK (0)

enum {
    THREAD_ERROR       = 0x0,
/* event & suspend states */
    THREAD_ACTEVENT    = 0x1,
    THREAD_BTNEVENT    = 0x2,
    THREAD_CUSTOMEVENT = 0x4,
    THREAD_PLAYBKEVENT = 0x8,
    THREAD_TIMEREVENT  = 0x10,
  //THREAD_AVAILEVENT  = 0x20,
  //THREAD_AVAILEVENT  = 0x40,
  //THREAD_AVAILEVENT  = 0x80,
    THREAD_EVENT_ALL   = 0xFF,
/* thread states */
  //THREAD_AVAILSTATE  = 0x1,
  //THREAD_AVAILSTATE  = 0x2,
  //THREAD_AVAILSTATE  = 0x4,
  //THREAD_AVAILSTATE  = 0x8,
  //THREAD_AVAILSTATE  = 0x10,
  //THREAD_AVAILSTATE  = 0x20,
    THREAD_QUIT        = 0x40,
  //THREAD_AVAILSTATE  = 0x80,
};

enum {
    ACTEVENT = 0,
    BTNEVENT,
    CUSTOMEVENT,
    PLAYBKEVENT,
    TIMEREVENT,
    EVENT_CT
};

struct cb_data {
    int            cb_ref;
    unsigned long  id;
    void           *data;
    long           next_tick;
    long           ticks;
};

struct thread_status {
    uint8_t event;
    uint8_t suspended;
    uint8_t thread;
    uint8_t unused;
};

struct event_data {
    /* lua */
    lua_State           *L;
    lua_State           *NEWL;
    /* rockbox */
    unsigned int         thread_id;
    long                *event_stack;
    volatile long       *get_tick;
    struct thread_status status;
    /* callbacks */
    struct cb_data      *cb[EVENT_CT];
};

static void set_event_meta(lua_State *L);
static struct event_data ev_data;
static struct mutex rev_mtx SHAREDBSS_ATTR;

static inline uint8_t event_flag(unsigned int event)
{
    return (1UL << event) & 0xFF;
}

static inline bool has_thread_status(uint8_t ev_flag)
{
    return (ev_data.status.thread & ev_flag) == ev_flag;
}

static inline void set_evt(uint8_t ev_flag)
{
    ev_data.status.event |= ev_flag;
}

static inline bool remove_evt(uint8_t ev_flag)
{
    /* returns previous flag status and clears it */
    bool has_flag = (ev_data.status.event & ev_flag) == ev_flag;
    ev_data.status.event &= ~(ev_flag);
    return has_flag;
}

static inline bool is_suspend(uint8_t ev_flag)
{
    return (ev_data.status.suspended & ev_flag) == ev_flag;
}

static void suspend_evt(uint8_t ev_flag, bool suspend)
{
    if(!suspend && !has_thread_status(THREAD_QUIT))
    {
        ev_data.status.suspended &= ~(ev_flag);
        ev_flag = 0;
    }
    remove_evt(ev_flag);
    ev_data.status.suspended |= ev_flag;
}

/* mutex lock and unlock routines allow us to execute the event thread without
 * trashing the lua state on error, yield, or sleep in the callback functions */

static inline void rev_lock_mtx(void)
{
    rb->mutex_lock(&rev_mtx);
}

static inline void rev_unlock_mtx(void)
{
    rb->mutex_unlock(&rev_mtx);
}

static void lua_interrupt_callback(lua_State *L, lua_Debug *ar)
{
    (void) ar;

    rb->yield();

    rev_lock_mtx();
    rev_unlock_mtx(); /* must wait till event thread is done to continue */

    /* if callback error, pass error to the main lua state */
    if (lua_status(ev_data.NEWL) != LUA_SUCCESS)
        luaL_error(L, lua_tostring(ev_data.NEWL, -1));
}

static void lua_interrupt_set(lua_State *L, int hookmask)
{
    lua_Hook hook;
    int count;
    static lua_Hook oldhook = NULL;
    static int oldmask      = 0;
    static int oldcount     = 0;

    if (hookmask == ENABLE_LUA_HOOK)
    {
        hook = lua_gethook(L);
        if (hook == lua_interrupt_callback)
            return; /* our hook is already active */
        /* preserve prior hook */
        oldhook = hook;
        oldmask = lua_gethookmask(L);
        oldcount = lua_gethookcount(L);
        hook = lua_interrupt_callback;
        count = 10;
    }
    else
    {
        hook = oldhook;
        hookmask = oldmask;
        count = oldcount;
    }

    lua_sethook(L, hook, hookmask, count);
}

static int lua_rev_callback(lua_State *L, struct cb_data *evt)
{
    int lua_status = LUA_ERRRUN;

    /* load cb function from lua registry */
    lua_rawgeti(L, LUA_REGISTRYINDEX, evt->cb_ref);

    lua_pushinteger(L, evt->id);
    lua_pushlightuserdata(L, evt->data);

    lua_status = lua_resume(L, 2); /* call the saved function */
    if (lua_status == LUA_SUCCESS)
            lua_settop(L, 0); /* eat any value(s) returned */
    else if (lua_status == LUA_YIELD) /* coroutine.yield() disallowed */
        luaL_where(L, 1); /* push error string on stack */

    return lua_status;
}

/* timer interrupt callback */
static void rev_timer_isr(void)
{
    uint8_t ev_flag = 0;
    long curr_tick = *(ev_data.get_tick);
    struct cb_data *evt;

    for (unsigned int i= 0; i < EVENT_CT; i++)
    {
        if (!is_suspend(event_flag(i)))
        {
            evt = ev_data.cb[i];

            if ((i == ACTEVENT || i == BTNEVENT) &&
                (rb->button_queue_count() || rb->button_status() || evt->id))
                    ev_flag |= event_flag(i); /* any buttons ready? */
            else if(evt->ticks > 0 && TIME_AFTER(curr_tick, evt->next_tick))
                ev_flag |= event_flag(i);

        }
    }
    set_evt(ev_flag);
    if (ev_data.status.event) /* any events ready? */
        lua_interrupt_set(ev_data.L, ENABLE_LUA_HOOK);
}

static void event_thread(void)
{
    unsigned long action;
    uint8_t ev_flag;
    unsigned int event;
    struct cb_data *evt;

    while(!has_thread_status(THREAD_QUIT) && lua_status(ev_data.L) == LUA_SUCCESS)
    {
        rev_lock_mtx();

        lua_interrupt_set(ev_data.L, ENABLE_LUA_HOOK);

        for (event = 0; event < EVENT_CT; event++)
        {
            ev_flag = event_flag(event);
            if (!remove_evt(ev_flag) || is_suspend(ev_flag))
                continue; /* check next event */

            evt = ev_data.cb[event];

            switch (event)
            {
                case ACTEVENT:
                    action = get_plugin_action(TIMEOUT_NOBLOCK, true);
                    if (action == ACTION_UNKNOWN)
                        continue; /* check next event */
                    else if (action == ACTION_NONE)
                    {
                        /* only send ACTION_NONE once */
                        if (evt->id == ACTION_NONE || rb->button_status() != 0)
                            continue; /* check next event */
                    }
                    evt->id = action;
                    break;
                case BTNEVENT:
                    evt->id = rb->button_get(false);
                    /* only send BUTTON_NONE once */
                    if (evt->id == BUTTON_NONE)
                        continue; /* check next event */
                    break;
                case CUSTOMEVENT:
                case PLAYBKEVENT:
                case TIMEREVENT:
                    break;

            }

            if (lua_rev_callback(ev_data.NEWL, evt) != LUA_SUCCESS)
            {
                rev_unlock_mtx();
                goto event_error;
            }
            evt->next_tick = *(ev_data.get_tick) + evt->ticks;
        }
        rev_unlock_mtx(); /* we are safe to release back to main lua state */

        do
        {
            lua_interrupt_set(ev_data.L, DISABLE_LUA_HOOK);
            rb->yield();
        } while (!has_thread_status(THREAD_QUIT) && (is_suspend(THREAD_EVENT_ALL)
                                                || !ev_data.status.event));

    }
    rb->yield();
    lua_interrupt_set(ev_data.L, DISABLE_LUA_HOOK);

event_error:

    /* thread is exiting -- clean up */
    rb->timer_unregister();
    rb->thread_exit();

    return;
}

static inline void create_event_thread_ref(struct event_data *ev_data)
{
    lua_State *L = ev_data->L;

    lua_createtable(L, 2, 0);

    ev_data->event_stack = (long *) lua_newuserdata(L, EV_STACKSZ);

    /* attach EVENT_METATABLE to ud so we get notified on garbage collection */
    set_event_meta(L);
    lua_rawseti(L, -2, 1);

    ev_data->NEWL = lua_newthread(L);
    lua_rawseti(L, -2, 2);

    lua_setfield(L, LUA_REGISTRYINDEX, EVENT_THREAD); /* store references */
}

static inline void destroy_event_thread_ref(struct event_data *ev_data)
{
    lua_State *L = ev_data->L;
    ev_data->event_stack = NULL;
    ev_data->NEWL = NULL;
    lua_pushnil(L);
    lua_setfield(L, LUA_REGISTRYINDEX, EVENT_THREAD); /* free references */
}

static inline void exit_event_thread(struct event_data *ev_data)
{
    suspend_evt(THREAD_EVENT_ALL, true);
    ev_data->status.thread = THREAD_QUIT;

    rb->thread_wait(ev_data->thread_id); /* wait for thread to exit */
    destroy_event_thread_ref(ev_data);

    ev_data->status.thread = 0;
    ev_data->thread_id = UINT_MAX;
}

static void init_event_thread(bool init, struct event_data *ev_data)
{
    if (ev_data->event_stack != NULL) /* make sure we don't double free */
    {
        if (!init && ev_data->thread_id != UINT_MAX)
            exit_event_thread(ev_data);

        return;
    }
    else if (!init)
        return;

    create_event_thread_ref(ev_data);

    ev_data->thread_id = rb->create_thread(&event_thread,
                                           ev_data->event_stack,
                                           EV_STACKSZ,
                                           0,
                                           EVENT_THREAD
                                           IF_PRIO(, PRIORITY_SYSTEM)
                                           IF_COP(, CPU));

    /* Timer is used to poll waiting events */
    if (!rb->timer_register(1, NULL, EV_TIMER_FREQ, rev_timer_isr IF_COP(, CPU)))
    {
        rb->splash(100, "No timer available!");
    }
}

static void playback_event_callback(unsigned short id, void *data)
{
    /* playback events are synchronous we need to return ASAP so set a flag */
    struct cb_data *evt = ev_data.cb[PLAYBKEVENT];
    evt->id = id;
    evt->data = data;
    if (!is_suspend(THREAD_PLAYBKEVENT))
    {
        set_evt(THREAD_PLAYBKEVENT);
        lua_interrupt_set(ev_data.L, ENABLE_LUA_HOOK);
    }
}

static void register_playbk_events(int flag_events)
{
    unsigned short i, pb_evt;
    static const unsigned short playback_events[7] =
    {                                         /*flags*/
        PLAYBACK_EVENT_START_PLAYBACK,        /* 0x1 */
        PLAYBACK_EVENT_TRACK_BUFFER,          /* 0x2 */
        PLAYBACK_EVENT_CUR_TRACK_READY,       /* 0x4 */
        PLAYBACK_EVENT_TRACK_FINISH,          /* 0x8 */
        PLAYBACK_EVENT_TRACK_CHANGE,          /* 0x10*/
        PLAYBACK_EVENT_TRACK_SKIP,            /* 0x20*/
        PLAYBACK_EVENT_NEXTTRACKID3_AVAILABLE /* 0x40*/
    };

    for(i = 0; i < ARRAYLEN(playback_events); i++)
    {
        pb_evt = playback_events[i];
        if (!(flag_events & (1 << i)))
            rb->remove_event(pb_evt, playback_event_callback);
        else
            rb->add_event(pb_evt, playback_event_callback);
    }
}

static void destroy_event_userdata(lua_State *L, unsigned int event)
{
    uint8_t ev_flag = event_flag(event);
    struct cb_data *evt = ev_data.cb[event];
    suspend_evt(ev_flag, true);
    if (evt != NULL)
        luaL_unref(L, LUA_REGISTRYINDEX, evt->cb_ref);

    ev_data.cb[event] = NULL;
}

static void create_event_userdata(lua_State *L, unsigned int event, int index)
{
    /* if function is already registered , unregister it */
    destroy_event_userdata(L, event);

    luaL_checktype(L, index, LUA_TFUNCTION);
    lua_pushvalue(L, index); /* copy passed lua function on top of stack */
    int ref_lua = luaL_ref(L, LUA_REGISTRYINDEX);

    ev_data.cb[event] = (struct cb_data *)lua_newuserdata(L, sizeof(struct cb_data));

    ev_data.cb[event]->cb_ref = ref_lua; /* store ref for later call/release */

    /* attach EVENT_METATABLE to ud so we get notified on garbage collection */
    set_event_meta(L);
    /* cb_data is on top of stack */
}

static int rockev_gc(lua_State *L) {
    bool has_events = false;
    void *d = (void *) lua_touserdata(L, 1);

    if (d != NULL)
    {
        for (unsigned int i= 0; i < EVENT_CT; i++)
        {
            if (d == ev_data.cb[i] || d == ev_data.event_stack)
            {
                if (i == PLAYBKEVENT)
                    register_playbk_events(0);
                destroy_event_userdata(L, i);
            }
            else if (ev_data.cb[i] != NULL)
                has_events = true;
        }

        if (!has_events) /* nothing to wait for kill thread */
            init_event_thread(false, &ev_data);
    }
  return 0;
}

static void set_event_meta(lua_State *L)
{
    if (luaL_newmetatable(L, EVENT_METATABLE))
    {
        /* set __gc field so we can clean-up our objects */
        lua_pushcfunction(L, rockev_gc);
        lua_setfield(L, -2, "__gc");
    }
    lua_setmetatable(L, -2);
}

static void init_event_data(lua_State *L, struct event_data *ev_data)
{
    /* lua */
    ev_data->L = L;
    /*ev_data->NEWL = NULL;*/
    /* rockbox */
    ev_data->thread_id = UINT_MAX;
    ev_data->get_tick = rb->current_tick;

    ev_data->status.event = 0;
    ev_data->status.suspended = THREAD_EVENT_ALL;
    ev_data->status.thread = 0;

    /*ev_data->event_stack = NULL;*/
    /* callbacks */
    for (unsigned int i= 0; i < EVENT_CT; i++)
        ev_data->cb[i] = NULL;
}

/******************************************************************************
 * LUA INTERFACE **************************************************************
*******************************************************************************
*/
static unsigned int get_event_by_name(lua_State *L)
{
    static const char *const ev_map[EVENT_CT] =
    {
        [ACTEVENT] = "action",
        [BTNEVENT] = "button",
        [CUSTOMEVENT] = "custom",
        [PLAYBKEVENT] = "playback",
        [TIMEREVENT] = "timer",
    };

    return luaL_checkoption(L, 1, NULL, ev_map);
}


static int rockev_register(lua_State *L)
{
    /* register (event, cb [, args] */
    unsigned int event = get_event_by_name(L);
    uint8_t ev_flag = event_flag(event);
    int playbk_events;

    lua_settop(L, 3); /* we need to lock our optional args before...*/
    create_event_userdata(L, event, 2);/* cb_data is on top of stack */
    init_event_thread(!has_thread_status(THREAD_QUIT), &ev_data);

    long event_ticks = 0;
    struct cb_data *evt;

    switch (event)
    {
        case ACTEVENT:
            /* fall through */
        case BTNEVENT:
            event_ticks = 0; /* button events not triggered by timeout */
            break;
        case CUSTOMEVENT:
            event_ticks = luaL_optinteger(L, 3, EV_TICKS);
            ev_flag = 0; /* don't remove suspend */
            break;
        case PLAYBKEVENT: /* see register_playbk_events() for flags */
            event_ticks = 0; /* playback events are not triggered by timeout */
            playbk_events = luaL_optinteger(L, 3, 0x3F);
            register_playbk_events(playbk_events);
            break;
        case TIMEREVENT:
            event_ticks = luaL_checkinteger(L, 3);
            break;
    }

    evt = ev_data.cb[event];
    evt->ticks = event_ticks;
    evt->next_tick = *(ev_data.get_tick) + event_ticks;
    suspend_evt(ev_flag, false);

    return 1; /* returns cb_data */
}

static int rockev_suspend(lua_State *L)
{
    unsigned int event; /*Arg 1 is event, pass nil to suspend all */
    bool suspend = luaL_optboolean(L, 2, true);
    uint8_t ev_flag = THREAD_EVENT_ALL;

    if (!lua_isnoneornil(L, 1))
    {
        event = get_event_by_name(L);
        ev_flag = event_flag(event);
    }

    suspend_evt(ev_flag, suspend);

    /* don't resume invalid events */
    for (unsigned int i = 0; i < EVENT_CT; i++)
    {
        if (ev_data.cb[i] == NULL)
            suspend_evt(event_flag(i), true);
    }

    return 0;
}

static int rockev_trigger(lua_State *L)
{
    unsigned int event = get_event_by_name(L);
    bool enable = luaL_optboolean(L, 2, true);

    uint8_t ev_flag = event_flag(event);
    struct cb_data *evt = ev_data.cb[event];

    /* don't trigger invalid events */
    if (evt != NULL)
    {
        /* allow user to pass an id to some of the callback functions */
        evt->id = luaL_optinteger(L, 3, evt->id);

        if (event == CUSTOMEVENT)
            suspend_evt(ev_flag, !enable);

        if (enable)
            set_evt(ev_flag);
        else
            remove_evt(ev_flag);
    }
    return 0;
}

static int rockev_unregister(lua_State *L)
{
    luaL_checkudata(L, 1, EVENT_METATABLE);
    rockev_gc(L);
    return 0;
}

static const struct luaL_reg evlib[] = {
    {"register",  rockev_register},
    {"suspend", rockev_suspend},
    {"trigger",    rockev_trigger},
    {"unregister", rockev_unregister},
    {NULL, NULL}
};

int luaopen_rockevents(lua_State *L) {
    rb->mutex_init(&rev_mtx);
    init_event_data(L, &ev_data);
    luaL_register(L, LUA_ROCKEVENTSNAME, evlib);
    return 1;
}
