// Aseprite
// Copyright (C) 2020-2023, 2025  Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.

#ifdef HAVE_CONFIG_H
  #include "config.h"
#endif

#include "app/app.h"
#include "app/app_menus.h"
#include "app/commands/command.h"
#include "app/commands/commands.h"
#include "app/console.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#include "app/ui/app_menuitem.h"
#include "base/version.h"

namespace app { namespace script {

namespace {

struct Plugin {
  Extension* ext;
  Plugin(Extension* ext) : ext(ext) {}
};

class PluginCommand : public Command {
public:
  PluginCommand(const std::string& id,
                const std::string& title,
                int onclickRef,
                int onenabledRef,
                int oncheckedRef)
    : Command(id.c_str())
    , m_title(title)
    , m_onclickRef(onclickRef)
    , m_onenabledRef(onenabledRef)
    , m_oncheckedRef(oncheckedRef)
  {
  }

  ~PluginCommand()
  {
    auto app = App::instance();
    ASSERT(app);
    if (!app)
      return;

    if (script::Engine* engine = app->scriptEngine()) {
      lua_State* L = engine->luaState();
      luaL_unref(L, LUA_REGISTRYINDEX, m_onclickRef);
    }
  }

  bool isPlugin() override { return true; }

protected:
  std::string onGetFriendlyName() const override { return m_title; }

  void onExecute(Context* context) override
  {
    script::Engine* engine = App::instance()->scriptEngine();
    lua_State* L = engine->luaState();

    lua_rawgeti(L, LUA_REGISTRYINDEX, m_onclickRef);
    if (lua_pcall(L, 0, 1, 0)) {
      if (const char* s = lua_tostring(L, -1)) {
        Console().printf("Error: %s", s);
      }
    }
    else {
      lua_pop(L, 1);
    }
  }

  bool onEnabled(Context* context) override
  {
    if (m_onenabledRef) {
      return callScriptRef(m_onenabledRef);
    }
    return true;
  }

  bool onChecked(Context* context) override
  {
    if (m_oncheckedRef) {
      return callScriptRef(m_oncheckedRef);
    }
    return false;
  }

private:
  bool callScriptRef(int ref)
  {
    ASSERT(ref);
    script::Engine* engine = App::instance()->scriptEngine();
    lua_State* L = engine->luaState();

    lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
    if (lua_pcall(L, 0, 1, 0)) {
      if (const char* s = lua_tostring(L, -1))
        Console().printf("Error: %s", s);
      return false;
    }

    bool ret = lua_toboolean(L, -1);
    lua_pop(L, 1);
    return ret;
  }

