#ifdef WITH_RIVE_SCRIPTING
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/viewmodel/viewmodel_property_boolean.hpp"
#include "rive/viewmodel/viewmodel_property_color.hpp"
#include "rive/viewmodel/viewmodel_property_enum.hpp"
#include "rive/viewmodel/viewmodel_property_number.hpp"
#include "rive/viewmodel/viewmodel_property_string.hpp"
#include "rive/viewmodel/viewmodel_property_trigger.hpp"
#include "rive/viewmodel/viewmodel_property_list.hpp"
#include "rive/viewmodel/viewmodel_instance_list.hpp"
#include "rive/viewmodel/viewmodel_instance_list_item.hpp"

#include <math.h>
#include <stdio.h>

using namespace rive;

static void pushViewModelInstanceValue(lua_State* L,
                                       ViewModelInstanceValue* propValue)
{
    switch (propValue->coreType())
    {
        case ViewModelInstanceNumberBase::typeKey:
            lua_newrive<ScriptedPropertyNumber>(
                L,
                L,
                ref_rcp(propValue->as<ViewModelInstanceNumber>()));
            break;
        case ViewModelInstanceTriggerBase::typeKey:
            lua_newrive<ScriptedPropertyTrigger>(
                L,
                L,
                ref_rcp(propValue->as<ViewModelInstanceTrigger>()));
            break;
        case ViewModelInstanceListBase::typeKey:
            lua_newrive<ScriptedPropertyList>(
                L,
                L,
                ref_rcp(propValue->as<ViewModelInstanceList>()));
            break;
        case ViewModelInstanceColorBase::typeKey:
            lua_newrive<ScriptedPropertyColor>(
                L,
                L,
                ref_rcp(propValue->as<ViewModelInstanceColor>()));
            break;
        case ViewModelInstanceStringBase::typeKey:
            lua_newrive<ScriptedPropertyString>(
                L,
                L,
                ref_rcp(propValue->as<ViewModelInstanceString>()));
            break;
        case ViewModelInstanceBooleanBase::typeKey:
            lua_newrive<ScriptedPropertyBoolean>(
                L,
                L,
                ref_rcp(propValue->as<ViewModelInstanceBoolean>()));
            break;
        case ViewModelInstanceEnumBase::typeKey:
            lua_newrive<ScriptedPropertyEnum>(
                L,
                L,
                ref_rcp(propValue->as<ViewModelInstanceEnum>()));
            break;
        default:
            lua_pushnil(L);
            break;
    }
}

// int ScriptedPropertyViewModel::pushValueOfType(const char* name, int
// coreType)
// {
//     auto vmi = m_instanceValue->referenceViewModelInstance();
//     if (!vmi)
//     {
//         lua_pushnil(m_state);
//     }
//     else
//     {
//         auto propValue = vmi->propertyValue(name);
//         if (propValue->coreType() != coreType)
//         {
//             lua_pushnil(m_state);
//         }
//         else
//         {
//             pushViewModelInstanceValue(m_state, propValue);
//         }
//     }
//     return 1;
// }

ScriptedProperty::~ScriptedProperty()
{
    m_instanceValue->removeDelegate(this);
    clearListeners();
}

ScriptedPropertyViewModel::~ScriptedPropertyViewModel()
{
    lua_unref(m_state, m_valueRef);
}

void ScriptedProperty::clearListeners()
{
    for (auto itr = m_listeners.begin(); itr != m_listeners.end(); ++itr)
    {
        const ScriptedListener& listener = *itr;
        lua_unref(m_state, listener.function);
        if (listener.userdata)
        {
            lua_unref(m_state, listener.userdata);
        }
    }
    m_listeners.clear();
}

ScriptedProperty::ScriptedProperty(lua_State* L,
                                   rcp<ViewModelInstanceValue> value) :
    m_state(L), m_instanceValue(std::move(value))
{
    m_instanceValue->addDelegate(this);
}

