// celx_observer.cpp
//
// Copyright (C) 2003-2008, the Celestia Development Team
//
// Lua script extensions for Celestia: observer 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 <memory>

#include <Eigen/Geometry>

#include <celestia/celestiacore.h>
#include <celscript/common/scriptmaps.h>
#include <celutil/logger.h>
#include <celutil/stringutils.h>
#include "celx.h"
#include "celx_internal.h"
#include "celx_observer.h"

using namespace std;
using namespace Eigen;
using celestia::util::GetLogger;

namespace astro = celestia::astro;

// Defined in celx.cpp.
ObserverFrame::CoordinateSystem parseCoordSys(std::string_view);

// ==================== Observer ====================

int observer_new(lua_State* l, Observer* o)
{
    CelxLua celx(l);

    Observer** ud = static_cast<Observer**>(lua_newuserdata(l, sizeof(Observer*)));
    *ud = o;

    celx.setClass(Celx_Observer);

    return 1;
}

Observer* to_observer(lua_State* l, int index)
{
    CelxLua celx(l);

    auto o = static_cast<Observer* const*>(lua_touserdata(l, index));
    const CelestiaCore* appCore = celx.appCore(AllErrors);

    // Check if pointer is still valid, i.e. is used by a view:
    if (o != nullptr && getViewByObserver(appCore, *o) != nullptr)
    {
        return *o;
    }
    return nullptr;
}

static Observer* this_observer(lua_State* l)
{
    CelxLua celx(l);

    Observer* obs = to_observer(l, 1);
    if (obs == nullptr)
    {
        celx.doError("Bad observer object (maybe tried to access a deleted view?)!");
    }

    return obs;
}


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

    celx.checkArgs(1, 1, "No arguments expected for observer:isvalid()");
    lua_pushboolean(l, to_observer(l, 1) != nullptr);
    return 1;
}

static int observer_tostring(lua_State* l)
{
    lua_pushstring(l, "[Observer]");

    return 1;
}

static int observer_setposition(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument required for setpos");

    Observer* o = this_observer(l);

    UniversalCoord* uc = celx.toPosition(2);
    if (uc == nullptr)
    {
        celx.doError("Argument to observer:setposition must be a position");
        return 0;
    }
    o->setPosition(*uc);
    return 0;
}

static int observer_setorientation(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument required for setorientation");

    Observer* o = this_observer(l);

    auto q = celx.toRotation(2);
    if (q == nullptr)
    {
        celx.doError("Argument to observer:setorientation must be a rotation");
        return 0;
    }
    o->setOrientation(*q);
    return 0;
}

static int observer_getorientation(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No arguments expected to observer:getorientation()");

    Observer* o = this_observer(l);
    celx.newRotation(o->getOrientation());

    return 1;
}

static int observer_rotate(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument required for rotate");

    Observer* o = this_observer(l);

    auto q = celx.toRotation(2);
    if (q == nullptr)
    {
        celx.doError("Argument to observer:setpos must be a rotation");
        return 0;
    }
    o->rotate(q->cast<float>());

    return 0;
}

static int observer_orbit(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument required for orbit");

    Observer* o = this_observer(l);

    auto q = celx.toRotation(2);
    if (q == nullptr)
    {
        celx.doError("Argument for observer:orbit must be a rotation");
        return 0;
    }
    o->orbit(Selection(), q->cast<float>());

    return 0;
}

