#include <stdlib.h>
#include <unistd.h>
#define SDL_MAIN_HANDLED /*To fix SDL's "undefined reference to WinMain"       \
                            issue*/
#include "demos/lv_demos.h"
#include "examples/lv_examples.h"
#include "sdl/sdl.h"
#include <SDL2/SDL.h>
#include <lvgl.h>

#include <lua.h>

#include <lauxlib.h>
#include <lualib.h>

#include <luavgl.h>
#include "widgets/widgets.h"

typedef struct {
  lua_State *L;
  lv_obj_t *root;
} lua_context_t;

typedef struct {
  lv_obj_t *root;
  make_font_cb make_font;
  delete_font_cb delete_font;
} luavgl_args_t;

/**
 * Initialize the Hardware Abstraction Layer (HAL) for the LVGL graphics
 * library
 */
static void hal_init(void)
{
  /* Use the 'monitor' driver which creates window on PC's monitor to simulate a
   * display*/
  sdl_init();

  /*Create a display buffer*/
  static lv_disp_draw_buf_t disp_buf1;
  static lv_color_t buf1_1[SDL_HOR_RES * SDL_VER_RES];
  lv_disp_draw_buf_init(&disp_buf1, buf1_1, NULL, SDL_HOR_RES * SDL_VER_RES);

  /*Create a display*/
  static lv_disp_drv_t disp_drv;
  lv_disp_drv_init(&disp_drv); /*Basic initialization*/
  disp_drv.draw_buf = &disp_buf1;
  disp_drv.flush_cb = sdl_display_flush;
  disp_drv.hor_res = SDL_HOR_RES;
  disp_drv.ver_res = SDL_VER_RES;

  lv_disp_t *disp = lv_disp_drv_register(&disp_drv);

  lv_theme_t *th = lv_theme_default_init(
      disp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED),
      LV_THEME_DEFAULT_DARK, LV_FONT_DEFAULT);
  lv_disp_set_theme(disp, th);

  lv_group_t *g = lv_group_create();
  lv_group_set_default(g);

  /* Add the mouse as input device
   * Use the 'mouse' driver which reads the PC's mouse*/
  static lv_indev_drv_t indev_drv_1;
  lv_indev_drv_init(&indev_drv_1); /*Basic initialization*/
  indev_drv_1.type = LV_INDEV_TYPE_POINTER;

  /*This function will be called periodically (by the library) to get the mouse
   * position and state*/
  indev_drv_1.read_cb = sdl_mouse_read;
  lv_indev_t *mouse_indev = lv_indev_drv_register(&indev_drv_1);

  static lv_indev_drv_t indev_drv_2;
  lv_indev_drv_init(&indev_drv_2); /*Basic initialization*/
  indev_drv_2.type = LV_INDEV_TYPE_KEYPAD;
  indev_drv_2.read_cb = sdl_keyboard_read;
  lv_indev_t *kb_indev = lv_indev_drv_register(&indev_drv_2);
  lv_indev_set_group(kb_indev, g);

  static lv_indev_drv_t indev_drv_3;
  lv_indev_drv_init(&indev_drv_3); /*Basic initialization*/
  indev_drv_3.type = LV_INDEV_TYPE_ENCODER;
  indev_drv_3.read_cb = sdl_mousewheel_read;
  lv_indev_t *enc_indev = lv_indev_drv_register(&indev_drv_3);
  lv_indev_set_group(enc_indev, g);

  /*Set a cursor for the mouse*/
  LV_IMG_DECLARE(mouse_cursor_icon); /*Declare the image file.*/
  lv_obj_t *cursor_obj =
      lv_img_create(lv_scr_act()); /*Create an image object for the cursor */
  lv_img_set_src(cursor_obj, &mouse_cursor_icon); /*Set the image source*/
  lv_indev_set_cursor(mouse_indev,
                      cursor_obj); /*Connect the image  object to the driver*/
}

/*
** Prints an error message, adding the program name in front of it
** (if present)
*/
static void l_message(const char *pname, const char *msg)
{
  printf("%s: %s\n", pname ? pname : " ", msg);
}

/*
** Check whether 'status' is not OK and, if so, prints the error
** message on the top of the stack. It assumes that the error object
** is a string, as it was either generated by Lua or by 'msghandler'.
*/
static int report(lua_State *L, int status)
{
  if (status != LUA_OK) {
    const char *msg = lua_tostring(L, -1);
    l_message("luactx", msg);
    lua_pop(L, 1); /* remove message */
  }
  return status;
}

/*
** Message handler used to run all chunks
*/
static int msghandler(lua_State *L)
{
  const char *msg = lua_tostring(L, 1);
  if (msg == NULL) {                         /* is error object not a string? */
    if (luaL_callmeta(L, 1, "__tostring") && /* does it have a metamethod */
        lua_type(L, -1) == LUA_TSTRING)      /* that produces a string? */
      return 1;                              /* that is the message */
    else
      msg = lua_pushfstring(L, "(error object is a %s value)",
                            luaL_typename(L, 1));
  }

  /* append a standard traceback */
  luaL_traceback(L, L, msg, 1);

  msg = lua_tostring(L, -1);
  lua_pop(L, 1);

  lv_obj_t *root = NULL;
  luavgl_ctx_t *ctx = luavgl_context(L);
  root = ctx->root ? ctx->root : lv_scr_act();
  lv_obj_t *label = lv_label_create(root);
  lv_label_set_text(label, msg);
  lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP);
  lv_obj_set_style_text_font(label, LV_FONT_DEFAULT, 0);
  lv_obj_set_width(label, LV_PCT(80));
  lv_obj_center(label);

  printf("trace back: \n%s\n", msg);
  return 0; /* return no trace, since we already processed it. */
}

