#include "lauxlib.h"

#include "log.h"
#include "lephisto.h"
#include "rng.h"
#include "land.h"
#include "lluadef.h"
#include "map.h"
#include "llua_space.h"

static int planetL_createmetatable(lua_State* L);
static int systemL_createmetatable(lua_State* L);
static int vectorL_createmetatable(lua_State* L);

/* Space. */
static int planetL_get(lua_State* L);
static int systemL_get(lua_State* L);
static const luaL_reg space_methods[] = {
  { "getPlanet",  planetL_get },
  { "getSystem",  systemL_get },
  { 0, 0 }
};

/* Planet metatable methods. */
static int planetL_eq(lua_State* L);
static int planetL_name(lua_State* L);
static int planetL_faction(lua_State* L);
static int planetL_class(lua_State* L);
static int planetL_services(lua_State* L);
static int planetL_position(lua_State* L);
static const luaL_reg planet_methods[] = {
  { "__eq",         planetL_eq        },
  { "name",         planetL_name      },
  { "faction",      planetL_faction   },
  { "class",        planetL_class     },
  { "services",     planetL_services  },
  { "pos",          planetL_position  },
  { 0, 0 } 
};

/* System metatable methods. */
static int systemL_eq(lua_State* L);
static int systemL_name(lua_State* L);
static int systemL_faction(lua_State* L);
static int systemL_jumpdistance(lua_State* L);
static const luaL_reg system_methods[] = {
  { "__eq",       systemL_eq            },
  { "name",       systemL_name          },
  { "faction",    systemL_faction       },
  { "jumpDist",   systemL_jumpdistance  },
  { 0, 0 }
};

/* Vector metatable methods. */
static int vectorL_new(lua_State* L);
static int vectorL_add(lua_State* L);
static int vectorL_sub(lua_State* L);
static int vectorL_mul(lua_State* L);
static int vectorL_div(lua_State* L);
static int vectorL_get(lua_State* L);
static int vectorL_set(lua_State* L);
static int vectorL_distance(lua_State* L);
static const luaL_reg vector_methods[] = {
  { "new",    vectorL_new },
  { "__add",  vectorL_add },
  { "__sub",  vectorL_sub },
  { "__mul",  vectorL_mul },
  { "__div",  vectorL_div },
  { "get",    vectorL_get },
  { "set",    vectorL_set },
  { "dist",   vectorL_distance },
  { 0, 0 }
}; /**< Vector metatable methods. */

/* Load the space library. */
int lua_loadSpace(lua_State* L, int readonly) {
  (void)readonly; /* Only read only atm. */

  /* Register the functions. */
  luaL_register(L, "space", space_methods);

  /* Register the metatables. */
  planetL_createmetatable(L);
  systemL_createmetatable(L);
  lua_loadVector(L);

  return 0;
}

/**
 * @fn int lua_loadVector(lua_State* L)
 *
 * @brief Loads the vector metatable.
 *    @param L State to load the vector metatable into.
 *    @return 0 on success.
 */
int lua_loadVector(lua_State* L) {
  vectorL_createmetatable(L);

  return 0;
}

/* -- SPACE -- */

/* Create a planet metatable from a planet and put it on top of the stack. */
static int planetL_createmetatable(lua_State* L) {
  /* Create the metatable. */
  luaL_newmetatable(L, PLANET_METATABLE);

  /* Create the access table. */
  lua_pushvalue(L, -1);
  lua_setfield(L, -2,"__index");

  /* Register the values. */
  luaL_register(L, NULL, planet_methods);

  return 0; /* No error. */
}

/* Register the system metatable. */
static int systemL_createmetatable(lua_State* L) {
  /* Create the metatable. */
  luaL_newmetatable(L, SYSTEM_METATABLE);

  /* Create the access table. */
  lua_pushvalue(L, -1);
  lua_setfield(L, -2, "__index");

  /* Register the values. */
  luaL_register(L, NULL, system_methods);

  return 0; /* No error. */
}

/**
 * @fn static int vectorL_createmetatable(lua_State* L)
 *
 * @brief Registers the vector metatable.
 *    @param L Lua state to register metatable in.
 *    @return 0 on success.
 */
static int vectorL_createmetatable(lua_State* L) {
  /* Create the metatable. */
  luaL_newmetatable(L, VECTOR_METATABLE);

  /* Create the access table. */
  lua_pushvalue(L, -1);
  lua_setfield(L, -2, "__index");

  /* Register the values. */
  luaL_register(L, NULL, vector_methods);

  return 0; /* No error. */
}