static int observer_lookat(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(3, 4, "Two or three arguments required for lookat");
    int argc = lua_gettop(l);

    Observer* o = this_observer(l);

    UniversalCoord* from = nullptr;
    UniversalCoord* to = nullptr;
    Vector3d* upd = nullptr;

    if (argc == 3)
    {
        to = celx.toPosition(2);
        upd = celx.toVector(3);
        if (to == nullptr)
        {
            celx.doError("Argument 1 (of 2) to observer:lookat must be of type position");
            return 0;
        }
    }
    else
    {
        if (argc == 4)
        {
            from = celx.toPosition(2);
            to = celx.toPosition(3);
            upd = celx.toVector(4);

            if (to == nullptr || from == nullptr)
            {
                celx.doError("Argument 1 and 2 (of 3) to observer:lookat must be of type position");
                return 0;
            }
        }
    }

    if (upd == nullptr)
    {
        celx.doError("Last argument to observer:lookat must be of type vector");
        return 0;
    }
    Vector3d nd;
    if (from == nullptr)
    {
        nd = to->offsetFromKm(o->getPosition());
    }
    else
    {
        nd = to->offsetFromKm(*from);
    }
    Vector3f up = upd->cast<float>();
    Vector3f n = nd.cast<float>().normalized();

    Vector3f v = n.cross(up).normalized();
    Vector3f u = v.cross(n);
    Matrix3f m;
    m.row(0) = v; m.row(1) = u; m.row(2) = n * (-1);
    o->setOrientation(Quaternionf(m));

    return 0;
}

static int observer_gototable(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "Expected one table as argument to goto");

    Observer* o = this_observer(l);
    if (!lua_istable(l, 2))
    {
        lua_pushstring(l, "Argument to goto must be a table");
    }

    Observer::JourneyParams jparams;
    jparams.duration = Observer::JourneyDuration;
    jparams.from = o->getPosition();
    jparams.to = o->getPosition();
    jparams.initialOrientation = o->getOrientation();
    jparams.finalOrientation = o->getOrientation();
    jparams.startInterpolation = Observer::StartInterpolation;
    jparams.endInterpolation = Observer::EndInterpolation;
    jparams.accelTime = Observer::AccelerationTime;
    jparams.traj = Observer::TrajectoryType::Linear;

    lua_pushstring(l, "duration");
    lua_gettable(l, 2);
    jparams.duration = celx.safeGetNumber(3, NoErrors, "", Observer::JourneyDuration);
    lua_settop(l, 2);

    lua_pushstring(l, "from");
    lua_gettable(l, 2);
    UniversalCoord* from = celx.toPosition(3);
    if (from != nullptr)
        jparams.from = *from;
    lua_settop(l, 2);

    lua_pushstring(l, "to");
    lua_gettable(l, 2);
    UniversalCoord* to = celx.toPosition(3);
    if (to != nullptr)
        jparams.to = *to;
    lua_settop(l, 2);

    lua_pushstring(l, "initialOrientation");
    lua_gettable(l, 2);
    auto rot1 = celx.toRotation(3);
    if (rot1 != nullptr)
        jparams.initialOrientation = *rot1;
    lua_settop(l, 2);

    lua_pushstring(l, "finalOrientation");
    lua_gettable(l, 2);
    auto rot2 = celx.toRotation(3);
    if (rot2 != nullptr)
        jparams.finalOrientation = *rot2;
    lua_settop(l, 2);

    lua_pushstring(l, "startInterpolation");
    lua_gettable(l, 2);
    jparams.startInterpolation = celx.safeGetNumber(3, NoErrors, "", Observer::StartInterpolation);
    lua_settop(l, 2);

    lua_pushstring(l, "endInterpolation");
    lua_gettable(l, 2);
    jparams.endInterpolation = celx.safeGetNumber(3, NoErrors, "", Observer::EndInterpolation);
    lua_settop(l, 2);

    lua_pushstring(l, "accelTime");
    lua_gettable(l, 2);
    jparams.accelTime = celx.safeGetNumber(3, NoErrors, "", Observer::AccelerationTime);
    lua_settop(l, 2);

    jparams.duration = max(0.0, jparams.duration);
    jparams.accelTime = min(1.0, max(0.1, jparams.accelTime));
    jparams.startInterpolation = min(1.0, max(0.0, jparams.startInterpolation));
    jparams.endInterpolation = min(1.0, max(0.0, jparams.endInterpolation));

    // args are in universal coords, let setFrame handle conversion:
    auto tmp = o->getFrame();
    o->setFrame(ObserverFrame::CoordinateSystem::Universal, Selection());
    o->gotoJourney(jparams);
    o->setFrame(tmp);

    return 0;
}