void ScriptedProperty::valueChanged()
{
    // This works because we don't actually call as we go (or we could
    // invalidate listeners if a callback registers a new or removes a
    // listener). Instead, we build up the call stack and then call for each
    // callback on the stack.
    for (auto itr = m_listeners.rbegin(); itr != m_listeners.rend(); itr++)
    {
        const ScriptedListener& listener = *itr;
        lua_rawgeti(m_state, LUA_REGISTRYINDEX, listener.function);
        if (listener.userdata)
        {
            lua_rawgeti(m_state, LUA_REGISTRYINDEX, listener.userdata);
        }
        else
        {
            // push nil so we have a constant number of args per call
            lua_pushnil(m_state);
        }
    }
    size_t calls = m_listeners.size();
    for (size_t i = 0; i < calls; i++)
    {
        lua_pcall(m_state, 1, 0, 0);
    }
}

int ScriptedProperty::addListener()
{
    if (lua_isfunction(m_state, 2))
    {
        int callbackRef = lua_ref(m_state, 2);
        m_listeners.push_back({callbackRef});
        return 0;
    }
    else if (lua_isfunction(m_state, 3))
    {
        int userdataRef = lua_ref(m_state, 2);
        int callbackRef = lua_ref(m_state, 3);
        m_listeners.push_back({callbackRef, userdataRef});
        return 0;
    }

    luaL_typeerrorL(m_state, 2, lua_typename(m_state, LUA_TFUNCTION));
    return 0;
}

int ScriptedProperty::removeListener()
{
    int checkIndex;
    if (lua_isfunction(m_state, 2))
    {
        checkIndex = 2;
    }
    else if (lua_isfunction(m_state, 3))
    {
        checkIndex = 3;
    }
    else
    {
        luaL_typeerrorL(m_state, 2, lua_typename(m_state, LUA_TFUNCTION));
    }

    for (auto itr = m_listeners.begin(); itr != m_listeners.end();)
    {
        const ScriptedListener& listener = *itr;
        lua_rawgeti(m_state, LUA_REGISTRYINDEX, listener.function);
        if (lua_rawequal(m_state, -1, checkIndex))
        {
            lua_unref(m_state, listener.function);
            if (listener.userdata)
            {
                lua_unref(m_state, listener.userdata);
            }
            itr = m_listeners.erase(itr);
        }
        else
        {
            ++itr;
        }
        lua_pop(m_state, 1);
    }
    return 0;
}

ScriptedPropertyViewModel::ScriptedPropertyViewModel(
    lua_State* L,
    rcp<ViewModel> viewModel,
    rcp<ViewModelInstanceViewModel> value) :
    ScriptedProperty(L, std::move(value)), m_viewModel(std::move(viewModel))
{}

ScriptedViewModel::ScriptedViewModel(lua_State* L,
                                     rcp<ViewModel> viewModel,
                                     rcp<ViewModelInstance> viewModelInstance) :
    m_state(L), m_viewModel(viewModel), m_viewModelInstance(viewModelInstance)
{}

ScriptedViewModel::~ScriptedViewModel()
{
    for (auto itr : m_propertyRefs)
    {
        lua_unref(m_state, itr.second);
    }
}

int ScriptedViewModel::instance(lua_State* L)
{
    if (m_viewModel)
    {
#ifdef WITH_RIVE_TOOLS
        m_viewModel->file()->triggerViewModelCreatedCallback(true);
#endif
        auto instance = m_viewModel->createInstance();
#ifdef WITH_RIVE_TOOLS
        m_viewModel->file()->triggerViewModelCreatedCallback(false);
#endif
        lua_newrive<ScriptedViewModel>(L, L, m_viewModel, instance);
    }
    return 1;
}

