#include "luavgl.h"
#include "private.h"

/* extended feature for object */
#include "event.c"
#include "style.c"
#include "widgets/widgets.c"

static int luavgl_anim_create(lua_State *L);
static int luavgl_obj_delete(lua_State *L);
static int luavgl_obj_clean(lua_State *L);

static void _lv_obj_set_align(void *obj, lua_State *L)
{
  if (lua_isinteger(L, -1)) {
    lv_obj_align(obj, lua_tointeger(L, -1), 0, 0);
    return;
  }

  if (!lua_istable(L, -1)) {
    luaL_argerror(L, -1, "should be table");
    LV_LOG_ERROR("para should be table");
    return;
  }

  lua_getfield(L, -1, "type");
  lv_align_t align = lua_tointeger(L, -1);
  lua_pop(L, 1);

  lua_getfield(L, -1, "x_ofs");
  lv_coord_t x_ofs = lua_tointeger(L, -1);
  lua_pop(L, 1);

  lua_getfield(L, -1, "y_ofs");
  lv_coord_t y_ofs = lua_tointeger(L, -1);
  lua_pop(L, 1);

  lv_obj_align(obj, align, x_ofs, y_ofs);
}

/**
 * @brief Set obj properties based on property table on stack top
 *
 * Internally used.
 */
static inline void luavgl_setup_obj(lua_State *L, lv_obj_t *obj)
{
  luavgl_iterate(L, -1, luavgl_obj_set_property_kv, obj);
}

static void obj_delete_cb(lv_event_t *e)
{
  lua_State *L = e->user_data;
  lua_pushlightuserdata(L, e->current_target);
  lua_rawget(L, LUA_REGISTRYINDEX);
  if (lua_isnoneornil(L, -1)) {
    goto pop_exit;
  }

  luavgl_obj_t *lobj = luavgl_to_lobj(L, -1);
  if (lobj->lua_created) {
    // The underlying object is now gone, so don't keep a reference to it.
    lobj->obj = NULL;
    // Ensure there's no dangling reference in the registry either.
    lua_pushlightuserdata(L, e->current_target);
    lua_pushnil(L);
    lua_rawset(L, LUA_REGISTRYINDEX);

    goto pop_exit;
  }

  /* Clean its children firstly */
  luavgl_obj_clean(L);

  /* Remove events added from lua, but keep them unremoved */
  int size = lv_array_size(&lobj->events);
  struct event_callback_s *event;
  struct event_callback_s **events = lv_array_front(&lobj->events);
  for (int i = 0; i < size; i++) {
    event = events[i];
    if (event == NULL || event->dsc == NULL) {
      continue;
    }

    luaL_unref(L, LUA_REGISTRYINDEX, event->ref);
    lv_free(event);
  }

  lv_array_deinit(&lobj->events);

  /* remove userdata from registry. */
  lua_checkstack(L, 2);
  lua_pushlightuserdata(L, lobj->obj);
  lua_pushnil(L);
  lua_rawset(L, LUA_REGISTRYINDEX);

  LV_LOG_INFO("delete obj: %p", lobj->obj);
  lobj->obj = NULL;
  return;

pop_exit:
  lua_pop(L, 1);
  return;
}

/**
 * get the obj userdata and uservalue, if uservalue is not a table, then add
 * one. result stack: table(from uservalue)
 * return uservalue type: LUA_TTABLE
 */
LUALIB_API int luavgl_obj_getuserdatauv(lua_State *L, int idx)
{
  int type = lua_getuservalue(L, idx);
  if (type == LUA_TTABLE)
    return type;

  lua_pop(L, 1);
  /* initial element: 1 */
  lua_createtable(L, 0, 1);

#if 0 /* reserved slot, not used for now */
  lua_pushinteger(L, 1);
  lua_pushnil(L);
  lua_rawset(L, -3);
#endif
  lua_pushvalue(L, -1); /* leave one on stack */
  lua_setuservalue(L, idx > 0 ? idx : idx - 3);
  return LUA_TTABLE;
}

