// Aseprite
// Copyright (C) 2022-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/cmd/set_tile_data_properties.h"
#include "app/cmd/set_tile_data_property.h"
#include "app/cmd/set_user_data_properties.h"
#include "app/cmd/set_user_data_property.h"
#include "app/script/docobj.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#include "app/script/values.h"
#include "app/tx.h"
#include "doc/tileset.h"
#include "doc/with_user_data.h"

#include <cstring>

namespace app { namespace script {

namespace {

struct Properties {
  // WithUserData ID or tileset ID
  doc::ObjectId id = 0;
  // If notile, it's the properties of a WithUserData, in other case,
  // it's the user data of this specific tile.
  doc::tile_index ti = doc::notile;
  std::string extID;

  Properties(const doc::WithUserData* wud, const std::string& extID) : id(wud->id()), extID(extID)
  {
  }

  Properties(const doc::Tileset* ts, const doc::tile_index ti, const std::string& extID)
    : id(ts->id())
    , ti(ti)
    , extID(extID)
  {
  }

  Properties(const Properties& copy, const std::string& extID)
    : id(copy.id)
    , ti(copy.ti)
    , extID(extID)
  {
  }

  doc::WithUserData* object(lua_State* L)
  {
    auto obj = static_cast<doc::WithUserData*>(doc::get_object(id));
    if (!obj) {
      // luaL_error never returns
      luaL_error(L, "the object with these properties was destroyed");
    }
    return obj;
  }

