// Aseprite
// Copyright (C) 2018-2025  Igara Studio S.A.
// Copyright (C) 2018  David Capello
//
// 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_layer_blend_mode.h"
#include "app/cmd/set_layer_flags.h"
#include "app/cmd/set_layer_name.h"
#include "app/cmd/set_layer_opacity.h"
#include "app/cmd/set_layer_tileset.h"
#include "app/doc.h"
#include "app/doc_api.h"
#include "app/pref/preferences.h"
#include "app/script/blend_mode.h"
#include "app/script/docobj.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"
#include "app/script/userdata.h"
#include "app/tx.h"
#include "app/ui/timeline/timeline.h"
#include "doc/layer.h"
#include "doc/layer_tilemap.h"
#include "doc/sprite.h"
#include "doc/tileset.h"
#include "doc/tilesets.h"

namespace app { namespace script {

using namespace doc;

namespace {

// Returns false if we are outside a transaction, so you have to
// change the flag manually.
bool set_layer_flags_with_undo(Layer* layer, const bool enableFlag, const LayerFlags flag)
{
  ASSERT(layer->sprite());
  Doc* doc = static_cast<Doc*>(layer->sprite()->document());
  ASSERT(doc);

  // With undo when we are inside a transaction
  if (doc->transaction()) {
    Tx tx(doc);
    LayerFlags newFlags = layer->flags();
    if (enableFlag)
      newFlags |= flag;
    else
      newFlags &= ~flag;

    tx(new cmd::SetLayerFlags(layer, newFlags));
    tx.commit();
    return true;
  }

  // Without undo
  return false;
}

int Layer_eq(lua_State* L)
{
  const auto a = may_get_docobj<Layer>(L, 1);
  const auto b = may_get_docobj<Layer>(L, 2);
  lua_pushboolean(L, (!a && !b) || (a && b && a->id() == b->id()));
  return 1;
}

int Layer_cel(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  auto cel = layer->cel(get_frame_number_from_arg(L, 2));
  if (cel)
    push_docobj<Cel>(L, cel);
  else
    lua_pushnil(L);
  return 1;
}

int Layer_get_id(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushinteger(L, layer->id());
  return 1;
}

int Layer_get_sprite(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  push_docobj<Sprite>(L, layer->sprite());
  return 1;
}

int Layer_get_parent(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  if (layer->parent() == layer->sprite()->root())
    push_docobj<Sprite>(L, layer->sprite());
  else
    push_docobj<Layer>(L, layer->parent());
  return 1;
}

int Layer_get_layers(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  if (layer->isGroup())
    push_group_layers(L, layer);
  else
    lua_pushnil(L);
  return 1;
}

int Layer_get_stackIndex(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const auto& layers = layer->parent()->layers();
  auto it = std::find(layers.begin(), layers.end(), layer);
  ASSERT(it != layers.end());
  if (it != layers.end())
    lua_pushinteger(L, it - layers.begin() + 1);
  else
    lua_pushnil(L);
  return 1;
}

int Layer_get_previous(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  if (auto previous = layer->getPrevious())
    push_docobj<Layer>(L, previous);
  else
    lua_pushnil(L);
  return 1;
}

int Layer_get_next(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  if (auto next = layer->getNext())
    push_docobj<Layer>(L, next);
  else
    lua_pushnil(L);
  return 1;
}

int Layer_get_name(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushstring(L, layer->name().c_str());
  return 1;
}

int Layer_get_opacity(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  if (layer->isImage() ||
      (layer->isGroup() && app::Preferences::instance().experimental.composeGroups())) {
    lua_pushinteger(L, static_cast<LayerImage*>(layer)->opacity());
    return 1;
  }
  else
    return 0;
}

int Layer_get_blendMode(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  if (layer->isImage() ||
      (layer->isGroup() && app::Preferences::instance().experimental.composeGroups())) {
    lua_pushinteger(
      L,
      int(base::convert_to<app::script::BlendMode>(static_cast<LayerImage*>(layer)->blendMode())));
    return 1;
  }
  else
    return 0;
}

int Layer_get_isImage(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isImage());
  return 1;
}

int Layer_get_isGroup(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isGroup());
  return 1;
}

int Layer_get_isTilemap(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isTilemap());
  return 1;
}

int Layer_get_isTransparent(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isTransparent());
  return 1;
}

int Layer_get_isBackground(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isBackground());
  return 1;
}

int Layer_get_isEditable(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isEditable());
  return 1;
}