  std::string m_title;
  int m_onclickRef;
  int m_onenabledRef;
  int m_oncheckedRef;
};

void deleteCommandIfExistent(Extension* ext, const std::string& id)
{
  auto cmd = Commands::instance()->byId(id.c_str());
  if (cmd) {
    Commands::instance()->remove(cmd);
    ext->removeCommand(id);
    delete cmd;
  }
}

void deleteMenuGroupIfExistent(Extension* ext, const std::string& id)
{
  if (auto* appMenus = AppMenus::instance())
    appMenus->removeMenuGroup(id);

  ext->removeMenuGroup(id);
}

int Plugin_gc(lua_State* L)
{
  get_obj<Plugin>(L, 1)->~Plugin();
  return 0;
}

int Plugin_newCommand(lua_State* L)
{
  auto plugin = get_obj<Plugin>(L, 1);
  if (lua_istable(L, 2)) {
    std::string id, title, group;
    int onenabledRef = 0;
    int oncheckedRef = 0;

    lua_getfield(L, 2, "id");
    if (const char* s = lua_tostring(L, -1)) {
      id = s;
    }
    lua_pop(L, 1);

    if (id.empty())
      return luaL_error(L, "Empty id field in plugin:newCommand{ id=... }");

    lua_getfield(L, 2, "title");
    if (const char* s = lua_tostring(L, -1)) {
      title = s;
    }
    lua_pop(L, 1);

    lua_getfield(L, 2, "group");
    if (const char* s = lua_tostring(L, -1)) {
      group = s;
    }
    lua_pop(L, 1);

    int type = lua_getfield(L, 2, "onenabled");
    if (type == LUA_TFUNCTION) {
      onenabledRef = luaL_ref(L, LUA_REGISTRYINDEX); // does a pop
    }
    else {
      lua_pop(L, 1);
    }

    type = lua_getfield(L, 2, "onchecked");
    if (type == LUA_TFUNCTION) {
      oncheckedRef = luaL_ref(L, LUA_REGISTRYINDEX); // does a pop
    }
    else {
      lua_pop(L, 1);
    }

    type = lua_getfield(L, 2, "onclick");
    if (type == LUA_TFUNCTION) {
      int onclickRef = luaL_ref(L, LUA_REGISTRYINDEX);

      // Delete the command if it already exist (e.g. we are
      // overwriting a previous registered command)
      deleteCommandIfExistent(plugin->ext, id);

      auto cmd = new PluginCommand(id, title, onclickRef, onenabledRef, oncheckedRef);
      Commands::instance()->add(cmd);
      plugin->ext->addCommand(id);

      // Add a new menu option if the "group" is defined
      if (!group.empty() && App::instance()->isGui()) { // On CLI menus do not make sense
        if (auto appMenus = AppMenus::instance()) {
          auto menuItem = std::make_unique<AppMenuItem>(title, id);
          menuItem->processMnemonicFromText();
          appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
        }
      }
    }
    else {
      lua_pop(L, 1);
    }
  }
  return 0;
}

int Plugin_deleteCommand(lua_State* L)
{
  std::string id;

  auto plugin = get_obj<Plugin>(L, 1);
  if (lua_istable(L, 2)) {
    lua_getfield(L, 2, "id");
    if (const char* s = lua_tostring(L, -1)) {
      id = s;
    }
    lua_pop(L, 1);
  }
  else if (const char* s = lua_tostring(L, 2)) {
    id = s;
  }

  if (id.empty())
    return luaL_error(L, "No command id specified in plugin:deleteCommand()");

  // TODO this can crash if we delete the command from the same command
  deleteCommandIfExistent(plugin->ext, id);
  return 0;
}

int Plugin_newMenuGroup(lua_State* L)
{
  auto plugin = get_obj<Plugin>(L, 1);
  if (lua_istable(L, 2)) {
    std::string id, title, group;

    lua_getfield(L, 2, "id"); // This new group ID
    if (const char* s = lua_tostring(L, -1)) {
      id = s;
    }
    lua_pop(L, 1);

    if (id.empty())
      return luaL_error(L, "Empty id field in plugin:newMenuGroup{ id=... }");

    lua_getfield(L, 2, "title");
    if (const char* s = lua_tostring(L, -1)) {
      title = s;
    }
    lua_pop(L, 1);

    lua_getfield(L, 2, "group"); // Parent group
    if (const char* s = lua_tostring(L, -1)) {
      group = s;
    }
    lua_pop(L, 1);

    // Delete the group if it already exist (e.g. we are overwriting a
    // previous registered group)
    deleteMenuGroupIfExistent(plugin->ext, id);

    plugin->ext->addMenuGroup(id);

    // Add a new menu option if the "group" is defined
    if (!group.empty() && App::instance()->isGui()) { // On CLI menus do not make sense
      if (auto appMenus = AppMenus::instance()) {
        auto menuItem = std::make_unique<AppMenuItem>(title, id);
        menuItem->setSubmenu(new Menu);
        appMenus->addMenuGroup(id, menuItem.get());
        appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
      }
    }
  }
  return 0;
}

int Plugin_deleteMenuGroup(lua_State* L)
{
  std::string id;

  auto plugin = get_obj<Plugin>(L, 1);
  if (lua_istable(L, 2)) {
    lua_getfield(L, 2, "id");
    if (const char* s = lua_tostring(L, -1)) {
      id = s;
    }
    lua_pop(L, 1);
  }
  else if (const char* s = lua_tostring(L, 2)) {
    id = s;
  }

  if (id.empty())
    return luaL_error(L, "No menu group id specified in plugin:deleteMenuGroup()");

  deleteMenuGroupIfExistent(plugin->ext, id);
  return 0;
}

int Plugin_newMenuSeparator(lua_State* L)
{
  auto* plugin = get_obj<Plugin>(L, 1);
  if (lua_istable(L, 2)) {
    std::string group;

    lua_getfield(L, 2, "group"); // Parent group
    if (const char* s = lua_tostring(L, -1)) {
      group = s;
    }
    lua_pop(L, 1);

    // Add a new separator if the "group" is defined
    if (!group.empty() && App::instance()->isGui()) { // On CLI menus do not make sense
      if (auto appMenus = AppMenus::instance()) {
        auto menuItem = std::make_unique<ui::MenuSeparator>();
        plugin->ext->addMenuSeparator(menuItem.get());
        appMenus->addMenuItemIntoGroup(group, std::move(menuItem));
      }
    }
  }
  return 0;
}

int Plugin_newFileFormat(lua_State* L)
{
  auto* plugin = get_obj<Plugin>(L, 1);
  if (!lua_istable(L, 2))
    return luaL_error(L,
                      "plugin:newFileFormat() must be called with a table as its first argument");

  Extension::CustomFormatDefinition format;

  int type = lua_getfield(L, 2, "binary");
  if (type != LUA_TNIL)
    format.binary = lua_toboolean(L, -1);
  lua_pop(L, 1);

  type = lua_getfield(L, 2, "supports");
  if (type != LUA_TNIL)
    format.flags = lua_tointeger(L, -1);
  lua_pop(L, 1);

  for (const char* field : { "extensions", "extension" }) {
    type = lua_getfield(L, 2, field);
    if (type == LUA_TTABLE) {
      lua_pushnil(L);
      while (lua_next(L, -2) != 0) {
        format.extensions.push_back(base::string_to_lower(luaL_tolstring(L, -1, nullptr)));
        lua_pop(L, 2);
      }
    }
    else if (type == LUA_TSTRING) {
      format.extensions.push_back(base::string_to_lower(lua_tostring(L, -1)));
    }
    else if (type != LUA_TNIL) {
      lua_pop(L, 2);
      return luaL_error(L, "format parameter \"%s\" must be a list or string", field);
    }
    lua_pop(L, 1);
  }

  if (format.extensions.empty()) {
    lua_pop(L, 1);
    return luaL_error(L, "format extension list is empty");
  }

  for (const auto& ext : format.extensions) {
    if (ext.empty()) {
      lua_pop(L, 1);
      return luaL_error(L, "format extension is empty");
    }
  }

  std::sort(format.extensions.begin(), format.extensions.end());
  format.extensions.erase(std::unique(format.extensions.begin(), format.extensions.end()),
                          format.extensions.end());

  type = lua_getfield(L, 2, "name");
  if (type != LUA_TNIL)
    format.name = lua_tostring(L, -1);
  else
    format.name = format.extensions.front();
  lua_pop(L, 1);

  type = lua_getfield(L, 2, "onload");
  if (type == LUA_TFUNCTION)
    format.onloadRef = luaL_ref(L, LUA_REGISTRYINDEX);
  else
    lua_pop(L, 1);

  type = lua_getfield(L, 2, "onsave");
  if (type == LUA_TFUNCTION)
    format.onsaveRef = luaL_ref(L, LUA_REGISTRYINDEX);
  else
    lua_pop(L, 1);

  if (format.onloadRef == -1 && format.onsaveRef == -1) {
    lua_pop(L, 1);
    return luaL_error(L, "format must have an onload or onsave function");
  }

  plugin->ext->addFileFormat(format);
  return 0;
}

int Plugin_get_name(lua_State* L)
{
  auto* plugin = get_obj<Plugin>(L, 1);
  lua_pushstring(L, plugin->ext->name().c_str());
  return 1;
}

int Plugin_get_displayName(lua_State* L)
{
  auto* plugin = get_obj<Plugin>(L, 1);
  lua_pushstring(L, plugin->ext->displayName().c_str());
  return 1;
}

int Plugin_get_path(lua_State* L)
{
  auto* plugin = get_obj<Plugin>(L, 1);
  lua_pushstring(L, plugin->ext->path().c_str());
  return 1;
}

int Plugin_get_version(lua_State* L)
{
  auto* plugin = get_obj<Plugin>(L, 1);
  const base::Version version(plugin->ext->version());
  push_version(L, version);
  return 1;
}

int Plugin_get_preferences(lua_State* L)
{
  if (!lua_getuservalue(L, 1)) {
    lua_newtable(L);
    lua_pushvalue(L, -1);
    lua_setuservalue(L, 1);
  }
  return 1;
}

int Plugin_set_preferences(lua_State* L)
{
  lua_pushvalue(L, 2);
  lua_setuservalue(L, 1);
  return 0;
}

const luaL_Reg Plugin_methods[] = {
  { "__gc",             Plugin_gc               },
  { "newCommand",       Plugin_newCommand       },
  { "deleteCommand",    Plugin_deleteCommand    },
  { "newMenuGroup",     Plugin_newMenuGroup     },
  { "deleteMenuGroup",  Plugin_deleteMenuGroup  },
  { "newMenuSeparator", Plugin_newMenuSeparator },
  { "newFileFormat",    Plugin_newFileFormat    },
  { nullptr,            nullptr                 }
};

const Property Plugin_properties[] = {
  { "name",        Plugin_get_name,        nullptr                },
  { "displayName", Plugin_get_displayName, nullptr                },
  { "path",        Plugin_get_path,        nullptr                },
  { "version",     Plugin_get_version,     nullptr                },
  { "preferences", Plugin_get_preferences, Plugin_set_preferences },
  { nullptr,       nullptr,                nullptr                }
};

} // anonymous namespace

DEF_MTNAME(Plugin);

void register_plugin_class(lua_State* L)
{
  REG_CLASS(L, Plugin);
  REG_CLASS_PROPERTIES(L, Plugin);
}

void push_plugin(lua_State* L, Extension* ext)
{
  push_new<Plugin>(L, ext);
}

}} // namespace app::script