// First argument is the target object or position; optional second argument
// is the travel time
static int observer_goto(lua_State* l)
{
    CelxLua celx(l);
    if (lua_gettop(l) == 2 && lua_istable(l, 2))
    {
        // handle this in own function
        return observer_gototable(l);
    }
    celx.checkArgs(1, 6, "One to five arguments expected to observer:goto");

    Observer* o = this_observer(l);

    Selection* sel = celx.toObject(2);
    UniversalCoord* uc = celx.toPosition(2);
    if (sel == nullptr && uc == nullptr)
    {
        celx.doError("First arg to observer:gotoobject must be object or position");
        return 0;
    }

    double travelTime, startInter, endInter, accelTime;
    travelTime = celx.safeGetNumber(3, WrongType,
                                    "Second arg to observer:gotoobject must be a number",
                                    Observer::JourneyDuration);
    startInter = celx.safeGetNumber(4, WrongType,
                                    "Third arg to observer:gotoobject must be a number",
                                    Observer::StartInterpolation);
    endInter = celx.safeGetNumber(5, WrongType,
                                  "Fourth arg to observer:gotoobject must be a number",
                                  Observer::EndInterpolation);
    accelTime = celx.safeGetNumber(6, WrongType,
                                   "Fifth arg to observer:goto must be a number",
                                   Observer::AccelerationTime);
    if (startInter < 0.0 || startInter > 1.0)
        startInter = Observer::StartInterpolation;
    if (endInter < 0.0 || endInter > 1.0)
        endInter = Observer::EndInterpolation;

    // The first argument may be either an object or a position
    if (sel != nullptr)
    {
        o->gotoSelection(*sel,
                         travelTime,
                         startInter,
                         endInter,
                         accelTime,
                         Vector3f::UnitY(),
                         ObserverFrame::CoordinateSystem::ObserverLocal);
    }
    else
    {
        o->gotoLocation(*uc, o->getOrientation(), travelTime);
    }

    return 0;
}

static int observer_gotolonglat(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 7, "One to five arguments expected to observer:gotolonglat");

    Observer* o = this_observer(l);

    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First arg to observer:gotolonglat must be an object");
        return 0;
    }
    double defaultDistance = sel->radius() * 5.0;

    double longitude  = celx.safeGetNumber(3, WrongType, "Second arg to observer:gotolonglat must be a number", 0.0);
    double latitude   = celx.safeGetNumber(4, WrongType, "Third arg to observer:gotolonglat must be a number", 0.0);
    double distance   = celx.safeGetNumber(5, WrongType, "Fourth arg to observer:gotolonglat must be a number", defaultDistance);
    double travelTime = celx.safeGetNumber(6, WrongType, "Fifth arg to observer:gotolonglat must be a number", 5.0);

    Vector3f up = Vector3f::UnitY();
    if (lua_gettop(l) >= 7)
    {
        auto uparg = celx.toVector(7);
        if (uparg == nullptr)
        {
            celx.doError("Sixth argument to observer:gotolonglat must be a vector");
            return 0;
        }
        up = uparg->cast<float>();
    }
    o->gotoSelectionLongLat(*sel, travelTime, distance, (float)longitude, (float)latitude, up);

    return 0;
}

// observer:gotolocation(position [, duration [, rotation]])
//
// Travels to `position` over `duration` seconds (default 5), optionally
// also rotating to a final `rotation`. Equivalent to the legacy gotoloc{}
// command, including its xrot/yrot/zrot Euler form (build the rotation
// script-side as an intrinsic-XYZ chain of celestia:newrotation factories).
//
// NOTE: `position` is interpreted in the observer's *current* reference
// frame, not in universal coordinates -- this matches the legacy gotoloc
// command, where Observer::gotoLocation assigns the input straight to
// journey.to without converting from universal. In practice that means
// scripts must explicitly put the observer in a known frame before
// calling -- e.g. obs:setframe(celestia:newframe("universal")) -- when
// the position is meant as an absolute coordinate. (.cel scripts get
// this for free because the legacy cancel{} command resets the frame to
// Universal as part of its work; obs:cancelgoto() does NOT, it only
// stops motion.) The table form obs:goto{to=...} avoids the gotcha by
// switching to universal internally before running the journey.
static int observer_gotolocation(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 4, "Expected one to three arguments to observer:gotolocation");

    Observer* o = this_observer(l);

    UniversalCoord* uc = celx.toPosition(2);
    if (uc == nullptr)
    {
        celx.doError("First arg to observer:gotolocation must be a position");
        return 0;
    }

    double travelTime = celx.safeGetNumber(3, WrongType,
                                           "Second argument to observer:gotolocation must be a number",
                                           5.0);
    if (travelTime < 0)
        travelTime = 0.0;

    Quaterniond orientation = o->getOrientation();
    if (const Quaterniond* rot = celx.toRotation(4); rot != nullptr)
        orientation = *rot;

    o->gotoLocation(*uc, orientation, travelTime);

    return 0;
}