int Layer_get_isVisible(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isVisible());
  return 1;
}

int Layer_get_isContinuous(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isContinuous());
  return 1;
}

int Layer_get_isCollapsed(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isCollapsed());
  return 1;
}

int Layer_get_isExpanded(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isExpanded());
  return 1;
}

int Layer_get_isReference(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  lua_pushboolean(L, layer->isReference());
  return 1;
}

int Layer_get_cels(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  push_cels(L, layer);
  return 1;
}

int Layer_get_tileset(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  if (layer->isTilemap())
    push_tileset(L, static_cast<doc::LayerTilemap*>(layer)->tileset());
  else
    lua_pushnil(L);
  return 1;
}

int Layer_get_uuid(lua_State* L)
{
  auto* layer = get_docobj<Layer>(L, 1);
  push_obj(L, layer->uuid());
  return 1;
}

int Layer_set_name(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const char* name = lua_tostring(L, 2);
  if (name) {
    Tx tx(layer->sprite());
    tx(new cmd::SetLayerName(layer, name));
    tx.commit();
  }
  return 0;
}

int Layer_set_opacity(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const int opacity = lua_tointeger(L, 2);
  if (layer->isImage() || layer->isGroup()) {
    Tx tx(layer->sprite());
    tx(new cmd::SetLayerOpacity(static_cast<LayerImage*>(layer), opacity));
    tx.commit();
  }
  return 0;
}

int Layer_set_blendMode(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  auto blendMode = app::script::BlendMode(lua_tointeger(L, 2));
  if (layer->isImage() || layer->isGroup()) {
    Tx tx(layer->sprite());
    tx(new cmd::SetLayerBlendMode(static_cast<LayerImage*>(layer),
                                  base::convert_to<doc::BlendMode>(blendMode)));
    tx.commit();
  }
  return 0;
}

int Layer_set_stackIndex(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const auto& layers = layer->parent()->layers();
  int newStackIndex = lua_tointeger(L, 2);
  int stackIndex = 1;
  auto parent = layer->parent();

  // First we need to know this layer stackIndex because we'll use
  auto it = std::find(layers.begin(), layers.end(), layer);
  ASSERT(it != layers.end());
  if (it != layers.end())
    stackIndex = it - layers.begin() + 1;

  Layer* beforeThis = nullptr;
  if (newStackIndex > stackIndex) {
    ++newStackIndex;
  }

  if (newStackIndex - 1 < int(parent->layers().size())) {
    beforeThis = parent->layers()[std::clamp(newStackIndex - 1, 0, (int)parent->layers().size())];
  }
  else {
    beforeThis = nullptr;
  }

  // Do nothing
  if (beforeThis == layer)
    return 0;

  Doc* doc = static_cast<Doc*>(layer->sprite()->document());
  Tx tx(doc);
  DocApi(doc, tx).restackLayerBefore(layer, parent, beforeThis);
  tx.commit();
  return 0;
}

int Layer_set_isEditable(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const bool newState = lua_toboolean(L, 2);

  if (!set_layer_flags_with_undo(layer, newState, LayerFlags::Editable))
    layer->setEditable(newState);

  return 0;
}

int Layer_set_isVisible(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const bool newState = lua_toboolean(L, 2);

  if (!set_layer_flags_with_undo(layer, newState, LayerFlags::Visible)) {
    Doc* doc = static_cast<Doc*>(layer->sprite()->document());
    doc->setLayerVisibilityWithNotifications(layer, newState);
  }

  return 0;
}

int Layer_set_isContinuous(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const bool newState = lua_toboolean(L, 2);

  if (!set_layer_flags_with_undo(layer, newState, LayerFlags::Continuous))
    layer->setContinuous(newState);

  return 0;
}

int Layer_set_isCollapsed(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const bool newState = lua_toboolean(L, 2);

  if (!set_layer_flags_with_undo(layer, newState, LayerFlags::Collapsed))
    layer->setCollapsed(newState);

  // TODO the timeline should be listening this kind of changes
  if (auto* timeline = App::instance()->timeline())
    timeline->refresh();

  return 0;
}

int Layer_set_isExpanded(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  const bool newState = !lua_toboolean(L, 2);

  if (!set_layer_flags_with_undo(layer, newState, LayerFlags::Collapsed))
    layer->setCollapsed(newState);

  // TODO the timeline should be listening this kind of changes
  if (auto* timeline = App::instance()->timeline())
    timeline->refresh();

  return 0;
}