/* -- PLANET -- */

/* Get planet at index. */
LuaPlanet* lua_toplanet(lua_State* L, int ind) {
  if(lua_isuserdata(L, ind)) {
    return (LuaPlanet*) lua_touserdata(L, ind);
  }

  luaL_typerror(L, ind, PLANET_METATABLE);
  return NULL;
}

/* Push a planet on the stack. */
LuaPlanet* lua_pushplanet(lua_State* L, LuaPlanet planet) {
  LuaPlanet* p;
  p = (LuaPlanet*) lua_newuserdata(L, sizeof(LuaPlanet));
  *p = planet;
  luaL_getmetatable(L, PLANET_METATABLE);
  lua_setmetatable(L, -2);
  return p;
}

/* Check see if ind is a planet. */
int lua_isplanet(lua_State* L, int ind) {
  int ret;

  if(lua_getmetatable(L, ind)==0)
    return 0;
  lua_getfield(L, LUA_REGISTRYINDEX, PLANET_METATABLE);

  ret = 0;
  if(lua_rawequal(L, -1, -2)) /* Does it have the correct mt? */
    ret = 1;

  lua_pop(L, 2); /* Remove both metatables. */
  return ret;
}

/* get a planet. */
static int planetL_get(lua_State* L) {
  int       i;
  int*      factions;
  int       nfactions;
  char**    planets;
  int       nplanets;
  char*     rndplanet;
  LuaPlanet planet;
  LuaSystem sys;

  rndplanet = NULL;
  nplanets = 0;

  /* Get the landed planet. */
  if(lua_gettop(L) == 0) {
    if(land_planet != NULL) {
      planet.p = land_planet;
      lua_pushplanet(L, planet);
      sys.s = system_get(planet_getSystem(land_planet->name));
      lua_pushsystem(L, sys);
      return 2;
    }
    return 0; /* Not landed. */
  }

  /* Get a planet by faction */
  else if(lua_isnumber(L, 1)) {
    i = lua_tonumber(L, 1);
    planets = space_getFactionPlanet(&nplanets, &i, 1);
  }

  /* Get a planet by name. */
  else if(lua_isstring(L, 1)) {
    rndplanet = (char*) lua_tostring(L, 1);
  }

  /* Get a planet from faction list. */
  else if(lua_istable(L, 1)) {
    /* Load up the table. */
    lua_pushnil(L);
    nfactions = (int)lua_gettop(L);
    factions = malloc(sizeof(int) * nfactions);
    i = 0;
    
    while(lua_next(L, -2) != 0) {
      factions[i++] = (int)lua_tonumber(L, -1);
      lua_pop(L,1);
    }

    /* Get the planets. */
    planets = space_getFactionPlanet(&nplanets, factions, nfactions);
    free(factions);
  }
  else LLUA_INVALID_PARAMETER(); /* Bad parameter. */

  /* No suitable planet found. */
  if((rndplanet == NULL) && (nplanets == 0)) {
    free(planets);
    return 0;
  }

  /* Pick random planet. */
  else if(rndplanet == NULL) {
    rndplanet = planets[RNG(0, nplanets-1)];
    free(planets);
  }

  /* Push the planet. */
  planet.p = planet_get(rndplanet); /* The real planet. */
  lua_pushplanet(L, planet);
  sys.s = system_get(planet_getSystem(rndplanet));
  lua_pushsystem(L, sys);
  return 2;
}

/* __eq (equality) metamethod for planets. */
static int planetL_eq(lua_State* L) {
  LuaPlanet* a, *b;
  a = lua_toplanet(L, 1);
  b = lua_toplanet(L, 2);
  if(a->p == b->p)
    lua_pushboolean(L, 1);
  else
    lua_pushboolean(L, 0);
  return 1;
}

/* Get the planets name. */
static int planetL_name(lua_State* L) {
  LuaPlanet* p;
  p = lua_toplanet(L, 1);
  lua_pushstring(L, p->p->name);
  return 1;
}

/* Get the planets faction. */
static int planetL_faction(lua_State* L) {
  LuaPlanet* p;
  p = lua_toplanet(L, 1);
  lua_pushstring(L, faction_name(p->p->faction));
  return 1;
}