static int luavgl_obj_create(lua_State *L)
{
  return luavgl_obj_create_helper(L, lv_obj_create);
}

static int luavgl_obj_delete(lua_State *L)
{
  luavgl_obj_t *lobj;

  /**
   * Some widget may create child obj that doesn't belong to lua. Ignore them
   * and report no error.
   */
  if (!(lobj = lua_touserdata(L, -1))) {
    return 0;
  }

  if (lobj->obj == NULL) {
    /* could be already deleted, but not gc'ed */
    return 0;
  }

  uint32_t cnt = lv_obj_get_child_cnt(lobj->obj);

  for (int i = cnt - 1; i >= 0; i--) {
    lv_obj_t *child = lv_obj_get_child(lobj->obj, i);
    lua_checkstack(L, 2);
    lua_pushlightuserdata(L, child);
    lua_rawget(L, LUA_REGISTRYINDEX);
    if (lua_isnoneornil(L, -1)) {
      continue;
    }

    luavgl_obj_delete(L);
  }

  luavgl_obj_remove_event_all(L, lobj);

  /* delete obj firstly, then cleanup memory */
  if (lobj->lua_created) {
    lv_obj_del(lobj->obj);
  }

  lua_checkstack(L, 2);
  /* remove userdata from registry. */
  lua_pushlightuserdata(L, lobj->obj);
  lua_pushnil(L);
  lua_rawset(L, LUA_REGISTRYINDEX);

  LV_LOG_INFO("delete obj: %p", lobj->obj);
  lobj->obj = NULL;

  lua_pop(L, 1); /* remove the userdata para */
  return 0;
}

static int luavgl_obj_clean(lua_State *L)
{
  luavgl_obj_t *lobj = luavgl_to_lobj(L, -1);
  if (lobj == NULL || lobj->obj == NULL)
    return 0;

  lv_obj_t *obj = lobj->obj;
  uint32_t cnt = lv_obj_get_child_cnt(obj);

  for (int i = cnt - 1; i >= 0; i--) {
    lv_obj_t *child = lv_obj_get_child(obj, i);

    lua_checkstack(L, 2);
    lua_pushlightuserdata(L, child);
    lua_rawget(L, LUA_REGISTRYINDEX);
    luavgl_obj_delete(L);
  }

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_set(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);

  if (!lua_istable(L, -1)) {
    luaL_error(L, "expect a table on 2nd para");
    lua_settop(L, 1);
    return 1;
  }

  luavgl_setup_obj(L, obj);

  lua_settop(L, 1);
  return 1;
}

/**
 * obj:align_to({base=base, type=type, x_ofs=0, y_ofs=0})
 */
static int luavgl_obj_align_to(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);

  if (!lua_istable(L, 2)) {
    LV_LOG_ERROR("para should be table");
    return luaL_argerror(L, 2, "should be table");
  }

  lua_getfield(L, 2, "type");
  lv_align_t align = lua_tointeger(L, -1);
  lua_pop(L, 1);

  lua_getfield(L, 2, "base");
  lv_obj_t *base = luavgl_to_lobj(L, -1)->obj;
  lua_pop(L, 1);
  if (base == NULL) {
    LV_LOG_ERROR("base is not lua obj");
    return luaL_argerror(L, -1, "base is not lua obj");
  }

  lua_getfield(L, 2, "x_ofs");
  lv_coord_t x_ofs = lua_tointeger(L, -1);
  lua_pop(L, 1);

  lua_getfield(L, 2, "y_ofs");
  lv_coord_t y_ofs = lua_tointeger(L, -1);
  lua_pop(L, 1);

  lv_obj_align_to(obj, base, align, x_ofs, y_ofs);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_set_parent(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_t *parent = luavgl_to_obj(L, 2);
  lv_obj_set_parent(obj, parent);
  lua_pop(L, 1);
  return 1;
}