int ScriptedViewModel::pushValue(const char* name, int coreType)
{
    auto itr = m_propertyRefs.find(name);
    if (itr != m_propertyRefs.end())
    {
        lua_rawgeti(m_state, LUA_REGISTRYINDEX, itr->second);
        return 1;
    }
    // To be fully typesafe at runtime we should check the property in the
    // viewmodel and make sure the one in the value matches the same type or
    // return a default based on the type of the viewmodel, but this incurs
    // extra performance and since our type system at edit-time lets the user be
    // intentional, for now we runtime error if the type is un-expected and we
    // optimize for the best case.

    auto vmi = m_viewModelInstance;
    if (!vmi)
    {
        // Get type from viewmodel if we have one and user didn't request a type
        // dynamically.
        if (coreType != 0 || !m_viewModel)
        {
            lua_pushnil(m_state);
        }
        else
        {
            auto prop = m_viewModel->property(name);
            switch (prop->coreType())
            {
                case ViewModelPropertyNumberBase::typeKey:
                    lua_newrive<ScriptedPropertyNumber>(m_state,
                                                        m_state,
                                                        nullptr);
                    break;
                case ViewModelPropertyTriggerBase::typeKey:
                    lua_newrive<ScriptedPropertyTrigger>(m_state,
                                                         m_state,
                                                         nullptr);
                    break;
                case ViewModelPropertyListBase::typeKey:
                    lua_newrive<ScriptedPropertyList>(m_state,
                                                      m_state,
                                                      nullptr);
                    break;
                case ViewModelPropertyColorBase::typeKey:
                    lua_newrive<ScriptedPropertyColor>(m_state,
                                                       m_state,
                                                       nullptr);
                    break;
                case ViewModelPropertyStringBase::typeKey:
                    lua_newrive<ScriptedPropertyString>(m_state,
                                                        m_state,
                                                        nullptr);
                    break;
                case ViewModelPropertyBooleanBase::typeKey:
                    lua_newrive<ScriptedPropertyBoolean>(m_state,
                                                         m_state,
                                                         nullptr);
                    break;
                case ViewModelPropertyEnumBase::typeKey:
                    lua_newrive<ScriptedPropertyEnum>(m_state,
                                                      m_state,
                                                      nullptr);
                    break;
            }
        }
    }
    else
    {
        auto propValue = vmi->propertyValue(name);
        if (propValue == nullptr ||
            (coreType != 0 && propValue->coreType() != coreType))
        {
            lua_pushnil(m_state);
        }
        else
        {
            pushViewModelInstanceValue(m_state, propValue);
        }
    }
    m_propertyRefs[name] = lua_ref(m_state, -1);
    return 1;
}

int ScriptedPropertyViewModel::pushValue()
{
    // N.B. we'll need to invalidate m_valueRef if the viewmodel's value on the
    // property changes.
    if (m_valueRef != 0)
    {
        lua_rawgeti(m_state, LUA_REGISTRYINDEX, m_valueRef);
        return 1;
    }
    if (m_instanceValue)
    {
        lua_newrive<ScriptedViewModel>(
            m_state,
            m_state,
            m_viewModel,
            m_instanceValue->as<ViewModelInstanceViewModel>()
                ->referenceViewModelInstance());
    }
    else
    {
        // Push nil or push an empty model ref? For now empty/with def values.
        lua_newrive<ScriptedViewModel>(m_state, m_state, m_viewModel, nullptr);
    }
    m_valueRef = lua_ref(m_state, -1);
    return 1;
}

static int property_vm_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto vmProp = lua_torive<ScriptedPropertyViewModel>(L, 1);
    assert(vmProp->state() == L);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            return vmProp->pushValue();
        default:
            return 0;
    }
}

static int vm_index(lua_State* L)
{
    auto vm = lua_torive<ScriptedViewModel>(L, 1);
    size_t namelen = 0;
    const char* name = luaL_checklstring(L, 2, &namelen);
    assert(vm->state() == L);
    return vm->pushValue(name);
}