static int lua_panic(lua_State *L)
{
  printf("LUA panic:\n%s\n", lua_tostring(L, -1));
  return 0; /* return to Lua to abort */
}

/*
** protected main entry
*/
static int pmain(lua_State *L)
{
  int status;
  const char *script = lua_tostring(L, 1);

  luavgl_args_t *args = lua_touserdata(L, 2);
  if (args == NULL || args->root == NULL) {
    printf("Null root object.\n");
    return 0;
  }

  luavgl_set_root(L, args->root);
  luavgl_set_font_extension(L, args->make_font, args->delete_font);

  /**
   * Set global variable SCRIPT_PATH, to make image src path easier.
   */
  char *path = strdup(script);
  if (path == NULL) {
    printf("no memory.\n");
    return 0;
  }

  int i = strlen(path);
  for (; i; i--) {
    if (path[i] == '/') {
      path[i + 1] = '\0';
      break;
    }
  }

  printf("script path: %s\n", path);
  lua_pushstring(L, path);
  lua_setglobal(L, "SCRIPT_PATH");
  luaL_openlibs(L);

  lua_getglobal(L, "package");
  lua_getfield(L, -1, "path");

  const char *pkg_path = lua_tostring(L, -1);
  char *new_path = malloc(strlen(pkg_path) + strlen(script) + 2);
  strcpy(new_path, pkg_path);
  strcat(new_path, ";"), strcat(new_path, path), strcat(new_path, "?.lua");
  lua_pop(L, 1);
  lua_pushstring(L, new_path);
  lua_setfield(L, -2, "path");
  lua_pop(L, 1);
  free(path);
  free(new_path);

  lua_atpanic(L, &lua_panic);

  luaL_requiref(L, "lvgl", luaopen_lvgl, 1);
  lua_pop(L, 1);
  luavgl_widgets_init(L);

  lua_pushcfunction(L, msghandler); /* push message handler */
  int base = lua_gettop(L);
  status = luaL_loadfile(L, script);
  if (status != LUA_OK) {
    lua_pushfstring(L, "failed to load: %s\n", script);
    /* manually show the error to screen. */
    lua_insert(L, 1);
    msghandler(L);
    return 0;
  }

  status = lua_pcall(L, 0, 0, base);
  lua_remove(L, base); /* remove message handler from the stack */
  report(L, status);
  lua_pushboolean(L, 1); /* signal no errors */
  return 1;
}

lua_context_t *lua_load_script(const char *script, luavgl_args_t *args)
{
  int ret, status;
  /* create the thread to run script. */
  if (script == NULL) {
    printf("args error.\n");
    return NULL;
  }

  printf("run script: %s\n", script);
  lua_State *L = luaL_newstate(); /* create state */
  if (L == NULL) {
    printf("no mem for lua state.\n");
    return NULL;
  }

  lua_pushcfunction(L, &pmain); /* to call 'pmain' in protected mode */
  lua_pushstring(L, script);
  lua_pushlightuserdata(L, args);
  status = lua_pcall(L, 2, 1, 0); /* do the call */
  ret = lua_toboolean(L, -1);
  report(L, status);
  if (!ret || status != LUA_OK) {
    /* This should never happen */
    printf("pcall failed.\n");
    lua_close(L);
    return NULL;
  }

  /* script may fail, but we continue until page destoried. */
  lua_context_t *luactx = calloc(sizeof(*luactx), 1);
  if (luactx == NULL) {
    printf("no memory.\n");
    goto lua_exit;
  }

  luactx->L = L;
  luactx->root = args->root;
  return luactx;

lua_exit:
  lua_close(L);

  return NULL;
}

int lua_terminate(lua_context_t *luactx)
{
  lua_State *L = luactx->L;

  lua_close(L);
  free(luactx);
  return 0;
}

static lua_context_t *lua_ctx;
static luavgl_args_t args;

static void reload_cb(lv_event_t *e)
{
  (void)e;
  if (lua_ctx != NULL) {
    lua_terminate(lua_ctx);
  }

  lua_ctx = lua_load_script(LUAVGL_EXAMPLE_DIR "/examples.lua", &args);
}

int main(int argc, char **argv)
{
  (void)argc; /*Unused*/
  (void)argv; /*Unused*/

  /*Initialize LVGL*/
  lv_init();

  /*Initialize the HAL (display, input devices, tick) for LVGL*/
  hal_init();

  args.root = lv_scr_act();

  lua_ctx = lua_load_script(LUAVGL_EXAMPLE_DIR "/examples.lua", &args);

  lv_obj_t *btn = lv_btn_create(lv_layer_sys());
  lv_obj_align(btn, LV_ALIGN_BOTTOM_RIGHT, 0, -50);
  lv_obj_set_size(btn, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
  lv_obj_set_style_pad_all(btn, 5, 0);
  lv_obj_add_event_cb(btn, reload_cb, LV_EVENT_CLICKED, NULL);
  lv_obj_t* label = lv_label_create(btn);
  lv_label_set_text(label, "RELOAD");
  lv_obj_center(label);

  while (1) {
    /* Periodically call the lv_task handler.
     * It could be done in a timer interrupt or an OS task too.*/
    lv_timer_handler();
    usleep(5 * 1000);
  }

  return 0;
}