static int luavgl_obj_get_screen(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_t *screen = lv_obj_get_screen(obj);

  /* check if userdata is added to this obj, so lua can access it. */
  lua_pushlightuserdata(L, screen);
  lua_rawget(L, LUA_REGISTRYINDEX);

  if (lua_isnil(L, -1)) {
    /* create luavgl object and attach screen on it. */
    luavgl_obj_t *lobj = luavgl_add_lobj(L, screen);
    /* mark it's non-lua created, thus cannot be deleted by lua */
    lobj->lua_created = false;
  }

  return 1;
}

static int luavgl_obj_get_parent(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_t *parent = lv_obj_get_parent(obj);

  /* check if userdata is added to this obj, so lua can access it. */
  lua_pushlightuserdata(L, parent);
  lua_rawget(L, LUA_REGISTRYINDEX);
  if (lua_isnil(L, -1)) {
    /* create luavgl object and attach screen on it. */
    luavgl_obj_t *lobj = luavgl_add_lobj(L, parent);
    /* mark it's non-lua created, thus cannot be deleted by lua */
    lobj->lua_created = false;
  }

  return 1;
}

static int luavgl_obj_set_get_parent(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  if (!lua_isnoneornil(L, 2)) {
    lv_obj_t *parent = luavgl_to_obj(L, 2);
    lv_obj_set_parent(obj, parent);
  }

  return luavgl_obj_get_parent(L);
}

static int luavgl_obj_get_child(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);

  int id = luavgl_tointeger(L, 2);
  lv_obj_t *child = lv_obj_get_child(obj, id);
  if (child == NULL) {
    lua_pushnil(L);
    return 1;
  }

  /* check if userdata is added to this obj, so lua can access it. */
  lua_pushlightuserdata(L, child);
  lua_rawget(L, LUA_REGISTRYINDEX);
  if (lua_isnil(L, -1)) {
    luavgl_add_lobj(L, child)->lua_created = false;
  }

  return 1;
}

static int luavgl_obj_get_child_cnt(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lua_pushinteger(L, lv_obj_get_child_cnt(obj));
  return 1;
}

static int luavgl_obj_get_state(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_state_t state = lv_obj_get_state(obj);
  lua_pushinteger(L, state);

  return 1;
}

/**
 * obj:scroll_to({x=10})
 * obj:scroll_to({x=10, anim=true})
 * obj:scroll_to({x=10, y=100, anim=false})
 */
static int luavgl_obj_scroll_to(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);

  if (!lua_istable(L, -1)) {
    luaL_argerror(L, -1, "should be table {x=0,y=0,anim=true}");
  }

  lua_getfield(L, -1, "anim");
  bool anim = luavgl_tointeger(L, -1);
  lua_pop(L, 1);

  lv_coord_t v;
  lua_getfield(L, -1, "x");
  if (!lua_isnil(L, -1)) {
    v = lua_tointeger(L, -1);
    lv_obj_scroll_to_x(obj, v, anim ? LV_ANIM_ON : LV_ANIM_OFF);
  }
  lua_pop(L, 1);

  lua_getfield(L, -1, "y");
  if (!lua_isnil(L, -1)) {
    v = lua_tointeger(L, -1);
    lv_obj_scroll_to_x(obj, v, anim ? LV_ANIM_ON : LV_ANIM_OFF);
  }
  lua_pop(L, 1);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_is_visible(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lua_pushboolean(L, lv_obj_is_visible(obj));

  return 1;
}