static int property_namecall_atom(lua_State* L,
                                  ScriptedProperty* property,
                                  int atom,
                                  bool& error)
{
    switch (atom)
    {
        case (int)LuaAtoms::addListener:
        {
            assert(property->state() == L);
            return property->addListener();
        }
        case (int)LuaAtoms::removeListener:
        {
            assert(property->state() == L);
            return property->removeListener();
        }
        case (int)LuaAtoms::fire:
        {
            auto value = property->instanceValue();
            if (value != nullptr && value->is<ViewModelInstanceTrigger>())
            {
                value->as<ViewModelInstanceTrigger>()->trigger();
                return 0;
            }
        }
        case (int)LuaAtoms::push:
        {
            auto scriptedViewModel = lua_torive<ScriptedViewModel>(L, 2);
            auto vmi = scriptedViewModel->viewModelInstance();
            auto copy = ref_rcp(vmi.get());
            auto list = property->instanceValue()->as<ViewModelInstanceList>();
            auto listItem = make_rcp<ViewModelInstanceListItem>();

            listItem->viewModelInstance(copy);
            list->addItem(listItem);
            return 0;
        }
        case (int)LuaAtoms::pop:
        {
            auto list = property->instanceValue()->as<ViewModelInstanceList>();
            auto listItem = list->pop();
            if (listItem)
            {
                auto vmi = listItem->viewModelInstance();
                if (vmi)
                {
                    lua_newrive<ScriptedViewModel>(L,
                                                   L,
                                                   ref_rcp(vmi->viewModel()),
                                                   vmi);
                    return 1;
                }
            }
            return 0;
        }
        case (int)LuaAtoms::swap:
        {
            auto list = property->instanceValue()->as<ViewModelInstanceList>();
            auto index1 = lua_tounsigned(L, 2);
            auto index2 = lua_tounsigned(L, 3);
            list->swap(index1 - 1, index2 - 1);
            return 0;
        }
        case (int)LuaAtoms::shift:
        {
            auto list = property->instanceValue()->as<ViewModelInstanceList>();
            auto listItem = list->shift();
            if (listItem)
            {
                auto vmi = listItem->viewModelInstance();
                if (vmi)
                {
                    lua_newrive<ScriptedViewModel>(L,
                                                   L,
                                                   ref_rcp(vmi->viewModel()),
                                                   vmi);
                    return 1;
                }
            }
            return 0;
        }

        case (int)LuaAtoms::insert:
        {
            auto scriptedViewModel = lua_torive<ScriptedViewModel>(L, 2);
            auto index = lua_tounsigned(L, 3);
            auto vmi = scriptedViewModel->viewModelInstance();
            auto copy = ref_rcp(vmi.get());
            auto list = property->instanceValue()->as<ViewModelInstanceList>();
            auto listItem = make_rcp<ViewModelInstanceListItem>();

            listItem->viewModelInstance(copy);
            list->addItemAt(listItem, index);
            return 0;
        }
    }
    error = true;
    return 0;
}