// observer:gotodistance(target, distance [, time [, up [, upframe]]])
//
// `upframe` is an optional coordinate-system name ("universal",
// "observer", "ecliptic", "equatorial", "bodyfixed", "lock", "chase")
// describing the frame the `up` vector is expressed in. The default
// remains "universal" for backward compatibility, but ports of the
// legacy goto{} command should pass "observer" to match its default
// (parseGotoCommand sets upFrame = ObserverLocal).
static int observer_gotodistance(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 6, "One to five arguments expected to observer:gotodistance");

    Observer* o = this_observer(l);
    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First arg to observer:gotodistance must be object");
        return 0;
    }

    double distance = celx.safeGetNumber(3, WrongType, "Second arg to observer:gotodistance must be a number", 20000);
    double travelTime = celx.safeGetNumber(4, WrongType, "Third arg to observer:gotodistance must be a number", 5.0);

    Vector3f up = Vector3f::UnitY();
    if (lua_gettop(l) > 4)
    {
        auto up_arg = celx.toVector(5);
        if (up_arg == nullptr)
        {
            celx.doError("Fourth arg to observer:gotodistance must be a vector");
            return 0;
        }

        up = up_arg->cast<float>();
    }

    auto upFrame = ObserverFrame::CoordinateSystem::Universal;
    if (lua_gettop(l) >= 6)
    {
        const char* upFrameName = celx.safeGetString(6, AllErrors,
                                                     "Fifth arg to observer:gotodistance must be a string");
        upFrame = parseCoordSys(upFrameName);
    }

    o->gotoSelection(*sel, travelTime, distance, up, upFrame);

    return 0;
}

static int observer_gotosurface(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 3, "One to two arguments expected to observer:gotosurface");

    Observer* o = this_observer(l);
    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First arg to observer:gotosurface must be object");
        return 0;
    }

    double travelTime = celx.safeGetNumber(3, WrongType, "Second arg to observer:gotosurface must be a number", 5.0);

    // This is needed because gotoSurface expects frame to be geosync:
    o->geosynchronousFollow(*sel);
    o->gotoSurface(*sel, travelTime);

    return 0;
}

static int observer_center(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 3, "Expected one or two arguments for to observer:center");

    Observer* o = this_observer(l);
    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First argument to observer:center must be an object");
        return 0;
    }
    double travelTime = celx.safeGetNumber(3, WrongType, "Second arg to observer:center must be a number", 5.0);

    o->centerSelection(*sel, travelTime);

    return 0;
}

static int observer_centerorbit(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 3, "Expected one or two arguments for to observer:center");

    Observer* o = this_observer(l);
    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First argument to observer:centerorbit must be an object");
        return 0;
    }
    double travelTime = celx.safeGetNumber(3, WrongType, "Second arg to observer:centerorbit must be a number", 5.0);

    o->centerSelectionCO(*sel, travelTime);

    return 0;
}

static int observer_cancelgoto(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "Expected no arguments to observer:cancelgoto");

    Observer* o = this_observer(l);
    o->cancelMotion();

    return 0;
}

static int observer_follow(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument expected for observer:follow");

    Observer* o = this_observer(l);
    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First argument to observer:follow must be an object");
        return 0;
    }
    o->follow(*sel);

    return 0;
}

static int observer_synchronous(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument expected for observer:synchronous");

    Observer* o = this_observer(l);
    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First argument to observer:synchronous must be an object");
        return 0;
    }
    o->geosynchronousFollow(*sel);

    return 0;
}