  doc::UserData::Properties& properties(lua_State* L, doc::WithUserData* obj = nullptr)
  {
    if (!obj)
      obj = object(L);
    if (ti == doc::notile) {
      return obj->userData().properties(extID);
    }
    else {
      ASSERT(obj->type() == doc::ObjectType::Tileset);
      return const_cast<doc::UserData*>(&static_cast<doc::Tileset*>(obj)->getTileData(ti))
        ->properties(extID);
    }
  }
};

using PropertiesIterator = doc::UserData::Properties::iterator;

int Properties_len(lua_State* L)
{
  auto propObj = get_obj<Properties>(L, 1);
  auto& properties = propObj->properties(L);
  lua_pushinteger(L, properties.size());
  return 1;
}

int Properties_index(lua_State* L)
{
  auto propObj = get_obj<Properties>(L, 1);
  const char* field = lua_tostring(L, 2);
  if (!field)
    return luaL_error(L, "field in 'properties.field' must be a string");

  auto& properties = propObj->properties(L);
  auto it = properties.find(field);
  if (it != properties.end()) {
    push_value_to_lua(L, (*it).second);
  }
  else {
    lua_pushnil(L);
  }
  return 1;
}

int Properties_newindex(lua_State* L)
{
  auto propObj = get_obj<Properties>(L, 1);
  const char* field = lua_tostring(L, 2);
  if (!field)
    return luaL_error(L, "field in 'properties.field' must be a string");

  auto obj = static_cast<doc::WithUserData*>(get_object(propObj->id));
  if (!obj)
    return luaL_error(L, "the object with these properties was destroyed");

  auto& properties = propObj->properties(L, obj);
  auto newValue = get_value_from_lua<doc::UserData::Variant>(L, 3);

  // Set property value with undo info
  // TODO add Object::sprite() member function
  // if (obj->sprite()) {
  if (Doc* doc = App::instance()->context()->activeDocument(); doc && doc->transaction()) {
    Tx tx(doc); // TODO propObj might not be member of "doc"
    if (propObj->ti != doc::notile) {
      tx(new cmd::SetTileDataProperty(static_cast<doc::Tileset*>(obj),
                                      propObj->ti,
                                      propObj->extID,
                                      field,
                                      std::move(newValue)));
    }
    else {
      tx(new cmd::SetUserDataProperty(obj, propObj->extID, field, std::move(newValue)));
    }
    tx.commit();
  }
  // Set property value without undo
  else {
    doc::set_property_value(properties, field, std::move(newValue));
  }
  return 0;
}

int Properties_call(lua_State* L)
{
  auto propObj = get_obj<Properties>(L, 1);
  const char* extID = lua_tostring(L, 2);
  if (!extID)
    return luaL_error(L, "extensionID in 'properties(\"extensionID\")' must be a string");

  // Special syntax to change the full extension properties using:
  //
  //   object.property("extension", { ...})
  //
  doc::UserData::Properties newProperties;
  if (lua_istable(L, 3)) {
    newProperties = get_value_from_lua<doc::UserData::Properties>(L, 3);
  }
  else if (auto* argProperties = may_get_properties(L, 3)) {
    newProperties = *argProperties;
  }
  else {
    // Just return the properties
    push_new<Properties>(L, *propObj, extID);
    return 1;
  }

  auto obj = static_cast<doc::WithUserData*>(get_object(propObj->id));
  if (!obj)
    return luaL_error(L, "the object with these properties was destroyed");

  // TODO add Object::sprite() member function
  // if (obj->sprite()) {
  if (Doc* doc = App::instance()->context()->activeDocument(); doc && doc->transaction()) {
    Tx tx(doc); // TODO propObj might not be member of "doc"
    if (propObj->ti != doc::notile) {
      tx(new cmd::SetTileDataProperties(static_cast<doc::Tileset*>(obj),
                                        propObj->ti,
                                        extID,
                                        std::move(newProperties)));
    }
    else {
      tx(new cmd::SetUserDataProperties(obj, extID, std::move(newProperties)));
    }
    tx.commit();
  }
  // Copy the newProperties into properties(extID)
  else {
    Properties propObjExt(*propObj, extID);
    auto& properties = propObjExt.properties(L);
    properties = std::move(newProperties);
  }

  push_new<Properties>(L, *propObj, extID);
  return 1;
}

int Properties_pairs_next(lua_State* L)
{
  auto propObj = get_obj<Properties>(L, 1);
  auto& properties = propObj->properties(L);
  auto& it = *get_obj<PropertiesIterator>(L, lua_upvalueindex(1));
  if (it == properties.end())
    return 0;
  lua_pushstring(L, (*it).first.c_str());
  push_value_to_lua(L, (*it).second);
  ++it;
  return 2;
}

int Properties_pairs(lua_State* L)
{
  auto propObj = get_obj<Properties>(L, 1);
  auto obj = static_cast<doc::WithUserData*>(get_object(propObj->id));
  if (!obj)
    return luaL_error(L, "the object with these properties was destroyed");

  auto& properties = propObj->properties(L);

  push_obj(L, properties.begin());
  lua_pushcclosure(L, Properties_pairs_next, 1);
  lua_pushvalue(L, 1); // Copy the same propObj as the second return value
  return 2;
}

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

const luaL_Reg Properties_methods[] = {
  { "__len",      Properties_len      },
  { "__call",     Properties_call     },
  { "__index",    Properties_index    },
  { "__newindex", Properties_newindex },
  { "__pairs",    Properties_pairs    },
  { nullptr,      nullptr             }
};

const luaL_Reg PropertiesIterator_methods[] = {
  { "__gc",  PropertiesIterator_gc },
  { nullptr, nullptr               }
};

} // anonymous namespace

DEF_MTNAME(Properties);
DEF_MTNAME(PropertiesIterator);

void register_properties_class(lua_State* L)
{
  REG_CLASS(L, Properties);
  REG_CLASS(L, PropertiesIterator);
}

void push_properties(lua_State* L, doc::WithUserData* wud, const std::string& extID)
{
  push_new<Properties>(L, wud, extID);
}

void push_tile_properties(lua_State* L,
                          const doc::Tileset* ts,
                          doc::tile_index ti,
                          const std::string& extID)
{
  push_new<Properties>(L, ts, ti, extID);
}

doc::UserData::Properties* may_get_properties(lua_State* L, int index)
{
  auto propObj = may_get_obj<Properties>(L, index);
  if (!propObj)
    return nullptr;

  return &propObj->properties(L);
}

}} // namespace app::script