static int vm_namecall(lua_State* L)
{
    int atom;
    const char* str = lua_namecallatom(L, &atom);
    if (str != nullptr)
    {
        auto vm = lua_torive<ScriptedViewModel>(L, 1);
        switch (atom)
        {
            case (int)LuaAtoms::getNumber:
            {
                size_t namelen = 0;
                const char* name = luaL_checklstring(L, 2, &namelen);
                assert(vm->state() == L);
                return vm->pushValue(name,
                                     ViewModelInstanceNumberBase::typeKey);
            }
            case (int)LuaAtoms::getTrigger:
            {
                size_t namelen = 0;
                const char* name = luaL_checklstring(L, 2, &namelen);
                assert(vm->state() == L);
                return vm->pushValue(name,
                                     ViewModelInstanceTriggerBase::typeKey);
            }
            case (int)LuaAtoms::getString:
            {
                size_t namelen = 0;
                const char* name = luaL_checklstring(L, 2, &namelen);
                assert(vm->state() == L);
                return vm->pushValue(name,
                                     ViewModelInstanceStringBase::typeKey);
            }
            case (int)LuaAtoms::getBoolean:
            {
                size_t namelen = 0;
                const char* name = luaL_checklstring(L, 2, &namelen);
                assert(vm->state() == L);
                return vm->pushValue(name,
                                     ViewModelInstanceBooleanBase::typeKey);
            }
            case (int)LuaAtoms::getColor:
            {
                size_t namelen = 0;
                const char* name = luaL_checklstring(L, 2, &namelen);
                assert(vm->state() == L);
                return vm->pushValue(name, ViewModelInstanceColorBase::typeKey);
            }
            case (int)LuaAtoms::getList:
            {
                size_t namelen = 0;
                const char* name = luaL_checklstring(L, 2, &namelen);
                assert(vm->state() == L);
                return vm->pushValue(name, ViewModelInstanceListBase::typeKey);
            }
            case (int)LuaAtoms::instance:
            case (int)LuaAtoms::newAtom:
            {
                assert(vm->state() == L);
                return vm->instance(L);
            }
            default:
                break;
        }
    }

    luaL_error(L,
               "%s is not a valid method of %s",
               str,
               ScriptedPropertyViewModel::luaName);
    return 0;
}

static int property_vm_namecall(lua_State* L)
{
    int atom;
    const char* str = lua_namecallatom(L, &atom);
    if (str != nullptr)
    {
        auto vm = lua_torive<ScriptedPropertyViewModel>(L, 1);

        bool error = false;
        int stackChange = property_namecall_atom(L, vm, atom, error);
        if (!error)
        {
            return stackChange;
        }
    }

    luaL_error(L,
               "%s is not a valid method of %s",
               str,
               ScriptedPropertyViewModel::luaName);
    return 0;
}

static int property_namecall(lua_State* L)
{
    int atom;
    const char* str = lua_namecallatom(L, &atom);
    const char* name = "Property";
    if (str != nullptr)
    {
        auto tag = lua_userdatatag(L, 1);
        switch (tag)
        {
            case ScriptedPropertyNumber::luaTag:
                name = ScriptedPropertyNumber::luaName;
                break;
            case ScriptedPropertyTrigger::luaTag:
                name = ScriptedPropertyTrigger::luaName;
                break;
            case ScriptedPropertyColor::luaTag:
                name = ScriptedPropertyColor::luaName;
                break;
            case ScriptedPropertyString::luaTag:
                name = ScriptedPropertyString::luaName;
                break;
            case ScriptedPropertyBoolean::luaTag:
                name = ScriptedPropertyBoolean::luaName;
                break;
            case ScriptedPropertyEnum::luaTag:
                name = ScriptedPropertyEnum::luaName;
                break;
            case ScriptedPropertyList::luaTag:
                name = ScriptedPropertyList::luaName;
                break;
            default:
                luaL_typeerror(L, 1, name);
                break;
        }
        auto vm = (ScriptedProperty*)lua_touserdata(L, 1);
        bool error = false;
        int stackChange = property_namecall_atom(L, vm, atom, error);
        if (!error)
        {
            return stackChange;
        }
    }
    luaL_error(L, "%s is not a valid method of %s", str, name);
    return 0;
}

ScriptedPropertyNumber::ScriptedPropertyNumber(
    lua_State* L,
    rcp<ViewModelInstanceNumber> value) :
    ScriptedProperty(L, std::move(value))
{}

ScriptedPropertyTrigger::ScriptedPropertyTrigger(
    lua_State* L,
    rcp<ViewModelInstanceTrigger> value) :
    ScriptedProperty(L, std::move(value))
{}

void ScriptedPropertyNumber::setValue(float value)
{
    if (m_instanceValue)
    {
        m_instanceValue->as<ViewModelInstanceNumber>()->propertyValue(value);
    }
}