/* Get the planets class. */
static int planetL_class(lua_State* L) {
  char buf[2];
  LuaPlanet* p;
  p = lua_toplanet(L, 1);
  buf[0] = planet_getClass(p->p);
  buf[1] = '\0';
  lua_pushstring(L, buf);
  return 1;
}

/* Get planet services. */
static int planetL_services(lua_State* L) {
  LuaPlanet* p;
  p = lua_toplanet(L, 1);
  lua_pushnumber(L, p->p->services);
  return 1;
}

/**
 * @fn static int planetL_position(lua_State* L)
 * @ingroup META_PLANET
 *
 * @brief Vec2 pos(nil)
 *
 * Get the position of the planet in the system.
 *    @return The position of the planet in the system.
 */
static int planetL_position(lua_State* L) {
  LuaPlanet* p;
  LuaVector v;
  p = lua_toplanet(L, 1);
  vectcpy(&v.vec, &p->p->pos);
  lua_pushvector(L, v);
  return 1;
}

/* -- SYSTEM -- */

/* Get system at index. */
LuaSystem* lua_tosystem(lua_State* L, int ind) {
  if(lua_isuserdata(L, ind)) {
    return (LuaSystem*) lua_touserdata(L,ind);
  }
  luaL_typerror(L, ind, SYSTEM_METATABLE);
  return NULL;
}

/* Pushes a system on the stack. */
LuaSystem* lua_pushsystem(lua_State* L, LuaSystem sys) {
  LuaSystem* s;
  s = (LuaSystem*) lua_newuserdata(L, sizeof(LuaSystem));
  *s = sys;
  luaL_getmetatable(L, SYSTEM_METATABLE);
  lua_setmetatable(L, -2);
  return s;
}

/* Check to see if ind is a planet. */
int lua_issystem(lua_State* L, int ind) {
  int ret;

  if(lua_getmetatable(L, ind)==0)
    return 0;
  lua_getfield(L, LUA_REGISTRYINDEX, SYSTEM_METATABLE);

  ret = 0;
  if(lua_rawequal(L, -1, -2)) /* Does it have the correct mt? */
    ret = 1;

  lua_pop(L, 2); /* Remove both metatables. */
  return ret;
}

/* Get a system. */
static int systemL_get(lua_State* L) {
  LuaSystem sys;
  LuaPlanet* p;

  /* Get the current system with no parameters. */
  if(lua_gettop(L) == 0) {
    sys.s = cur_system;
  }
  /* Passing a string (systemname) */
  else if(lua_isstring(L, 1)) {
    sys.s = system_get((char*)lua_tostring(L, 1));
  }
  /* Passing a planet */
  else if(lua_isplanet(L, 1)) {
    p = lua_toplanet(L, 1);
    sys.s = system_get(planet_getSystem(p->p->name));
  }
  else LLUA_INVALID_PARAMETER();

  /* Return the system. */
  lua_pushsystem(L, sys);
  return 1;
}

/* Check system for equality. */
static int systemL_eq(lua_State* L) {
  LuaSystem* a, *b;
  a = lua_tosystem(L, 1);
  b = lua_tosystem(L, 2);
  if(a->s == b->s)
    lua_pushboolean(L, 1);
  else
    lua_pushboolean(L, 0);

  return 1;
}

/* Return the systems name. */
static int systemL_name(lua_State* L) {
  LuaSystem* sys;
  sys = lua_tosystem(L, 1);
  lua_pushstring(L, sys->s->name);
  return 1;
}

/* Get system factions. */
static int systemL_faction(lua_State* L) {
  int i;
  LuaSystem* sys;
  sys = lua_tosystem(L, 1);

  /* Return result in table. */
  lua_newtable(L);
  for(i = 0; i < sys->s->nplanets; i++) {
    if(sys->s->planets[i].faction > 0) { /* Faction must be valid. */
      lua_pushboolean(L, 1); /* Value. */
      lua_setfield(L, -2, faction_name(sys->s->planets[i].faction)); /* Key. */
      /* Allows syntax foo = space.faction("foo"); if foo["bar"] then ... end */
    }
  }

  return 1;
}

