// Aseprite
// Copyright (C) 2019  Igara Studio S.A.
// Copyright (C) 2017-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/script/luacpp.h"
#include "fmt/format.h"
#include "gfx/point.h"

#include <cmath>

namespace app {
namespace script {

namespace {

gfx::Point Point_new(lua_State* L, int index)
{
  gfx::Point pt(0, 0);
  // Copy other rectangle
  if (auto pt2 = may_get_obj<gfx::Point>(L, index)) {
    pt = *pt2;
  }
  // Convert {x=int,y=int} or {int,int} into a Point
  else if (lua_istable(L, index)) {
    const int type = lua_getfield(L, index, "x");
    if (VALID_LUATYPE(type)) {
      lua_getfield(L, index, "y");
      pt.x = lua_tointeger(L, -2);
      pt.y = lua_tointeger(L, -1);
      lua_pop(L, 2);
    }
    else {
      lua_pop(L, 1);

      // TODO Investigate this further: why we cannot use two
      // lua_geti() calls and then lua_pop(L, 2) when we are iterating
      // points in a table defined like {{0,0},{32,32}}

      lua_geti(L, index, 1);
      pt.x = lua_tointeger(L, -1);
      lua_pop(L, 1);

      lua_geti(L, index, 2);
      pt.y = lua_tointeger(L, -1);
      lua_pop(L, 1);
    }
  }
  else {
    pt.x = lua_tointeger(L, index);
    pt.y = lua_tointeger(L, index+1);
  }
  return pt;
}

int Point_new(lua_State* L)
{
  push_obj(L, Point_new(L, 1));
  return 1;
}

int Point_gc(lua_State* L)
{
  get_obj<gfx::Point>(L, 1)->~PointT();
  return 0;
}

int Point_eq(lua_State* L)
{
  const auto a = get_obj<gfx::Point>(L, 1);
  const auto b = get_obj<gfx::Point>(L, 2);
  lua_pushboolean(L, *a == *b);
  return 1;
}

int Point_tostring(lua_State* L)
{
  const auto pt = get_obj<gfx::Point>(L, 1);
  lua_pushstring(L, fmt::format("Point{{ x={}, y={} }}",
                                pt->x, pt->y).c_str());
  return 1;
}

int Point_unm(lua_State* L)
{
  const auto pt = get_obj<gfx::Point>(L, 1);
  push_obj(L, -(*pt));
  return 1;
}

int Point_add(lua_State* L)
{
  gfx::Point result(0, 0);
  if (lua_isuserdata(L, 1))
    result += *get_obj<gfx::Point>(L, 1);
  else
    result += lua_tointeger(L, 1);
  if (lua_isuserdata(L, 2))
    result += *get_obj<gfx::Point>(L, 2);
  else
    result += lua_tointeger(L, 2);
  push_obj(L, result);
  return 1;
}

int Point_sub(lua_State* L)
{
  gfx::Point result = *get_obj<gfx::Point>(L, 1);
  if (lua_isuserdata(L, 2))
    result -= *get_obj<gfx::Point>(L, 2);
  else
    result -= lua_tointeger(L, 2);
  push_obj(L, result);
  return 1;
}

int Point_mul(lua_State* L)
{
  gfx::Point result = *get_obj<gfx::Point>(L, 1);
  result *= lua_tointeger(L, 2);
  push_obj(L, result);
  return 1;
}

int Point_div(lua_State* L)
{
  gfx::Point result = *get_obj<gfx::Point>(L, 1);
  const int value = lua_tointeger(L, 2);
  if (value == 0)
    return luaL_error(L, "attempt to divide by zero");
  result /= value;
  push_obj(L, result);
  return 1;
}

int Point_mod(lua_State* L)
{
  gfx::Point result = *get_obj<gfx::Point>(L, 1);
  const int value = lua_tointeger(L, 2);
  if (value == 0)
    return luaL_error(L, "attempt to divide by zero");
  result.x %= value;
  result.y %= value;
  push_obj(L, result);
  return 1;
}

int Point_pow(lua_State* L)
{
  gfx::Point result = *get_obj<gfx::Point>(L, 1);
  const int value = lua_tointeger(L, 2);
  result.x = std::pow(result.x, value);
  result.y = std::pow(result.y, value);
  push_obj(L, result);
  return 1;
}

int Point_get_x(lua_State* L)
{
  const auto pt = get_obj<gfx::Point>(L, 1);
  lua_pushinteger(L, pt->x);
  return 1;
}

int Point_get_y(lua_State* L)
{
  const auto pt = get_obj<gfx::Point>(L, 1);
  lua_pushinteger(L, pt->y);
  return 1;
}

int Point_set_x(lua_State* L)
{
  auto pt = get_obj<gfx::Point>(L, 1);
  pt->x = lua_tointeger(L, 2);
  return 0;
}

int Point_set_y(lua_State* L)
{
  auto pt = get_obj<gfx::Point>(L, 1);
  pt->y = lua_tointeger(L, 2);
  return 0;
}

const luaL_Reg Point_methods[] = {
  { "__gc", Point_gc },
  { "__eq", Point_eq },
  { "__tostring", Point_tostring },
  { "__unm", Point_unm },
  { "__add", Point_add },
  { "__sub", Point_sub },
  { "__mul", Point_mul },
  { "__div", Point_div },
  { "__mod", Point_mod },
  { "__pow", Point_pow },
  { "__idiv", Point_div },
  { nullptr, nullptr }
};

const Property Point_properties[] = {
  { "x", Point_get_x, Point_set_x },
  { "y", Point_get_y, Point_set_y },
  { nullptr, nullptr, nullptr }
};

} // anonymous namespace

DEF_MTNAME(gfx::Point);

void register_point_class(lua_State* L)
{
  using gfx::Point;
  REG_CLASS(L, Point);
  REG_CLASS_NEW(L, Point);
  REG_CLASS_PROPERTIES(L, Point);
}

gfx::Point convert_args_into_point(lua_State* L, int index)
{
  return Point_new(L, index);
}

} // namespace script
} // namespace app
