//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//

#include <lua.h>
#include <lauxlib.h>
#include <stdlib.h>
#include <math.h>
#include <inttypes.h>
#include <string.h>

extern const char * LONG_NUM_TYPE;
extern int64_t lualongnumber_checklong(lua_State *L, int index);
extern int64_t lualongnumber_pushlong(lua_State *L, int64_t *val);

////////////////////////////////////////////////////////////////////////////////

static void l_serialize(char *buf, int len, int64_t val) {
  snprintf(buf, len, "%"PRId64, val);
}

static int64_t l_deserialize(const char *buf) {
  int64_t data;
  int rv;
  // Support hex prefixed with '0x'
  if (strstr(buf, "0x") == buf) {
    rv = sscanf(buf, "%"PRIx64, &data);
  } else {
    rv = sscanf(buf, "%"PRId64, &data);
  }
  if (rv == 1) {
    return data;
  }
  return 0; // Failed
}

////////////////////////////////////////////////////////////////////////////////

static int l_new(lua_State *L) {
  int64_t val;
  const char *str = NULL;
  if (lua_type(L, 1) == LUA_TSTRING) {
    str = lua_tostring(L, 1);
    val = l_deserialize(str);
  } else if (lua_type(L, 1) == LUA_TNUMBER) {
    val = (int64_t)lua_tonumber(L, 1);
    str = (const char *)1;
  }
  lualongnumber_pushlong(L, (str ? &val : NULL));
  return 1;
}

////////////////////////////////////////////////////////////////////////////////

// a + b
static int l_add(lua_State *L) {
  int64_t a, b, c;
  a = lualongnumber_checklong(L, 1);
  b = lualongnumber_checklong(L, 2);
  c = a + b;
  lualongnumber_pushlong(L, &c);
  return 1;
}

// a / b
static int l_div(lua_State *L) {
  int64_t a, b, c;
  a = lualongnumber_checklong(L, 1);
  b = lualongnumber_checklong(L, 2);
  c = a / b;
  lualongnumber_pushlong(L, &c);
  return 1;
}

// a == b (both a and b are lualongnumber's)
static int l_eq(lua_State *L) {
  int64_t a, b;
  a = lualongnumber_checklong(L, 1);
  b = lualongnumber_checklong(L, 2);
  lua_pushboolean(L, (a == b ? 1 : 0));
  return 1;
}

// garbage collection
static int l_gc(lua_State *L) {
  lua_pushnil(L);
  lua_setmetatable(L, 1);
  return 0;
}

// a < b
static int l_lt(lua_State *L) {
  int64_t a, b;
  a = lualongnumber_checklong(L, 1);
  b = lualongnumber_checklong(L, 2);
  lua_pushboolean(L, (a < b ? 1 : 0));
  return 1;
}

// a <= b
static int l_le(lua_State *L) {
  int64_t a, b;
  a = lualongnumber_checklong(L, 1);
  b = lualongnumber_checklong(L, 2);
  lua_pushboolean(L, (a <= b ? 1 : 0));
  return 1;
}

// a % b
static int l_mod(lua_State *L) {
  int64_t a, b, c;
  a = lualongnumber_checklong(L, 1);
  b = lualongnumber_checklong(L, 2);
  c = a % b;
  lualongnumber_pushlong(L, &c);
  return 1;
}

// a * b
static int l_mul(lua_State *L) {
  int64_t a, b, c;
  a = lualongnumber_checklong(L, 1);
  b = lualongnumber_checklong(L, 2);
  c = a * b;
  lualongnumber_pushlong(L, &c);
  return 1;
}

// a ^ b
static int l_pow(lua_State *L) {
  long double a, b;
  int64_t c;
  a = (long double)lualongnumber_checklong(L, 1);
  b = (long double)lualongnumber_checklong(L, 2);
  c = (int64_t)pow(a, b);
  lualongnumber_pushlong(L, &c);
  return 1;
}

// a - b
static int l_sub(lua_State *L) {
  int64_t a, b, c;
  a = lualongnumber_checklong(L, 1);
  b = lualongnumber_checklong(L, 2);
  c = a - b;
  lualongnumber_pushlong(L, &c);
  return 1;
}

// tostring()
static int l_tostring(lua_State *L) {
  int64_t a;
  char str[256];
  l_serialize(str, 256, lualongnumber_checklong(L, 1));
  lua_pushstring(L, str);
  return 1;
}

// -a
static int l_unm(lua_State *L) {
  int64_t a, c;
  a = lualongnumber_checklong(L, 1);
  c = -a;
  lualongnumber_pushlong(L, &c);
  return 1;
}

////////////////////////////////////////////////////////////////////////////////

static const luaL_Reg methods[] = {
  {"__add", l_add},
  {"__div", l_div},
  {"__eq", l_eq},
  {"__gc", l_gc},
  {"__lt", l_lt},
  {"__le", l_le},
  {"__mod", l_mod},
  {"__mul", l_mul},
  {"__pow", l_pow},
  {"__sub", l_sub},
  {"__tostring", l_tostring},
  {"__unm", l_unm},
  {NULL, NULL},
};

static const luaL_Reg funcs[] = {
  {"new", l_new},
  {NULL, NULL}
};

////////////////////////////////////////////////////////////////////////////////

static void set_methods(lua_State *L,
  const char *metatablename,
  const struct luaL_Reg *methods) {
  luaL_getmetatable(L, metatablename);   // mt
  // No need for a __index table since everything is __*
  for (; methods->name; methods++) {
    lua_pushstring(L, methods->name);    // mt, "name"
    lua_pushcfunction(L, methods->func); // mt, "name", func
    lua_rawset(L, -3);                   // mt
  }
  lua_pop(L, 1);
}

LUALIB_API int luaopen_liblualongnumber(lua_State *L) {
  luaL_newmetatable(L, LONG_NUM_TYPE);
  lua_pop(L, 1);
  set_methods(L, LONG_NUM_TYPE, methods);

#if LUA_VERSION_NUM >= 502
    lua_newtable(L);
    luaL_setfuncs(L, funcs, 0);
#else
  luaL_register(L, "liblualongnumber", funcs);
#endif
  return 1;
}