/* Get jump distance for current system, or to another. */
static int systemL_jumpdistance(lua_State* L) {
  LLUA_MIN_ARGS(1);
  LuaSystem* sys;
  StarSystem** s;
  int jumps;
  char* start, *goal;

  sys = lua_tosystem(L, 1);
  start = sys->s->name;

  if((lua_gettop(L) > 1) && lua_isstring(L, 2))
    goal = (char*)lua_tostring(L, 2);
  else
    goal = cur_system->name;

  s = map_getJumpPath(&jumps, start, goal, 1);
  free(s);

  lua_pushnumber(L, jumps);
  return 1;
}

/**
 * @defgroup META_VECTOR Vector Metatable
 *
 * @brief Represents a 2d vector in Lua.
 *
 * To call members of the metatable always use:
 * @code
 * vector:function(param)
 * @endcode
 */

/**
 * @fn LuaVector* lua_tovector(lua_State* L, int ind)
 * 
 * @brief Gets vector at index.
 *    @param L Lua state to get vector from.
 *    @param ind Index position of vector.
 *    @return The LuaVector at ind.
 */
LuaVector* lua_tovector(lua_State* L, int ind) {
  if(lua_isuserdata(L, ind)) {
    return (LuaVector*)lua_touserdata(L, ind);
  }

  luaL_typerror(L, ind, VECTOR_METATABLE);
  return NULL;
}

/**
 * @fn LuaVector* lua_pushvector(lua_State* L, LuaVector vec)
 *
 * @brief Pushes a vector on the stack.
 *    @param L Lua state to push vector onto.
 *    @param sys Vector to push.
 *    @return Vector just pushed.
 */
LuaVector* lua_pushvector(lua_State* L, LuaVector vec) {
  LuaVector* v;
  v = (LuaVector*) lua_newuserdata(L, sizeof(LuaVector));
  *v = vec;
  luaL_getmetatable(L, VECTOR_METATABLE);
  lua_setmetatable(L, -2);
  return v;
}

/**
 * @fn int lua_isvector(lua_State* L, int ind)
 *
 * @brief Check to see if ind is a vector.
 *    @param L lua state to check.
 *    @param ind Index position to check.
 *    @return 1 if there is a vector at index position.
 */
int lua_isvector(lua_State* L, int ind) {
  int ret;

  if(lua_getmetatable(L, ind)==0)
    return 0;
  lua_getfield(L, LUA_REGISTRYINDEX, VECTOR_METATABLE);
  
  ret = 0;
  if(lua_rawequal(L, -1, -2)) /* Does it have the correct mt? */
    ret = 1;

  lua_pop(L, 2); /* Remove both metatables. */
  return ret;
}

/**
 * @fn static int vectorL_new(lua_State* L)
 * @ingroup META_VECTOR
 *
 * @brief 
 */
static int vectorL_new(lua_State* L) {
  LuaVector v;
  double x, y;

  if((lua_gettop(L) > 1) && lua_isnumber(L,1) && lua_isnumber(L,2)) {
    x = lua_tonumber(L,1);
    y = lua_tonumber(L,2);
  } else {
    x = 0.;
    y = 0.;
  }

  vect_cset(&v.vec, x, y);
  lua_pushvector(L, v);
  return 1;
}

/**
 * @fn static int vectorL_add(lua_State* L)
 * @ingroup META_VECTOR
 *
 * @brief __add(Vec2 vector)
 *
 * __add(number x, number y)
 *
 * Adds two vectors or a vector and some cartesian coords.
 */
static int vectorL_add(lua_State* L) {
  LLUA_MIN_ARGS(2);
  LuaVector* v1, *v2;
  double x, y;

  /* Get self. */
  v1 = lua_tovector(L, 1);

  /* Get rest of parameters. */
  v2 = NULL;
  if(lua_isvector(L, 2)) {
    v2 = lua_tovector(L, 2);
    x = v2->vec.x;
    y = v2->vec.y;
  }
  else LLUA_INVALID_PARAMETER();

  /* Actually add it. */
  vect_cadd(&v1->vec, x, y);
  return 0;
}

/**
 * @fn static int vectorL_sub(lua_State* L)
 * @ingroup META_VECTOR
 *
 * @brief __sub(Vec2 vector)
 *
 * __sub(number x, number y)
 *
 * Subtracts two vectors or a vector and some cartesian coords.
 */