int ScriptedPropertyNumber::pushValue()
{
    if (m_instanceValue)
    {
        lua_pushnumber(
            m_state,
            m_instanceValue->as<ViewModelInstanceNumber>()->propertyValue());
    }
    else
    {
        lua_pushnumber(m_state, 0);
    }
    return 1;
}

ScriptedPropertyColor::ScriptedPropertyColor(
    lua_State* L,
    rcp<ViewModelInstanceColor> value) :
    ScriptedProperty(L, std::move(value))
{}

void ScriptedPropertyColor::setValue(unsigned value)
{
    if (m_instanceValue)
    {
        m_instanceValue->as<ViewModelInstanceColor>()->propertyValue(
            (int)value);
    }
}

int ScriptedPropertyColor::pushValue()
{
    if (m_instanceValue)
    {
        lua_pushunsigned(m_state,
                         (unsigned)m_instanceValue->as<ViewModelInstanceColor>()
                             ->propertyValue());
    }
    else
    {
        lua_pushunsigned(m_state, 0);
    }
    return 1;
}

ScriptedPropertyString::ScriptedPropertyString(
    lua_State* L,
    rcp<ViewModelInstanceString> value) :
    ScriptedProperty(L, std::move(value))
{}

void ScriptedPropertyString::setValue(const std::string& value)
{
    if (m_instanceValue)
    {
        m_instanceValue->as<ViewModelInstanceString>()->propertyValue(value);
    }
}

int ScriptedPropertyString::pushValue()
{
    if (m_instanceValue)
    {
        lua_pushstring(m_state,
                       m_instanceValue->as<ViewModelInstanceString>()
                           ->propertyValue()
                           .c_str());
    }
    else
    {
        lua_pushstring(m_state, "");
    }
    return 1;
}

ScriptedPropertyBoolean::ScriptedPropertyBoolean(
    lua_State* L,
    rcp<ViewModelInstanceBoolean> value) :
    ScriptedProperty(L, std::move(value))
{}

void ScriptedPropertyBoolean::setValue(bool value)
{
    if (m_instanceValue)
    {
        m_instanceValue->as<ViewModelInstanceBoolean>()->propertyValue(value);
    }
}

int ScriptedPropertyBoolean::pushValue()
{
    if (m_instanceValue)
    {
        lua_pushboolean(
            m_state,
            m_instanceValue->as<ViewModelInstanceBoolean>()->propertyValue());
    }
    else
    {
        lua_pushboolean(m_state, 0);
    }
    return 1;
}

ScriptedPropertyEnum::ScriptedPropertyEnum(lua_State* L,
                                           rcp<ViewModelInstanceEnum> value) :
    ScriptedProperty(L, std::move(value))
{}

void ScriptedPropertyEnum::setValue(const std::string& value)
{
    if (m_instanceValue)
    {
        m_instanceValue->as<ViewModelInstanceEnum>()->value(value);
    }
}

int ScriptedPropertyEnum::pushValue()
{
    if (m_instanceValue)
    {
        auto vmi = m_instanceValue->as<ViewModelInstanceEnum>();
        auto vmProp = vmi->viewModelProperty()->as<ViewModelPropertyEnum>();
        if (vmProp != nullptr && vmProp->dataEnum() != nullptr)
        {
            auto values = vmProp->dataEnum()->values();
            uint32_t index = vmi->propertyValue();
            if (index < values.size())
            {
                lua_pushstring(m_state, values[index]->key().c_str());
                return 1;
            }
        }
    }
    lua_pushstring(m_state, "");
    return 1;
}

ScriptedPropertyList::ScriptedPropertyList(lua_State* L,
                                           rcp<ViewModelInstanceList> value) :
    ScriptedProperty(L, std::move(value))
{}

ScriptedPropertyList::~ScriptedPropertyList()
{
    for (const auto& pair : m_propertyRefs)
    {
        lua_unref(m_state, pair.second);
    }
}