static int observer_lock(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument expected for observer:lock");

    Observer* o = this_observer(l);
    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First argument to observer:phaseLock must be an object");
        return 0;
    }
    o->phaseLock(*sel);

    return 0;
}

static int observer_chase(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument expected for observer:chase");

    Observer* o = this_observer(l);
    Selection* sel = celx.toObject(2);
    if (sel == nullptr)
    {
        celx.doError("First argument to observer:chase must be an object");
        return 0;
    }
    o->chase(*sel);

    return 0;
}

static int observer_track(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument expected for observer:track");

    Observer* o = this_observer(l);

    // If the argument is nil, clear the tracked object
    if (lua_isnil(l, 2))
    {
        o->setTrackedObject(Selection());
    }
    else
    {
        // Otherwise, turn on tracking and set the tracked object
        Selection* sel = celx.toObject(2);
        if (sel == nullptr)
        {
            celx.doError("First argument to observer:center must be an object");
            return 0;
        }
        o->setTrackedObject(*sel);
    }

    return 0;
}

static int observer_gettrackedobject(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No arguments expected to observer:gettrackedobject");

    Observer* o = this_observer(l);
    celx.newObject(o->getTrackedObject());

    return 1;
}

// Return true if the observer is still moving as a result of a goto, center,
// or similar command.
static int observer_travelling(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No arguments expected to observer:travelling");

    Observer* o = this_observer(l);
    if (o->getMode() == Observer::ObserverMode::Travelling)
        lua_pushboolean(l, 1);
    else
        lua_pushboolean(l, 0);

    return 1;
}

// Return the observer's current time as a Julian day number
static int observer_gettime(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No arguments expected to observer:gettime");

    Observer* o = this_observer(l);
    lua_pushnumber(l, o->getTime());

    return 1;
}

// Return the observer's current position
static int observer_getposition(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No arguments expected to observer:getposition");

    Observer* o = this_observer(l);
    celx.newPosition(o->getPosition());

    return 1;
}

static int observer_getsurface(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "One argument expected to observer:getsurface()");

    Observer* obs = this_observer(l);
    lua_pushstring(l, obs->getDisplayedSurface().c_str());

    return 1;
}

static int observer_setsurface(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument expected to observer:setsurface()");

    Observer* obs = this_observer(l);
    const char* s = lua_tostring(l, 2);

    if (s == nullptr)
        obs->setDisplayedSurface("");
    else
        obs->setDisplayedSurface(s);

    return 0;
}

static int observer_getframe(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No arguments expected for observer:getframe()");

    Observer* obs = this_observer(l);

    auto frame = obs->getFrame();
    celx.newFrame(*frame);
    return 1;
}

static int observer_setframe(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument required for observer:setframe()");

    Observer* obs = this_observer(l);

    ObserverFrame* frame;
    frame = celx.toFrame(2);
    if (frame != nullptr)
    {
        obs->setFrame(std::make_shared<ObserverFrame>(*frame));
    }
    else
    {
        celx.doError("Argument to observer:setframe must be a frame");
    }
    return 0;
}

static int observer_setspeed(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument required for observer:setspeed()");

    Observer* obs = this_observer(l);

    double speed = celx.safeGetNumber(2, AllErrors, "First argument to observer:setspeed must be a number");
    obs->setTargetSpeed((float) astro::microLightYearsToKilometers(speed));

    return 0;
}

static int observer_getspeed(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No argument expected for observer:getspeed()");

    const Observer* obs = this_observer(l);

    lua_pushnumber(l, (lua_Number) astro::kilometersToMicroLightYears(obs->getTargetSpeed()));

    return 1;
}

