/*
 *
 * luamm:  C++ binding for lua
 *
 * Copyright (C) 2010 Pavel Labath et al.
 *
 * 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <config.h>

#include "luamm.hh"

namespace lua {
namespace {

#if LUA_VERSION_NUM >= 502
// These two functions were deprecated in 5.2. Limited backwards compatibility
// is provided by macros. We want them as real functions, because we take their
// addresses.

#undef lua_equal
int lua_equal(lua_State *L, int index1, int index2) {
  return lua_compare(L, index1, index2, LUA_OPEQ);
}

#undef lua_lessthan
int lua_lessthan(lua_State *L, int index1, int index2) {
  return lua_compare(L, index1, index2, LUA_OPLT);
}
#endif

// keys for storing values in lua registry
const char cpp_exception_metatable[] = "lua::cpp_exception_metatable";
const char cpp_function_metatable[] = "lua::cpp_function_metatable";
const char lua_exception_namespace[] = "lua::lua_exception_namespace";
const char this_cpp_object[] = "lua::this_cpp_object";

// converts C++ exceptions to strings, so lua can do something with them
int exception_to_string(lua_State *l) {
  auto *ptr = static_cast<std::exception_ptr *>(lua_touserdata(l, -1));
  assert(ptr);
  try {
    std::rethrow_exception(*ptr);
  } catch (std::exception &e) { lua_pushstring(l, e.what()); } catch (...) {
    lua_pushstring(l, typeid(*ptr).name());
  }
  return 1;
}

int absindex(lua_State *l, int index) {
  return index < 0 && -index <= lua_gettop(l) ? lua_gettop(l) + 1 + index
                                              : index;
}

// Just like getfield(), only without calling metamethods (or throwing random
// exceptions)
inline void rawgetfield(lua_State *l, int index, const char *k) {
  index = absindex(l, index);
  if (lua_checkstack(l, 1) == 0) { throw std::bad_alloc(); }

  lua_pushstring(l, k);
  lua_rawget(l, index);
}

// Just like setfield(), only without calling metamethods (or throwing random
// exceptions)
inline void rawsetfield(lua_State *l, int index, const char *k) {
  index = absindex(l, index);
  if (lua_checkstack(l, 2) == 0) { throw std::bad_alloc(); }

  lua_pushstring(l, k);
  lua_insert(l, -2);
  lua_rawset(l, index);
}

int closure_trampoline(lua_State *l) {
  lua_checkstack(l, 2);
  rawgetfield(l, REGISTRYINDEX, this_cpp_object);
  assert(lua_islightuserdata(l, -1));
  auto *L = static_cast<state *>(lua_touserdata(l, -1));
  lua_pop(l, 1);

  try {
    auto *fn = static_cast<cpp_function *>(L->touserdata(lua_upvalueindex(1)));
    assert(fn);
    return (*fn)(L);
  } catch (lua::exception &e) {
    // rethrow lua errors as such
    e.push_lua_error(L);
  } catch (...) {
    // C++ exceptions (pointers to them, actually) are stored as lua userdata
    // and then thrown
    L->createuserdata<std::exception_ptr>(std::current_exception());
    L->rawgetfield(REGISTRYINDEX, cpp_exception_metatable);
    L->setmetatable(-2);
  }

  // lua_error does longjmp(), so destructors for objects in this function will
  // not be called
  return lua_error(l);
}

/*
 * This function is called when lua encounters an error outside of any protected
 * environment
 * Throwing the exception through lua code appears to work, even if it was
 * compiled without -fexceptions. If it turns out, it fails in some conditions,
 * it could be replaced with some longjmp() magic. But that shouldn't be
 * necessary, as this function will not be called under normal conditions (we
 * execute everything in protected mode).
 */
int panic_throw(lua_State *l) {
  if (lua_checkstack(l, 1) == 0) { throw std::bad_alloc(); }

  rawgetfield(l, REGISTRYINDEX, this_cpp_object);
  assert(lua_islightuserdata(l, -1));
  auto *L = static_cast<state *>(lua_touserdata(l, -1));
  lua_pop(l, 1);

  throw lua::exception(L);
}

// protected mode wrappers for various lua functions
int safe_concat_trampoline(lua_State *l) {
  lua_concat(l, lua_gettop(l));
  return 1;
}

template <int (*compare)(lua_State *, int, int)>
int safe_compare_trampoline(lua_State *l) {
  int r = compare(l, 1, 2);
  lua_pop(l, 2);
  lua_pushinteger(l, r);
  return 1;
}