void ScriptedPropertyList::valueChanged()
{
    m_changed = true;
    ScriptedProperty::valueChanged();
}

int ScriptedPropertyList::pushLength()
{
    if (m_instanceValue)
    {
        lua_pushinteger(m_state,
                        (int)m_instanceValue->as<ViewModelInstanceList>()
                            ->listItems()
                            .size());
    }
    else
    {
        lua_pushinteger(m_state, 0);
    }
    return 1;
}

int ScriptedPropertyList::pushValue(int index)
{
    if (m_instanceValue)
    {
        auto items = m_instanceValue->as<ViewModelInstanceList>()->listItems();

        if (m_changed)
        {
            std::unordered_map<ViewModelInstance*, int> refs;

            // re-validate references.
            for (auto& item : items)
            {
                auto vmi = item->viewModelInstance().get();
                auto itr = m_propertyRefs.find(vmi);
                if (itr != m_propertyRefs.end())
                {
                    refs[vmi] = itr->second;
                    m_propertyRefs.erase(itr);
                }
            }

            for (const auto& pair : m_propertyRefs)
            {
                lua_unref(m_state, pair.second);
            }
            m_propertyRefs = refs;

            m_changed = false;
        }

        if (index >= items.size())
        {
            lua_pushnil(m_state);
        }
        else
        {

            auto listItem = items[index];
            auto vmi = listItem->viewModelInstance();
            auto itr = m_propertyRefs.find(vmi.get());
            if (itr != m_propertyRefs.end())
            {
                lua_rawgeti(m_state, LUA_REGISTRYINDEX, itr->second);
            }
            else
            {
                lua_newrive<ScriptedViewModel>(m_state,
                                               m_state,
                                               ref_rcp(vmi->viewModel()),
                                               vmi);
                m_propertyRefs[vmi.get()] = lua_ref(m_state, -1);
            }
        }
    }
    else
    {
        lua_pushnil(m_state);
    }
    return 1;
}

static int property_list_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    auto propertyList = lua_torive<ScriptedPropertyList>(L, 1);
    // if it's not an atom it should be an index into the array
    if (!key)
    {
        int index = luaL_checkinteger(L, 2);
        return propertyList->pushValue(index - 1);
    }

    switch (atom)
    {
        case (int)LuaAtoms::length:
            assert(propertyList->state() == L);
            return propertyList->pushLength();
        default:
            return 0;
    }
}

static int property_number_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyNumber = lua_torive<ScriptedPropertyNumber>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            assert(propertyNumber->state() == L);
            return propertyNumber->pushValue();
        default:
            return 0;
    }
}

static int property_color_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyColor = lua_torive<ScriptedPropertyColor>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            assert(propertyColor->state() == L);
            return propertyColor->pushValue();
        default:
            return 0;
    }
}

static int property_number_newindex(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyNumber = lua_torive<ScriptedPropertyNumber>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            propertyNumber->setValue(float(luaL_checknumber(L, 3)));
        default:
            return 0;
    }

    return 0;
}

static int property_color_newindex(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyColor = lua_torive<ScriptedPropertyColor>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            propertyColor->setValue(luaL_checkunsigned(L, 3));
        default:
            return 0;
    }

    return 0;
}

static int property_string_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyString = lua_torive<ScriptedPropertyString>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            assert(propertyString->state() == L);
            return propertyString->pushValue();
        default:
            return 0;
    }
}

static int property_string_newindex(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyString = lua_torive<ScriptedPropertyString>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            propertyString->setValue(luaL_checkstring(L, 3));
        default:
            return 0;
    }

    return 0;
}

static int property_boolean_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyBoolean = lua_torive<ScriptedPropertyBoolean>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            assert(propertyBoolean->state() == L);
            return propertyBoolean->pushValue();
        default:
            return 0;
    }
}