// observer:setfov(fov [, syncZoom])
//
// Sets the observer's field of view (radians). When `syncZoom` is true
// (the default), also runs CelestiaCore::setZoomFromFOV so each view's
// zoom field is recomputed from its FOV via the projection mode -- this
// is the long-standing celx behavior. Pass `false` to skip the sync,
// matching the legacy `set{name "FOV"}` cel command which only writes
// Observer::fov and leaves Observer::zoom alone.
static int observer_setfov(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 3, "One or two arguments expected to observer:setfov(fov [, syncZoom])");

    Observer* obs = this_observer(l);
    double fov = celx.safeGetNumber(2, AllErrors, "Argument to observer:setfov() must be a number");
    bool syncZoom = celx.safeGetBoolean(3, WrongType,
                                        "Second argument to observer:setfov() must be a boolean",
                                        true);
    if ((fov >= 0.001_deg) && (fov <= 120.0_deg))
    {
        obs->setFOV(static_cast<float>(fov));
        if (syncZoom)
            celx.appCore(AllErrors)->setZoomFromFOV();
    }
    return 0;
}

static int observer_getfov(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No argument expected to observer:getfov()");

    Observer* obs = this_observer(l);
    lua_pushnumber(l, obs->getFOV());
    return 1;
}

static int observer_splitview(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 3, "One or two arguments expected for observer:splitview()");

    const Observer* obs = this_observer(l);
    CelestiaCore* appCore = celx.appCore(AllErrors);
    const char* splitType = celx.safeGetString(2, AllErrors, "First argument to observer:splitview() must be a string");
    celestia::View::Type type = (compareIgnoringCase(splitType, "h") == 0)
        ? celestia::View::HorizontalSplit
        : celestia::View::VerticalSplit;
    double splitPos = celx.safeGetNumber(3, WrongType, "Number expected as argument to observer:splitview()", 0.5);
    if (splitPos < 0.1)
        splitPos = 0.1;
    if (splitPos > 0.9)
        splitPos = 0.9;
    celestia::View* view = getViewByObserver(appCore, obs);
    appCore->splitView(type, view, (float)splitPos);
    return 0;
}

static int observer_deleteview(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No argument expected for observer:deleteview()");

    const Observer* obs = this_observer(l);
    CelestiaCore* appCore = celx.appCore(AllErrors);
    celestia::View* view = getViewByObserver(appCore, obs);
    appCore->deleteView(view);
    return 0;
}

static int observer_singleview(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No argument expected for observer:singleview()");

    const Observer* obs = this_observer(l);
    CelestiaCore* appCore = celx.appCore(AllErrors);
    const celestia::View* view = getViewByObserver(appCore, obs);
    appCore->singleView(view);
    return 0;
}

static int observer_makeactiveview(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No argument expected for observer:makeactiveview()");

    const Observer* obs = this_observer(l);
    CelestiaCore* appCore = celx.appCore(AllErrors);
    const celestia::View* view = getViewByObserver(appCore, obs);
    appCore->setActiveView(view);
    return 0;
}

static int observer_equal(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "Wrong number of arguments for comparison!");

    Observer* o1 = this_observer(l);
    Observer* o2 = to_observer(l, 2);

    lua_pushboolean(l, (o1 == o2));
    return 1;
}

static int observer_setlocationflags(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument expected for observer:setlocationflags()");
    Observer* obs = this_observer(l);
    if (!lua_istable(l, 2))
    {
        celx.doError("Argument to observer:setlocationflags() must be a table");
        return 0;
    }

    lua_pushnil(l);
    auto locationFlags = obs->getLocationFilter();
    while (lua_next(l, -2) != 0)
    {
        string key;
        bool value = false;
        if (lua_isstring(l, -2))
        {
            key = lua_tostring(l, -2);
        }
        else
        {
            celx.doError("Keys in table-argument to observer:setlocationflags() must be strings");
            return 0;
        }
        if (lua_isboolean(l, -1))
        {
            value = lua_toboolean(l, -1) != 0;
        }
        else
        {
            celx.doError("Values in table-argument to observer:setlocationflags() must be boolean");
            return 0;
        }
        auto &LocationFlagMap = celx.appCore(AllErrors)->scriptMaps().LocationFlagMap;
        if (LocationFlagMap.count(key) == 0)
        {
            GetLogger()->warn("Unknown key: {}\n", key);
        }
        else
        {
            const auto flag = LocationFlagMap[key];
            if (value)
            {
                locationFlags |= flag;
            }
            else
            {
                locationFlags &= ~flag;
            }
        }
        lua_pop(l,1);
    }
    obs->setLocationFilter(locationFlags);
    return 0;
}