int safe_gc_trampoline(lua_State *l) {
  int what = lua_tointeger(l, -2);
  int data = lua_tointeger(l, -1);
  lua_pop(l, 2);
  lua_pushinteger(l, lua_gc(l, what, data));
  return 1;
}

template <void (*misc)(lua_State *, int), int nresults>
int safe_misc_trampoline(lua_State *l) {
  misc(l, 1);
  return nresults;
}

// Overloaded for Lua 5.3+ as lua_gettable and others return an int
template <int (*misc)(lua_State *, int), int nresults>
int safe_misc_trampoline(lua_State *l) {
  misc(l, 1);
  return nresults;
}

int safe_next_trampoline(lua_State *l) {
  int r = lua_next(l, 1);
  lua_checkstack(l, 1);
  lua_pushinteger(l, r);
  return r != 0 ? 3 : 1;
}

}  // namespace

std::string exception::get_error_msg(state *L) {
  static const std::string default_msg("Unknown lua exception");

  try {
    return L->tostring(-1);
  } catch (not_string_error &e) { return default_msg; }
}

exception::exception(state *l) : std::runtime_error(get_error_msg(l)), L(l) {
  L->checkstack(1);

  L->rawgetfield(REGISTRYINDEX, lua_exception_namespace);
  L->insert(-2);
  key = L->ref(-2);
  L->pop(1);
}

exception::~exception() {
  if (L == nullptr) { return; }
  L->checkstack(1);

  L->rawgetfield(REGISTRYINDEX, lua_exception_namespace);
  L->unref(-1, key);
  L->pop();
}

void exception::push_lua_error(state *l) {
  if (l != L) {
    throw std::runtime_error(
        "Cannot transfer exceptions between different lua contexts");
  }
  l->checkstack(2);

  l->rawgetfield(REGISTRYINDEX, lua_exception_namespace);
  l->rawgeti(-1, key);
  l->replace(-2);
}

state::state() {
  if (lua_State *l = luaL_newstate()) {
    cobj.reset(l, &lua_close);
  } else {
    // docs say this can happen only in case of a memory allocation error
    throw std::bad_alloc();
  }

  // set our panic function
  lua_atpanic(cobj.get(), panic_throw);

  checkstack(2);

  // store a pointer to ourselves
  pushlightuserdata(this);
  rawsetfield(REGISTRYINDEX, this_cpp_object);

  // a metatable for C++ exceptions travelling through lua code
  newmetatable(cpp_exception_metatable);
  lua_pushcfunction(cobj.get(), &exception_to_string);
  rawsetfield(-2, "__tostring");
  pushboolean(false);
  rawsetfield(-2, "__metatable");
  pushdestructor<std::exception_ptr>();
  rawsetfield(-2, "__gc");
  pop();

  // a metatable for C++ functions callable from lua code
  newmetatable(cpp_function_metatable);
  pushboolean(false);
  rawsetfield(-2, "__metatable");
  pushdestructor<cpp_function>();
  rawsetfield(-2, "__gc");
  pop();

  // while they're travelling through C++ code, lua exceptions will reside here
  newtable();
  rawsetfield(REGISTRYINDEX, lua_exception_namespace);

  luaL_openlibs(cobj.get());
}

void state::call(int nargs, int nresults, int errfunc) {
  int r = lua_pcall(cobj.get(), nargs, nresults, errfunc);
  if (r == 0) { return; }

  if (r == LUA_ERRMEM) {
    // memory allocation error, cross your fingers
    throw std::bad_alloc();
  }

  checkstack(3);
  rawgetfield(REGISTRYINDEX, cpp_exception_metatable);
  if (getmetatable(-2)) {
    if (rawequal(-1, -2)) {
      // it's a C++ exception, rethrow it
      auto *ptr = static_cast<std::exception_ptr *>(touserdata(-3));
      assert(ptr);

      /*
       * we create a copy, so we can pop the object without fearing the
       * exception will be collected by lua's GC
       */
      std::exception_ptr t(*ptr);
      ptr = nullptr;
      pop(3);
      std::rethrow_exception(t);
    }
    pop(2);
  }
  // it's a lua exception, wrap it
  if (r == LUA_ERRERR) { throw lua::errfunc_error(this); }
  { throw lua::exception(this); }
}

void state::checkstack(int extra) {
  if (lua_checkstack(cobj.get(), extra) == 0) { throw std::bad_alloc(); }
}

void state::concat(int n) {
  assert(n >= 0);
  checkstack(1);
  lua_pushcfunction(cobj.get(), safe_concat_trampoline);
  insert(-n - 1);
  call(n, 1, 0);
}

