#define LUA_LIB
#include "lualib.h"

#include "vendor/Soup/soup/Canvas.hpp"
#include "vendor/Soup/soup/MemoryRefReader.hpp"
#include "vendor/Soup/soup/QrCode.hpp"

static soup::Canvas* checkcanvas (lua_State *L, int i) {
  return (soup::Canvas*)luaL_checkudata(L, i, "pluto:canvas");
}

static void pushcanvas (lua_State *L, soup::Canvas&& canvas) {
  new (lua_newuserdata(L, sizeof(soup::Canvas))) soup::Canvas(std::move(canvas));
  if (luaL_newmetatable(L, "pluto:canvas")) {
    lua_pushliteral(L, "__index");
    luaL_loadbuffer(L, "return require\"pluto:canvas\"", 28, 0);
    lua_call(L, 0, 1);
    lua_settable(L, -3);
    lua_pushliteral(L, "__gc");
    lua_pushcfunction(L, [](lua_State *L) {
      std::destroy_at<>(checkcanvas(L, 1));
      return 0;
    });
    lua_settable(L, -3);
  }
  lua_setmetatable(L, -2);
}

static int canvas_new (lua_State* L) {
  const auto width = luaL_checkinteger(L, 1);
  const auto height = luaL_checkinteger(L, 2);
  pushcanvas(L, soup::Canvas(static_cast<unsigned int>(width), static_cast<unsigned int>(height)));
  return 1;
}

static int canvas_bmp (lua_State *L) {
  size_t size;
  const char *data = luaL_checklstring(L, 1, &size);
  soup::MemoryRefReader r(data, size);
  pushcanvas(L, soup::Canvas::fromBmp(r));
  return 1;
}

static int canvas_qrcode (lua_State *L) {
  size_t size;
  const char *data = luaL_checklstring(L, 1, &size);
  soup::QrCode::ecc ecl = soup::QrCode::ecc::LOW;
  unsigned int border = 0;
  soup::Rgb fg = soup::Rgb::BLACK;
  soup::Rgb bg = soup::Rgb::WHITE;
  if (lua_gettop(L) >= 2) {
    luaL_checktype(L, 2, LUA_TTABLE);

    lua_pushliteral(L, "ecl");
    if (lua_gettable(L, 2) > LUA_TNIL) {
      const char* const options[] = { "low", "medium", "quartile", "high", nullptr };
      ecl = static_cast<soup::QrCode::ecc>(luaL_checkoption(L, -1, "low", options));
    }

    lua_pushliteral(L, "border");
    if (lua_gettable(L, 2) > LUA_TNIL)
      border = static_cast<unsigned int>(luaL_checkinteger(L, -1));

    lua_pushliteral(L, "fg");
    if (lua_gettable(L, 2) > LUA_TNIL)
      fg = soup::Rgb(static_cast<uint32_t>(luaL_checkinteger(L, -1)));

    lua_pushliteral(L, "bg");
    if (lua_gettable(L, 2) > LUA_TNIL)
      bg = soup::Rgb(static_cast<uint32_t>(luaL_checkinteger(L, -1)));
  }

  bool fail = false;
  try {
    std::string content(data, size);
    pushcanvas(L, soup::QrCode::encodeText(content, ecl).toCanvas(border, fg, bg));
  }
  catch (...) {
    fail = true;
  }
  if (l_unlikely(fail))
    luaL_error(L, "failed to encode qrcode");
  return 1;
}

static int canvas_get (lua_State* L) {
  const auto c = checkcanvas(L, 1);
  const auto x = static_cast<unsigned int>(luaL_checkinteger(L, 2));
  const auto y = static_cast<unsigned int>(luaL_checkinteger(L, 3));
  if (l_unlikely(x >= c->width || y >= c->height)) {
    luaL_error(L, "out of bounds");
  }
  lua_pushinteger(L, c->get(x, y).toInt());
  return 1;
}

static int canvas_set (lua_State* L) {
  const auto c = checkcanvas(L, 1);
  const auto x = static_cast<unsigned int>(luaL_checkinteger(L, 2));
  const auto y = static_cast<unsigned int>(luaL_checkinteger(L, 3));
  const auto v = soup::Rgb(static_cast<uint32_t>(luaL_checkinteger(L, 4)));
  if (l_unlikely(x >= c->width || y >= c->height)) {
    luaL_error(L, "out of bounds");
  }
  c->set(x, y, v);
  return 0;
}

static int canvas_fill (lua_State* L) {
  const auto c = checkcanvas(L, 1);
  const auto v = soup::Rgb(static_cast<uint32_t>(luaL_checkinteger(L, 2)));
  c->fill(v);
  return 0;
}

static int canvas_size (lua_State *L) {
  const auto c = checkcanvas(L, 1);
  lua_pushinteger(L, c->width);
  lua_pushinteger(L, c->height);
  return 2;
}

static int canvas_mulsize (lua_State *L) {
  const auto c = checkcanvas(L, 1);
  const auto x = static_cast<unsigned int>(luaL_checkinteger(L, 2));
  if (l_unlikely(x < 2))
    luaL_error(L, "multiplier must be at least 2");
  c->resizeNearestNeighbour(c->width * x, c->height * x);
  return 0;
}

static int canvas_tobmp (lua_State* L) {
  pluto_pushstring(L, checkcanvas(L, 1)->toBmp());
  return 1;
}

static int canvas_topng (lua_State* L) {
  pluto_pushstring(L, checkcanvas(L, 1)->toPng());
  return 1;
}

static int canvas_tobwstring(lua_State* L) {
  pluto_pushstring(L, checkcanvas(L, 1)->toStringDownsampledDoublewidthUtf8(true, false, soup::Rgb(static_cast<uint32_t>(luaL_checkinteger(L, 2)))));
  return 1;
}

static const luaL_Reg funcs_canvas[] = {
  {"new", canvas_new},
  {"bmp", canvas_bmp},
  {"qrcode", canvas_qrcode},
  {"get", canvas_get},
  {"set", canvas_set},
  {"fill", canvas_fill},
  {"size", canvas_size},
  {"mulsize", canvas_mulsize},
  {"tobmp", canvas_tobmp},
  {"topng", canvas_topng},
  {"tobwstring", canvas_tobwstring},
  {nullptr, nullptr}
};
PLUTO_NEWLIB(canvas);