static int property_boolean_newindex(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyBoolean = lua_torive<ScriptedPropertyBoolean>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            propertyBoolean->setValue(luaL_checkboolean(L, 3));
        default:
            return 0;
    }

    return 0;
}

static int property_enum_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyEnum = lua_torive<ScriptedPropertyEnum>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
            assert(propertyEnum->state() == L);
            return propertyEnum->pushValue();
        default:
            return 0;
    }
}

static int property_enum_newindex(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);
    if (!key)
    {
        luaL_typeerrorL(L, 2, lua_typename(L, LUA_TSTRING));
        return 0;
    }

    auto propertyEnum = lua_torive<ScriptedPropertyEnum>(L, 1);
    switch (atom)
    {
        case (int)LuaAtoms::value:
        {
            propertyEnum->setValue(luaL_checkstring(L, 3));
        }
        default:
            return 0;
    }

    return 0;
}

static int vm_eq(lua_State* L)
{
    auto lhs = lua_torive<ScriptedViewModel>(L, 1);
    auto rhs = lua_torive<ScriptedViewModel>(L, 2);
    lua_pushboolean(L, lhs->viewModelInstance() == rhs->viewModelInstance());
    return 1;
}

int luaopen_rive_properties(lua_State* L)
{
    {
        lua_register_rive<ScriptedViewModel>(L);

        lua_pushcfunction(L, vm_index, nullptr);
        lua_setfield(L, -2, "__index");

        lua_pushcfunction(L, vm_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_pushcfunction(L, vm_eq, nullptr);
        lua_setfield(L, -2, "__eq");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    {
        lua_register_rive<ScriptedPropertyViewModel>(L);

        lua_pushcfunction(L, property_vm_index, nullptr);
        lua_setfield(L, -2, "__index");

        lua_pushcfunction(L, property_vm_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    {
        lua_register_rive<ScriptedPropertyNumber>(L);

        lua_pushcfunction(L, property_number_index, nullptr);
        lua_setfield(L, -2, "__index");

        lua_pushcfunction(L, property_number_newindex, nullptr);
        lua_setfield(L, -2, "__newindex");

        lua_pushcfunction(L, property_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    {
        lua_register_rive<ScriptedPropertyColor>(L);

        lua_pushcfunction(L, property_color_index, nullptr);
        lua_setfield(L, -2, "__index");

        lua_pushcfunction(L, property_color_newindex, nullptr);
        lua_setfield(L, -2, "__newindex");

        lua_pushcfunction(L, property_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    {
        lua_register_rive<ScriptedPropertyString>(L);

        lua_pushcfunction(L, property_string_index, nullptr);
        lua_setfield(L, -2, "__index");

        lua_pushcfunction(L, property_string_newindex, nullptr);
        lua_setfield(L, -2, "__newindex");

        lua_pushcfunction(L, property_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    {
        lua_register_rive<ScriptedPropertyBoolean>(L);

        lua_pushcfunction(L, property_boolean_index, nullptr);
        lua_setfield(L, -2, "__index");

        lua_pushcfunction(L, property_boolean_newindex, nullptr);
        lua_setfield(L, -2, "__newindex");

        lua_pushcfunction(L, property_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    {
        lua_register_rive<ScriptedPropertyEnum>(L);

        lua_pushcfunction(L, property_enum_index, nullptr);
        lua_setfield(L, -2, "__index");

        lua_pushcfunction(L, property_enum_newindex, nullptr);
        lua_setfield(L, -2, "__newindex");

        lua_pushcfunction(L, property_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    {
        lua_register_rive<ScriptedPropertyTrigger>(L);

        lua_pushcfunction(L, property_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    {
        lua_register_rive<ScriptedPropertyList>(L);

        lua_pushcfunction(L, property_namecall, nullptr);
        lua_setfield(L, -2, "__namecall");

        lua_pushcfunction(L, property_list_index, nullptr);
        lua_setfield(L, -2, "__index");

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }

    return 0;
}
#endif