static int observer_getlocationflags(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(1, 1, "No arguments expected for observer:getlocationflags()");
    Observer* obs = this_observer(l);
    lua_newtable(l);
    const auto locationFlags = obs->getLocationFilter();
    auto &LocationFlagMap = celx.appCore(AllErrors)->scriptMaps().LocationFlagMap;
    std::string itString;
    itString.reserve(celestia::scripts::FlagMapNameLength);
    for (const auto& it : LocationFlagMap)
    {
        itString.clear();
        itString.append(it.first);
        lua_pushstring(l, itString.c_str());
        lua_pushboolean(l, (it.second & locationFlags) != 0);
        lua_settable(l, -3);
    }
    return 1;
}

// observer:changeorbitdistance(d)
//
// Exponential dolly toward (d > 0) or away from (d < 0) the current
// selection.
static int observer_changeorbitdistance(lua_State* l)
{
    CelxLua celx(l);
    celx.checkArgs(2, 2, "One argument required for observer:changeorbitdistance");

    Observer* o = this_observer(l);
    double d = celx.safeGetNumber(2, AllErrors,
                                  "Argument to observer:changeorbitdistance must be a number");

    const CelestiaCore* appCore = celx.appCore(AllErrors);
    o->changeOrbitDistance(appCore->getSimulation()->getSelection(), static_cast<float>(d));
    return 0;
}

void CreateObserverMetaTable(lua_State* l)
{
    CelxLua celx(l);
    celx.createClassMetatable(Celx_Observer);

    celx.registerMethod("__tostring", observer_tostring);
    celx.registerMethod("isvalid", observer_isvalid);
#if LUA_VERSION_NUM < 502
    celx.registerMethod("goto", observer_goto);
#endif
    celx.registerMethod("gotoobject", observer_goto);
    celx.registerMethod("gotolonglat", observer_gotolonglat);
    celx.registerMethod("gotolocation", observer_gotolocation);
    celx.registerMethod("gotodistance", observer_gotodistance);
    celx.registerMethod("gotosurface", observer_gotosurface);
    celx.registerMethod("cancelgoto", observer_cancelgoto);
    celx.registerMethod("setposition", observer_setposition);
    celx.registerMethod("lookat", observer_lookat);
    celx.registerMethod("setorientation", observer_setorientation);
    celx.registerMethod("getorientation", observer_getorientation);
    celx.registerMethod("getspeed", observer_getspeed);
    celx.registerMethod("setspeed", observer_setspeed);
    celx.registerMethod("getfov", observer_getfov);
    celx.registerMethod("setfov", observer_setfov);
    celx.registerMethod("rotate", observer_rotate);
    celx.registerMethod("orbit", observer_orbit);
    celx.registerMethod("changeorbitdistance", observer_changeorbitdistance);
    celx.registerMethod("center", observer_center);
    celx.registerMethod("centerorbit", observer_centerorbit);
    celx.registerMethod("follow", observer_follow);
    celx.registerMethod("synchronous", observer_synchronous);
    celx.registerMethod("chase", observer_chase);
    celx.registerMethod("lock", observer_lock);
    celx.registerMethod("track", observer_track);
    celx.registerMethod("gettrackedobject", observer_gettrackedobject);
    celx.registerMethod("travelling", observer_travelling);
    celx.registerMethod("getframe", observer_getframe);
    celx.registerMethod("setframe", observer_setframe);
    celx.registerMethod("gettime", observer_gettime);
    celx.registerMethod("getposition", observer_getposition);
    celx.registerMethod("getsurface", observer_getsurface);
    celx.registerMethod("setsurface", observer_setsurface);
    celx.registerMethod("splitview", observer_splitview);
    celx.registerMethod("deleteview", observer_deleteview);
    celx.registerMethod("singleview", observer_singleview);
    celx.registerMethod("makeactiveview", observer_makeactiveview);
    celx.registerMethod("getlocationflags", observer_getlocationflags);
    celx.registerMethod("setlocationflags", observer_setlocationflags);
    celx.registerMethod("__eq", observer_equal);

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