// celx_frame.cpp
//
// Copyright (C) 2003-2008, the Celestia Development Team
//
// Lua script extensions for Celestia: frame object
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

#include "celx.h"
#include "celx_internal.h"
#include "celx_frame.h"
#include <celestia/celestiacore.h>
#include <celengine/observer.h>
#include <Eigen/Geometry>

using namespace std;
using namespace Eigen;


int frame_new(lua_State* l, const ObserverFrame& f)
{
    CelxLua celx(l);

    // Use placement new to put the new frame in the userdata block.
    void* block = lua_newuserdata(l, sizeof(ObserverFrame));
    new (block) ObserverFrame(f);

    celx.setClass(Celx_Frame);

    return 1;
}


ObserverFrame* to_frame(lua_State* l, int index)
{
    CelxLua celx(l);

    return static_cast<ObserverFrame*>(celx.checkUserData(index, Celx_Frame));
}


static ObserverFrame* this_frame(lua_State* l)
{
    CelxLua celx(l);

    ObserverFrame* f = to_frame(l, 1);
    if (f == nullptr)
    {
        celx.doError("Bad frame object!");
    }

    return f;
}


// Convert from frame coordinates to universal.
static int frame_from(lua_State* l)
{
    CelxLua celx(l);

    celx.checkArgs(2, 3, "Two or three arguments required for frame:from");

    ObserverFrame* frame = this_frame(l);
    CelestiaCore* appCore = celx.appCore(AllErrors);

    UniversalCoord* uc = nullptr;
    Quaterniond* q = nullptr;
    double jd = 0.0;

    if (celx.isType(2, Celx_Position))
    {
        uc = celx.toPosition(2);
    }
    else if (celx.isType(2, Celx_Rotation))
    {
        q = celx.toRotation(2);
    }
    if (uc == nullptr && q == nullptr)
    {
        celx.doError("Position or rotation expected as second argument to frame:from()");
        return 0;
    }

    jd = celx.safeGetNumber(3, WrongType, "Second arg to frame:from must be a number", appCore->getSimulation()->getTime());

    if (uc != nullptr)
    {
        UniversalCoord uc1 = frame->convertToUniversal(*uc, jd);
        celx.newPosition(uc1);
    }
    else
    {
        Quaterniond q1 = frame->convertToUniversal(*q, jd);
        celx.newRotation(q1);
    }

    return 1;
}

// Convert from universal to frame coordinates.
static int frame_to(lua_State* l)
{
    CelxLua celx(l);

    celx.checkArgs(2, 3, "Two or three arguments required for frame:to");

    ObserverFrame* frame = this_frame(l);
    CelestiaCore* appCore = celx.appCore(AllErrors);

    UniversalCoord* uc = nullptr;
    Quaterniond* q = nullptr;
    double jd = 0.0;

    if (celx.isType(2, Celx_Position))
    {
        uc = celx.toPosition(2);
    }
    else if (celx.isType(2, Celx_Rotation))
    {
        q = celx.toRotation(2);
    }

    if (uc == nullptr && q == nullptr)
    {
        celx.doError("Position or rotation expected as second argument to frame:to()");
        return 0;
    }

    jd = celx.safeGetNumber(3, WrongType, "Second arg to frame:to must be a number", appCore->getSimulation()->getTime());

    if (uc != nullptr)
    {
        UniversalCoord uc1 = frame->convertFromUniversal(*uc, jd);
        celx.newPosition(uc1);
    }
    else
    {
        Quaterniond q1 = frame->convertFromUniversal(*q, jd);
        celx.newRotation(q1);
    }

    return 1;
}

static int frame_getrefobject(lua_State* l)
{
    CelxLua celx(l);

    celx.checkArgs(1, 1, "No arguments expected for frame:getrefobject()");

    ObserverFrame* frame = this_frame(l);
    if (frame->getRefObject().getType() == SelectionType::None)
    {
        celx.push(CelxValue());
    }
    else
    {
        celx.newObject(frame->getRefObject());
    }

    return 1;
}

static int frame_gettargetobject(lua_State* l)
{
    CelxLua celx(l);

    celx.checkArgs(1, 1, "No arguments expected for frame:gettarget()");

    ObserverFrame* frame = this_frame(l);
    if (frame->getTargetObject().getType() == SelectionType::None)
    {
        lua_pushnil(l);
    }
    else
    {
        celx.newObject(frame->getTargetObject());
    }
    return 1;
}

static int frame_getcoordinatesystem(lua_State* l)
{
    CelxLua celx(l);

    celx.checkArgs(1, 1, "No arguments expected for frame:getcoordinatesystem()");

    ObserverFrame* frame = this_frame(l);
    string coordsys;
    switch (frame->getCoordinateSystem())
    {
        case ObserverFrame::CoordinateSystem::Universal:
            coordsys = "universal"; break;
        case ObserverFrame::CoordinateSystem::Ecliptical:
            coordsys = "ecliptic"; break;
        case ObserverFrame::CoordinateSystem::Equatorial:
            coordsys = "equatorial"; break;
        case ObserverFrame::CoordinateSystem::BodyFixed:
            coordsys = "bodyfixed"; break;
        case ObserverFrame::CoordinateSystem::ObserverLocal:
            coordsys = "observer"; break;
        case ObserverFrame::CoordinateSystem::PhaseLock:
            coordsys = "lock"; break;
        case ObserverFrame::CoordinateSystem::Chase:
            coordsys = "chase"; break;
        default:
            coordsys = "invalid";
    }

    celx.push(coordsys.c_str());

    return 1;
}

static int frame_tostring(lua_State* l)
{
    CelxLua celx(l);

    // TODO: print out the actual information about the frame
    celx.push("[Frame]");

    return 1;
}


/*! Garbage collection metamethod frame objects.
*/
static int frame_gc(lua_State* l)
{
    ObserverFrame* frame = this_frame(l);

    // Explicitly call the destructor since the object was created with placement new
    frame->~ObserverFrame();

    return 0;
}


void CreateFrameMetaTable(lua_State* l)
{
    CelxLua celx(l);

    celx.createClassMetatable(Celx_Frame);

    celx.registerMethod("__tostring", frame_tostring);
    celx.registerMethod("__gc", frame_gc);
    celx.registerMethod("to", frame_to);
    celx.registerMethod("from", frame_from);
    celx.registerMethod("getcoordinatesystem", frame_getcoordinatesystem);
    celx.registerMethod("getrefobject", frame_getrefobject);
    celx.registerMethod("gettargetobject", frame_gettargetobject);

    lua_pop(l, 1); // remove metatable from stack
}