static int luavgl_obj_add_flag(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_flag_t flag = lua_tointeger(L, 2);
  lv_obj_add_flag(obj, flag);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_clear_flag(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_flag_t flag = lua_tointeger(L, 2);
  lv_obj_clear_flag(obj, flag);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_add_state(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_state_t state = lua_tointeger(L, 2);
  lv_obj_add_state(obj, state);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_clear_state(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_state_t state = lua_tointeger(L, 2);
  lv_obj_clear_state(obj, state);

  lua_settop(L, 1);
  return 1;
}

/**
 * obj:scroll_by(x, y, anim_en)
 */
static int luavgl_obj_scroll_by(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  int x = luavgl_tointeger(L, 2);
  int y = luavgl_tointeger(L, 3);
  int anim_en = luavgl_tointeger(L, 4);

  lv_obj_scroll_by(obj, x, y, anim_en);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_scroll_by_bounded(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  int dx = luavgl_tointeger(L, 2);
  int dy = luavgl_tointeger(L, 3);
  int anim_en = luavgl_tointeger(L, 4);

  lv_obj_scroll_by_bounded(obj, dx, dy, anim_en);
  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_scroll_to_view(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  int anim_en = luavgl_tointeger(L, 2);

  lv_obj_scroll_to_view(obj, anim_en);
  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_scroll_to_view_recursive(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  int anim_en = luavgl_tointeger(L, 2);

  lv_obj_scroll_to_view_recursive(obj, anim_en);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_scroll_by_raw(lua_State *L)
{
#if LV_VERSION_CHECK(8, 3, 0)
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  int x = luavgl_tointeger(L, 2);
  int y = luavgl_tointeger(L, 3);

  _lv_obj_scroll_by_raw(obj, x, y);
#endif

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_is_scrolling(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lua_pushboolean(L, lv_obj_is_scrolling(obj));
  return 1;
}

static int luavgl_obj_scrollbar_invalidate(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_scrollbar_invalidate(obj);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_readjust_scroll(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  int anim_en = luavgl_tointeger(L, 2);
  lv_obj_readjust_scroll(obj, anim_en);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_is_editable(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lua_pushboolean(L, lv_obj_is_editable(obj));
  return 1;
}

static int luavgl_obj_is_group_def(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lua_pushboolean(L, lv_obj_is_group_def(obj));
  return 1;
}

static int luavgl_obj_is_layout_positioned(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lua_pushboolean(L, lv_obj_is_layout_positioned(obj));
  return 1;
}

static int luavgl_obj_mark_layout_as_dirty(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_mark_layout_as_dirty(obj);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_center(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_center(obj);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_invalidate(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_invalidate(obj);

  return 1;
}

static int luavgl_obj_set_flex_flow(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_flex_flow_t flow = luavgl_tointeger(L, 2);

  lv_obj_set_flex_flow(obj, flow);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_set_flex_align(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_flex_align_t m = luavgl_tointeger(L, 2);
  lv_flex_align_t c = luavgl_tointeger(L, 3);
  lv_flex_align_t t = luavgl_tointeger(L, 4);

  lv_obj_set_flex_align(obj, m, c, t);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_set_flex_grow(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  uint8_t grow = luavgl_tointeger(L, 2);

  lv_obj_set_flex_grow(obj, grow);

  lua_settop(L, 1);
  return 1;
}

static int luavgl_obj_indev_search(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_point_t point;
  if (lua_istable(L, 2)) {
    lua_geti(L, 2, 1);
    point.x = lua_tointeger(L, -1);
    lua_pop(L, 1);

    lua_geti(L, 2, 2);
    point.y = lua_tointeger(L, -1);
    lua_pop(L, 1);
  } else {
    point.x = lua_tointeger(L, 2);
    point.y = lua_tointeger(L, 3);
  }

  obj = lv_indev_search_obj(obj, &point);
  if (obj == NULL) {
    lua_pushnil(L);
  } else {
    lua_pushlightuserdata(L, obj);
    lua_rawget(L, LUA_REGISTRYINDEX);
    if (lua_isnoneornil(L, -1)) {
      luavgl_add_lobj(L, obj)->lua_created = false;
    }
  }

  return 1;
}

static int luavgl_obj_get_coords(lua_State *L)
{
  lv_area_t area;
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_obj_get_coords(obj, &area);
  lua_newtable(L);

  lua_pushinteger(L, area.x1);
  lua_setfield(L, -2, "x1");

  lua_pushinteger(L, area.x2);
  lua_setfield(L, -2, "x2");

  lua_pushinteger(L, area.y1);
  lua_setfield(L, -2, "y1");

  lua_pushinteger(L, area.y2);
  lua_setfield(L, -2, "y2");

  return 1;
}

/**
 * get object real position using lv_obj_get_x/x2/y/y2
 */
static int luavgl_obj_get_pos(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lua_newtable(L);

  lua_pushinteger(L, lv_obj_get_x(obj));
  lua_setfield(L, -2, "x1");

  lua_pushinteger(L, lv_obj_get_x2(obj));
  lua_setfield(L, -2, "x2");

  lua_pushinteger(L, lv_obj_get_y(obj));
  lua_setfield(L, -2, "y1");

  lua_pushinteger(L, lv_obj_get_y2(obj));
  lua_setfield(L, -2, "y2");

  return 1;
}

/**
 * set this object as the current selection
 */
static int luavgl_obj_focus(lua_State *L) {
  lv_obj_t *obj = luavgl_to_obj(L, 1);

  lv_group_t *group = lv_obj_get_group(obj);
  if (group == NULL) {
    return 0;
  }
  lv_group_focus_obj(obj);

  return 0;
}

static int luavgl_obj_move_to_index(lua_State *L) {
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  int idx = luavgl_tointeger(L, 2);

  lv_obj_move_to_index(obj, idx);
  return 0;
}

/**
 * Remove all animations associates to this object
 */
static int luavgl_obj_remove_anim_all(lua_State *L)
{
  lv_obj_t *obj = luavgl_to_obj(L, 1);
  lv_anim_del(obj, NULL);
  return 1;
}

static int luavgl_obj_gc(lua_State *L)
{
  if (lua_type(L, 1) != LUA_TUSERDATA) {
    /* If t = setmetatable({}, obj_meta_table), this will happen when t is
     * gc;ed. Currently all metatables for classes based on obj, that has no own
     * __gc will goes here.
     */
    return 0;
  }

  LV_LOG_INFO("enter");

  luavgl_obj_t *lobj = lua_touserdata(L, 1);
  if (lobj == NULL || lobj->obj == NULL) {
    /* obj is already deleted. It's ok. */
    return 0;
  }

  LV_LOG_INFO("GC for obj: %p", lobj->obj);
  luavgl_obj_delete(L);

  return 0;
}

static const luaL_Reg luavgl_obj_methods[] = {
    {"set",                      luavgl_obj_set                     },
    {"set_style",                luavgl_obj_set_style               },
    {"align_to",                 luavgl_obj_align_to                },
    {"delete",                   luavgl_obj_delete                  },
    {"clean",                    luavgl_obj_clean                   },

 /* misc. functions */
    {"parent",                   luavgl_obj_set_get_parent          },
    {"set_parent",               luavgl_obj_set_parent              },
    {"get_parent",               luavgl_obj_get_parent              },
    {"get_child",                luavgl_obj_get_child               },
    {"get_child_cnt",            luavgl_obj_get_child_cnt           },
    {"get_screen",               luavgl_obj_get_screen              },
    {"get_state",                luavgl_obj_get_state               },
    {"scroll_to",                luavgl_obj_scroll_to               },
    {"is_scrolling",             luavgl_obj_is_scrolling            },
    {"is_visible",               luavgl_obj_is_visible              },
    {"add_flag",                 luavgl_obj_add_flag                },
    {"clear_flag",               luavgl_obj_clear_flag              },
    {"add_state",                luavgl_obj_add_state               },
    {"clear_state",              luavgl_obj_clear_state             },
    {"add_style",                luavgl_obj_add_style               },
    {"remove_style",             luavgl_obj_remove_style            },
    {"remove_style_all",         luavgl_obj_remove_style_all        },
    {"scroll_by",                luavgl_obj_scroll_by               },
    {"scroll_by_bounded",        luavgl_obj_scroll_by_bounded       },
    {"scroll_to_view",           luavgl_obj_scroll_to_view          },
    {"scroll_to_view_recursive", luavgl_obj_scroll_to_view_recursive},
    {"scroll_by_raw",            luavgl_obj_scroll_by_raw           },
    {"scrollbar_invalidate",     luavgl_obj_scrollbar_invalidate    },
    {"readjust_scroll",          luavgl_obj_readjust_scroll         },
    {"is_editable",              luavgl_obj_is_editable             },
    {"is_group_def",             luavgl_obj_is_group_def            },
    {"is_layout_positioned",     luavgl_obj_is_layout_positioned    },
    {"mark_layout_as_dirty",     luavgl_obj_mark_layout_as_dirty    },
    {"center",                   luavgl_obj_center                  },
    {"invalidate",               luavgl_obj_invalidate              },
    {"set_flex_flow",            luavgl_obj_set_flex_flow           },
    {"set_flex_align",           luavgl_obj_set_flex_align          },
    {"set_flex_grow",            luavgl_obj_set_flex_grow           },
    {"indev_search",             luavgl_obj_indev_search            },
    {"get_coords",               luavgl_obj_get_coords              },
    {"get_pos",                  luavgl_obj_get_pos                 },
    {"focus",                    luavgl_obj_focus                   },
    {"move_to_index",            luavgl_obj_move_to_index           },

    {"onevent",                  luavgl_obj_on_event                },
    {"onPressed",                luavgl_obj_on_pressed              },
    {"onClicked",                luavgl_obj_on_clicked              },
    {"onShortClicked",           luavgl_obj_on_short_clicked        },
    {"anim",                     luavgl_anim_create                 },
    {"Anim",                     luavgl_anim_create                 },
    {"remove_all_anim",          luavgl_obj_remove_anim_all         }, /* remove all */
    {NULL,                       NULL                               },
};

static void luavgl_obj_init(lua_State *L)
{
  /* base lv_obj */
  luavgl_obj_newmetatable(L, &lv_obj_class, "lv_obj", luavgl_obj_methods);
  lua_pushcfunction(L, luavgl_obj_gc);
  lua_setfield(L, -2, "__gc");

  /**
   * Widget creation functions is a metatable, so we can add extended widget to
   * it.
   *
   * widgets = {}
   * lib = {widget_create_methods}
   * widgets.__index = lib
   * obj.__metatable = widgets
   */
  lua_getfield(L, -1, "__index");
  luaL_newmetatable(L, "widgets");
  luaL_newlib(L, widget_create_methods);
  lua_setfield(L, -2, "__index");
  lua_setmetatable(L, -2);
  lua_pop(L, 1); /* remove obj.__index table */

  lua_pop(L, 1); /* remove obj metatable */
}

static const luavgl_value_setter_t obj_property_table[] = {
    {"x",              0,                 {.setter = (setter_int_t)lv_obj_set_x}             },
    {"y",              0,                 {.setter = (setter_int_t)lv_obj_set_y}             },
    {"w",              0,                 {.setter = (setter_int_t)lv_obj_set_width}         },
    {"h",              0,                 {.setter = (setter_int_t)lv_obj_set_height}        },
    {"align",          SETTER_TYPE_STACK, {.setter_stack = _lv_obj_set_align}                },

    {"scrollbar_mode", 0,                 {.setter = (setter_int_t)lv_obj_set_scrollbar_mode}},
    {"scroll_dir",     0,                 {.setter = (setter_int_t)lv_obj_set_scroll_dir}    },
    {"scroll_snap_x",  0,                 {.setter = (setter_int_t)lv_obj_set_scroll_snap_x} },
    {"scroll_snap_y",  0,                 {.setter = (setter_int_t)lv_obj_set_scroll_snap_y} },
};

/**
 * Set object property.
 * Differ from set object style, this one is usually used to set widget
 * property like lv_WIDGETNAME_set_src()
 *
 * Used internally.
 *
 * Expected stack:
 * stack[-2]: key(property name)
 * stack[-1]: value(could be any lua data)
 */
LUALIB_API int luavgl_obj_set_property_kv(lua_State *L, void *data)
{
  lv_obj_t *obj = data;

  /* Check for integer key with userdata as value */
  if (lua_type(L, -2) == LUA_TNUMBER && lua_type(L, -1) == LUA_TUSERDATA) {
    lv_obj_t *child = luavgl_to_obj(L, -1);
    lv_obj_set_parent(child, obj);
    return 0;
  }

  int ret = luavgl_set_property(L, obj, obj_property_table);

  if (ret == 0)
    return 0;

  /* fallback to set obj local style, with default state. */
  return luavgl_obj_set_style_kv(L, obj, 0);
}

LUALIB_API int luavgl_obj_create_helper(lua_State *L,
                                        lv_obj_t *(*create)(lv_obj_t *parent))
{
  luavgl_ctx_t *ctx = luavgl_context(L);
  lv_obj_t *parent;

  if (lua_isnoneornil(L, 1)) {
    parent = ctx->root;
  } else {
    parent = luavgl_to_obj(L, 1);
    /* remove parent, in order to keep clean stack to call obj.set */
    lua_remove(L, 1);
  }

  LV_LOG_INFO("create obj on: %p", parent);

  lv_obj_t *obj = create(parent);
  luavgl_add_lobj(L, obj)->lua_created = true;

  if (!lua_istable(L, -2)) {
    /* no need to call setup */
    return 1;
  }

  lua_insert(L, -2); /* obj, prop */

  /* now call obj.set to setup property */
  lua_getfield(L, -2, "set"); /* obj, prop, set */
  if (!luavgl_is_callable(L, -1)) {
    /* set method not found, call basic obj set method. */
    lua_pop(L, 2); /* remove prop table, and set method */
    luavgl_setup_obj(L, obj);
  } else {
    lua_insert(L, -3); /* set, obj, prop */
    lua_call(L, 2, 0); /* now stack is clean */
    lua_pushlightuserdata(L, obj);
    lua_rawget(L, LUA_REGISTRYINDEX);
  }

  LV_LOG_INFO("create obj: %p", obj);
  return 1;
}

/**
 * Add existing lvgl obj to lua, return lobj(luavgl obj).
 * If no metatable not found for this obj class, then lv_obj_class metatable is
 * used
 */
LUALIB_API luavgl_obj_t *luavgl_add_lobj(lua_State *L, lv_obj_t *obj)
{
  luavgl_obj_t *lobj;

  /* In rare case, obj may be deleted but not gc'ed in lua, and lvgl quickly
   * creates another obj but happen to malloced same address, thus using obj
   * light userdata as key may have some potential issues. */
  lua_pushlightuserdata(L, obj);
  lua_rawget(L, LUA_REGISTRYINDEX);
  if (!lua_isnoneornil(L, -1)) {
    /* already exists */
    return luavgl_to_lobj(L, -1);
  }
  lua_pop(L, 1); /* pop nil value */

  lobj = lua_newuserdata(L, sizeof(*lobj));

  if (luavgl_obj_getmetatable(L, obj->class_p) == LUA_TNIL) {
    lua_pop(L, 1);
    LV_LOG_ERROR("cannot find metatable for class: %p", obj->class_p);
    /* use base obj metatable instead */
    luavgl_obj_getmetatable(L, &lv_obj_class);
  }

  lua_setmetatable(L, -2);

  lv_memset(lobj, 0, sizeof(*lobj));
  lobj->obj = obj;

  /* Init event array to store events added from lua. */
  lv_array_init(&lobj->events, 0, sizeof(struct event_callback_s *));
  lv_obj_add_event_cb(obj, obj_delete_cb, LV_EVENT_DELETE, L);

  /* registry[obj] = lobj */
  lua_pushlightuserdata(L, obj);
  lua_pushvalue(L, -2);
  lua_rawset(L, LUA_REGISTRYINDEX);
  return lobj;
}