bool state::equal(int index1, int index2) {
  // avoid pcall overhead in trivial cases
  if (rawequal(index1, index2)) { return true; }

  return safe_compare(&safe_compare_trampoline<lua_equal>, index1, index2);
}

int state::gc(int what, int data) {
  checkstack(3);
  lua_pushcfunction(cobj.get(), safe_gc_trampoline);
  pushinteger(what);
  pushinteger(data);
  call(2, 1, 0);
  assert(state::_isnumber(-1));
  int r = tointeger(-1);
  pop();
  return r;
}

void state::getfield(int index, const char *k) {
  checkstack(1);
  index = absindex(index);
  pushstring(k);
  gettable(index);
}

void state::getglobal(const char *name) {
#if LUA_VERSION_NUM >= 502
  checkstack(1);
  pushinteger(LUA_RIDX_GLOBALS);
  gettable(REGISTRYINDEX);
  getfield(-1, name);
  replace(-2);
#else
  getfield(LUA_GLOBALSINDEX, name);
#endif
}

void state::gettable(int index) {
  checkstack(2);
  pushvalue(index);
  insert(-2);
  lua_pushcfunction(cobj.get(), (&safe_misc_trampoline<&lua_gettable, 1>));
  insert(-3);
  call(2, 1, 0);
}

bool state::lessthan(int index1, int index2) {
  return safe_compare(&safe_compare_trampoline<&lua_lessthan>, index1, index2);
}

void state::loadfile(const char *filename) {
  switch (luaL_loadfile(cobj.get(), filename)) {
    case 0:
      return;
    case LUA_ERRSYNTAX:
      throw lua::syntax_error(this);
    case LUA_ERRFILE:
      throw lua::file_error(this);
    case LUA_ERRMEM:
      throw std::bad_alloc();
    default:
      assert(0);
  }
}

void state::loadstring(const char *s) {
  switch (luaL_loadstring(cobj.get(), s)) {
    case 0:
      return;
    case LUA_ERRSYNTAX:
      throw lua::syntax_error(this);
    case LUA_ERRMEM:
      throw std::bad_alloc();
    default:
      assert(0);
  }
}

bool state::next(int index) {
  checkstack(2);
  pushvalue(index);
  insert(-2);
  lua_pushcfunction(cobj.get(), &safe_next_trampoline);
  insert(-3);

  call(2, MULTRET, 0);

  assert(state::_isnumber(-1));
  int r = tointeger(-1);
  pop();
  return r != 0;
}

void state::pushclosure(const cpp_function &fn, int n) {
  checkstack(2);

  createuserdata<cpp_function>(fn);
  rawgetfield(REGISTRYINDEX, cpp_function_metatable);
  setmetatable(-2);

  insert(-n - 1);
  lua_pushcclosure(cobj.get(), &closure_trampoline, n + 1);
}

void state::rawgetfield(int index, const char *k) {
  lua::rawgetfield(cobj.get(), index, k);
}

void state::rawsetfield(int index, const char *k) {
  lua::rawsetfield(cobj.get(), index, k);
}

bool state::safe_compare(lua_CFunction trampoline, int index1, int index2) {
  // if one of the indexes is invalid, return false
  if (isnone(index1) || isnone(index2)) { return false; }

  // convert relative indexes into absolute
  index1 = absindex(index1);
  index2 = absindex(index2);

  checkstack(3);
  lua_pushcfunction(cobj.get(), trampoline);
  pushvalue(index1);
  pushvalue(index2);
  call(2, 1, 0);
  assert(state::_isnumber(-1));
  int r = tointeger(-1);
  pop();
  return r != 0;
}

void state::setfield(int index, const char *k) {
  checkstack(1);
  index = absindex(index);
  pushstring(k);
  insert(-2);
  settable(index);
}

void state::setglobal(const char *name) {
#if LUA_VERSION_NUM >= 502
  stack_sentry s(*this, -1);
  checkstack(1);
  pushinteger(LUA_RIDX_GLOBALS);
  gettable(REGISTRYINDEX);
  insert(-2);
  setfield(-2, name);
  pop();
#else
  setfield(LUA_GLOBALSINDEX, name);
#endif
}

void state::settable(int index) {
  checkstack(2);
  pushvalue(index);
  insert(-3);
  lua_pushcfunction(cobj.get(), (&safe_misc_trampoline<&lua_settable, 0>));
  insert(-4);
  call(3, 0, 0);
}

std::string state::tostring(int index) {
  size_t len;
  const char *str = lua_tolstring(cobj.get(), index, &len);
  if (str == nullptr) { throw not_string_error(); }
  return std::string(str, len);
}
}  // namespace lua