int Layer_set_parent(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  LayerGroup* parent = nullptr;
  if (auto sprite = may_get_docobj<Sprite>(L, 2)) {
    parent = sprite->root();
  }
  else if (auto parentLayer = may_get_docobj<Layer>(L, 2)) {
    if (parentLayer->isGroup())
      parent = static_cast<LayerGroup*>(parentLayer);
    else
      return luaL_error(L, "the given parent is not a layer group or sprite");
  }

  if (!parent)
    return luaL_error(L, "parent cannot be nil");
  else if (parent == layer)
    return luaL_error(L, "the parent of a layer cannot be the layer itself");

  // TODO Why? should we be able to do this? It would require some hard work:
  // 1. convert color modes
  // 2. multiple transactions for both modified sprites?
  if (parent->sprite() != layer->sprite())
    return luaL_error(L, "you cannot move layers between sprites");

  if (parent) {
    Doc* doc = static_cast<Doc*>(layer->sprite()->document());
    Tx tx(doc);
    DocApi(doc, tx).restackLayerAfter(layer, parent, parent->lastLayer());
    tx.commit();
  }
  return 0;
}

int Layer_set_tileset(lua_State* L)
{
  auto layer = get_docobj<Layer>(L, 1);
  if (layer->isTilemap()) {
    auto tilemap = static_cast<doc::LayerTilemap*>(layer);
    doc::tileset_index tsi = tilemap->tilesetIndex();

    if (auto tileset = may_get_docobj<Tileset>(L, 2))
      tsi = layer->sprite()->tilesets()->getIndex(tileset);
    else if (lua_isinteger(L, 2))
      tsi = lua_tointeger(L, 2);

    if (tsi != tilemap->tilesetIndex()) {
      Tx tx(layer->sprite());
      tx(new cmd::SetLayerTileset(tilemap, tsi));
      tx.commit();
    }
  }
  return 0;
}

const luaL_Reg Layer_methods[] = {
  { "__eq",  Layer_eq  },
  { "cel",   Layer_cel },
  { nullptr, nullptr   }
};

const Property Layer_properties[] = {
  { "id",            Layer_get_id,                   nullptr                        },
  { "sprite",        Layer_get_sprite,               nullptr                        },
  { "parent",        Layer_get_parent,               Layer_set_parent               },
  { "layers",        Layer_get_layers,               nullptr                        },
  { "stackIndex",    Layer_get_stackIndex,           Layer_set_stackIndex           },
  { "previous",      Layer_get_previous,             nullptr                        },
  { "next",          Layer_get_next,                 nullptr                        },
  { "name",          Layer_get_name,                 Layer_set_name                 },
  { "opacity",       Layer_get_opacity,              Layer_set_opacity              },
  { "blendMode",     Layer_get_blendMode,            Layer_set_blendMode            },
  { "isImage",       Layer_get_isImage,              nullptr                        },
  { "isGroup",       Layer_get_isGroup,              nullptr                        },
  { "isTilemap",     Layer_get_isTilemap,            nullptr                        },
  { "isTransparent", Layer_get_isTransparent,        nullptr                        },
  { "isBackground",  Layer_get_isBackground,         nullptr                        },
  { "isEditable",    Layer_get_isEditable,           Layer_set_isEditable           },
  { "isVisible",     Layer_get_isVisible,            Layer_set_isVisible            },
  { "isContinuous",  Layer_get_isContinuous,         Layer_set_isContinuous         },
  { "isCollapsed",   Layer_get_isCollapsed,          Layer_set_isCollapsed          },
  { "isExpanded",    Layer_get_isExpanded,           Layer_set_isExpanded           },
  { "isReference",   Layer_get_isReference,          nullptr                        },
  { "cels",          Layer_get_cels,                 nullptr                        },
  { "color",         UserData_get_color<Layer>,      UserData_set_color<Layer>      },
  { "data",          UserData_get_text<Layer>,       UserData_set_text<Layer>       },
  { "properties",    UserData_get_properties<Layer>, UserData_set_properties<Layer> },
  { "tileset",       Layer_get_tileset,              Layer_set_tileset              },
  { "uuid",          Layer_get_uuid,                 nullptr                        },
  { nullptr,         nullptr,                        nullptr                        }
};

} // anonymous namespace

DEF_MTNAME(Layer);

void register_layer_class(lua_State* L)
{
  using Layer = doc::Layer;
  REG_CLASS(L, Layer);
  REG_CLASS_PROPERTIES(L, Layer);
}

}} // namespace app::script
