/**
 * server/src/lua/sandbox.cpp - Lua sandbox
 *
 * This file is part of the traintastic source code.
 *
 * Copyright (C) 2019-2022 Reinder Feenstra
 *
 * 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 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "sandbox.hpp"
#include "push.hpp"
#include "object.hpp"
#include "method.hpp"
#include "event.hpp"
#include "eventhandler.hpp"
#include "log.hpp"
#include "class.hpp"
#include "to.hpp"
#include "type.hpp"
#include "enums.hpp"
#include "sets.hpp"
#include "getversion.hpp"
#include <version.hpp>
#include <traintastic/utils/str.hpp>
#include "../world/world.hpp"

#define LUA_SANDBOX "_sandbox"
#define LUA_SANDBOX_GLOBALS "_sandbox_globals"

constexpr std::array<std::string_view, 23> readOnlyGlobals = {{
  // Lua baselib:
  "assert",
  "type",
  "pairs",
  "ipairs",
  "next",
  "tonumber",
  "tostring",
  "_G",
  // Lua libs:
  LUA_MATHLIBNAME,
  LUA_STRLIBNAME,
  LUA_TABLIBNAME,
  // Constants:
  "VERSION",
  "VERSION_MAJOR",
  "VERSION_MINOR",
  "VERSION_PATCH",
  "CODENAME",
  "LUA_VERSION",
  // Objects:
  "world",
  "log",
  // Functions:
  "is_instance",
  // Type info:
  "class",
  "enum",
  "set",
}};

static void addExtensions(lua_State* L, std::initializer_list<std::pair<const char*, lua_CFunction>> extensions)
{
  assert(lua_istable(L, -1));
  for(auto [name, func] : extensions)
  {
    lua_pushcfunction(L, func);
    lua_setfield(L, -2, name);
  }
}

static void addBaseLib(lua_State* L, std::initializer_list<const char*> names, std::initializer_list<std::pair<const char*, lua_CFunction>> extensions = {})
{
  // load Lua baselib:
  lua_pushcfunction(L, luaopen_base);
  lua_pushliteral(L, "");
  lua_call(L, 1, 0);

  for(const char* name : names)
  {
    lua_getglobal(L, name);
    assert(!lua_isnil(L, -1));
    lua_setfield(L, -2, name);
  }

  addExtensions(L, extensions);
}

static void addLib(lua_State* L, const char* libraryName, lua_CFunction openFunction, std::initializer_list<const char*> names, std::initializer_list<std::pair<const char*, lua_CFunction>> extensions = {})
{
  lua_createtable(L, 0, names.size() + extensions.size());

  luaL_requiref(L, libraryName, openFunction, 1);

  for(const char* name : names)
  {
    lua_getfield(L, -1, name);
    assert(!lua_isnil(L, -1));
    lua_setfield(L, -3, name);
  }

  lua_pop(L, 1); // pop lib

  addExtensions(L, extensions);

  Lua::ReadOnlyTable::wrap(L, -1);
  lua_setfield(L, -2, libraryName);
}

namespace Lua {

void Sandbox::close(lua_State* L)
{
  delete *static_cast<StateData**>(lua_getextraspace(L)); // free state data
  lua_close(L);
}

int Sandbox::__index(lua_State* L)
{
  lua_getglobal(L, LUA_SANDBOX_GLOBALS);
  lua_replace(L, 1);

  lua_rawget(L, 1);

  return 1;
}

int Sandbox::__newindex(lua_State* L)
{
  if(std::find(readOnlyGlobals.begin(), readOnlyGlobals.end(), to<std::string_view>(L, 2)) != readOnlyGlobals.end())
    errorGlobalNIsReadOnly(L, lua_tostring(L, 2));

  lua_getglobal(L, LUA_SANDBOX_GLOBALS);
  lua_replace(L, 1);

  lua_rawset(L, 1);

  return 0;
}

SandboxPtr Sandbox::create(Script& script)
{
  lua_State* L = luaL_newstate();

  // create state data:
  *static_cast<StateData**>(lua_getextraspace(L)) = new StateData(script);

  // register types:
  Enums::registerTypes<LUA_ENUMS>(L);
  Sets::registerTypes<LUA_SETS>(L);
  Object::registerType(L);
  Method::registerType(L);
  Event::registerType(L);

  // setup sandbox:
  lua_newtable(L);
  luaL_newmetatable(L, LUA_SANDBOX);
  lua_pushcfunction(L, __index);
  lua_setfield(L, -2, "__index");
  lua_pushcfunction(L, __newindex);
  lua_setfield(L, -2, "__newindex");
  lua_setmetatable(L, -2);
  lua_setglobal(L, LUA_SANDBOX);

  // setup globals:
  lua_newtable(L);

  // add standard Lua lib functions and extensions/replacements:
  addBaseLib(L,
    {
      "assert", "pairs", "ipairs", "next", "tonumber", "tostring",
    },
    {
      {"type", type},
    });
  addLib(L, LUA_MATHLIBNAME, luaopen_math, {
    "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp",
    "floor", "fmod", "huge", "log", "max", "maxinteger", "min", "mininteger",
    "modf", "pi", "rad", "random", "randomseed", "sin", "sqrt", "tan",
    "tointeger", "type", "ult",
    });
  addLib(L, LUA_STRLIBNAME, luaopen_string, {
    "byte", "char", "find", "format", "gmatch", "gsub", "len", "lower",
    "match", "pack", "packsize", "rep", "reverse", "sub", "unpack", "upper",
    });
  addLib(L, LUA_TABLIBNAME, luaopen_table, {
    "concat", "insert", "pack", "unpack", "remove", "move", "sort",
    });

  // set VERSION:
  lua_pushstring(L, TRAINTASTIC_VERSION_FULL);
  lua_setfield(L, -2, "VERSION");
  lua_pushinteger(L, TRAINTASTIC_VERSION_MAJOR);
  lua_setfield(L, -2, "VERSION_MAJOR");
  lua_pushinteger(L, TRAINTASTIC_VERSION_MINOR);
  lua_setfield(L, -2, "VERSION_MINOR");
  lua_pushinteger(L, TRAINTASTIC_VERSION_PATCH);
  lua_setfield(L, -2, "VERSION_PATCH");

  // set LUA_VERSION
  push(L, getVersion());
  lua_setfield(L, -2, "LUA_VERSION");

  // add world:
  push(L, script.world().shared_from_this());
  lua_setfield(L, -2, "world");

  // add logger:
  Log::push(L);
  lua_setfield(L, -2, "log");

  // add class types:
  lua_newtable(L);
  Class::registerValues(L);
  ReadOnlyTable::wrap(L, -1);
  lua_setfield(L, -2, "class");

  // add enum values:
  lua_newtable(L);
  Enums::registerValues<LUA_ENUMS>(L);
  ReadOnlyTable::wrap(L, -1);
  lua_setfield(L, -2, "enum");

  // add set values:
  lua_newtable(L);
  Sets::registerValues<LUA_SETS>(L);
  ReadOnlyTable::wrap(L, -1);
  lua_setfield(L, -2, "set");

  // let global _G point to the sandbox:
  lua_getglobal(L, LUA_SANDBOX);
  lua_setfield(L, -2, "_G");

  lua_setglobal(L, LUA_SANDBOX_GLOBALS);

  return SandboxPtr(L, close);
}

Sandbox::StateData& Sandbox::getStateData(lua_State* L)
{
  return **static_cast<StateData**>(lua_getextraspace(L));
}

int Sandbox::getGlobal(lua_State* L, const char* name)
{
  lua_getglobal(L, LUA_SANDBOX_GLOBALS); // get the sandbox
  lua_pushstring(L, name);
  const int type = lua_gettable(L, -2); // get item
  lua_insert(L, lua_gettop(L) - 1); // swap item and sandbox on the stack
  lua_pop(L, 1); // remove sandbox from the stack
  return type;
}

int Sandbox::pcall(lua_State* L, int nargs, int nresults, int errfunc)
{
  // check if the function has _ENV as first upvalue
  // if so, replace it by the sandbox
  // NOTE: functions which don't use any globals, don't have an _ENV !!
  assert(lua_isfunction(L, -(1 + nargs)));
  const char* name = lua_getupvalue(L, -(1 + nargs), 1);
  if(name)
    lua_pop(L, 1); // remove upvalue from stack
  if(name && strcmp(name, "_ENV") == 0)
  {
    lua_getglobal(L, LUA_SANDBOX); // get the sandbox
    assert(lua_istable(L, -1));
    if(!lua_setupvalue(L, -(2 + nargs), 1)) // change _ENV to the sandbox
    {
      assert(false); // should never happen
      lua_pop(L, 2 + nargs); // clear stack
      lua_pushliteral(L, "Internal error @ " __FILE__ ":" STR(__LINE__));
      return LUA_ERRRUN;
    }
  }
  return lua_pcall(L, nargs, nresults, errfunc);
}

Sandbox::StateData::~StateData()
{
  while(!m_eventHandlers.empty())
  {
    auto handler = m_eventHandlers.begin()->second;
    m_eventHandlers.erase(m_eventHandlers.begin());
    handler->disconnect();
  }
}

}