static int vectorL_sub(lua_State* L) {
  LLUA_MIN_ARGS(2);
  LuaVector* v1, *v2;
  double x, y;

  /* Get self. */
  v1 = lua_tovector(L,1);

  /* Get rest of parameters. */
  v2 = NULL;
  if(lua_isvector(L, 2)) {
    v2 = lua_tovector(L, 2);
    x = v2->vec.x;
    y = v2->vec.y;
  }
  else if((lua_gettop(L) > 2) && lua_isnumber(L, 2) && lua_isnumber(L, 3)) {
    x = lua_tonumber(L, 2);
    y = lua_tonumber(L, 3);
  }
  else LLUA_INVALID_PARAMETER();

  /* Actually add it. */
  vect_cadd(&v1->vec, -x, -y);
  return 0;
}

/**
 * @fn static int vectorL_mul(lua_State* L)
 * @ingroup META_VECTOR
 *
 * @brief __mul(number mod)
 *
 * Multiplies by a number.
 *    @param mod Amount to multiply by.
 */
static int vectorL_mul(lua_State* L) {
  LLUA_MIN_ARGS(2);
  LuaVector* v1;
  double mod;
  
  /* Get self. */
  v1 = lua_tovector(L,1);

  /* Get rest of parameters. */
  if(lua_isnumber(L, 2))
    mod = lua_tonumber(L, 2);
  else LLUA_INVALID_PARAMETER();

  /* Actually add it. */
  vect_cadd(&v1->vec, v1->vec.x * mod, v1->vec.x * mod);
  return 0;
}

/**
 * @fn static int vectorL_div(lua_State* L)
 * @ingroup META_VECTOR
 *
 * @brief __div(number mod)
 *
 * Divides a vector by a number.
 *    @param mod Amount to divide by.
 */
static int vectorL_div(lua_State* L) {
  LLUA_MIN_ARGS(2);
  LuaVector* v1;
  double mod;

  /* Get self. */
  v1 = lua_tovector(L,1);

  /* Get rest of parameters. */
  if(lua_isnumber(L, 2))
    mod = lua_tonumber(L, 2);
  else LLUA_INVALID_PARAMETER();

  /* Actually add it. */
  vect_cadd(&v1->vec, v1->vec.x / mod, v1->vec.x / mod);
  return 0;
}

/**
 * @fn static int vectorL_get(lua_State* L)
 * @ingroup META_VECTOR
 *
 * @brief number, number get(nil)
 *
 * Get the catesian positions of the vector.
 *    @return X and Y position of the vector.
 */
static int vectorL_get(lua_State* L) {
  LLUA_MIN_ARGS(1);
  LuaVector* v1;

  /* Get self. */
  v1 = lua_tovector(L,1);

  /* Push the vector. */
  lua_pushnumber(L, v1->vec.x);
  lua_pushnumber(L, v1->vec.y);
  return 2;
}

/**
 * @fn static int vectorL_set(lua_State* L)
 *
 * @brief set(number x, number y)
 *
 * Set the vector by cartesian coordinates.
 *    @param x X coordinate to set.
 *    @param y Y coordinate to set.
 */
static int vectorL_set(lua_State* L) {
  LLUA_MIN_ARGS(3);
  LuaVector* v1;
  double x, y;

  /* Get self */
  v1 = lua_tovector(L, 1);

  /* Get parameters. */
  if(lua_isnumber(L,2))
    x = lua_tonumber(L,2);
  else LLUA_INVALID_PARAMETER();
  if(lua_isnumber(L, 3))
    y = lua_tonumber(L, 3);
  else LLUA_INVALID_PARAMETER();

  vect_cset(&v1->vec, x, y);
  return 0;
}

/**
 * @fn static int vectorL_distance(lua_State* L)
 * @ingroup META_VECTOR
 *
 * @brief number dist( [Vec2 vector] )
 *
 * Get the distance from the Vec2.
 *    @param vector Vector to get distance from, uses origin(0,0) if not set.
 *    @return The distance calculated.
 */
static int vectorL_distance(lua_State* L) {
  LLUA_MIN_ARGS(1);
  LuaVector* v1, *v2;
  double dist;

  /* Get self. */
  v1 = lua_tovector(L, 1);

  /* Get rest of parameters. */
  v2 = NULL;
  if((lua_gettop(L) > 1) && lua_isvector(L,2))
    v2 = lua_tovector(L,2);
  else LLUA_INVALID_PARAMETER();

  /* Get distance. */
  if(v2 == NULL)
    dist = vect_odist(&v1->vec);
  else
    dist = vect_dist(&v1->vec, &v2->vec);

  /* Return the distance. */
  lua_pushnumber(L, dist);
  return 1;
}

