#ifdef WITH_RIVE_SCRIPTING
#include "rive/math/vec2d.hpp"
#include "rive/lua/rive_lua_libs.hpp"
#include "rive/math/raw_path.hpp"
#include "rive/math/contour_measure.hpp"
#include "rive/math/path_measure.hpp"
#include "rive/factory.hpp"

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

using namespace rive;

RenderPath* ScriptedPath::renderPath(lua_State* L)
{
    if (m_isRenderPathDirty)
    {
        m_isRenderPathDirty = false;
        if (m_renderFrameId == Artboard::frameId())
        {
            luaL_error(L, "Path was modified between draws in the same frame.");
        }
        m_renderFrameId = Artboard::frameId();
        if (!m_renderPath)
        {
            ScriptingContext* context =
                static_cast<ScriptingContext*>(lua_getthreaddata(L));
            m_renderPath = context->factory()->makeEmptyRenderPath();
            m_renderPath->fillRule(FillRule::clockwise);
        }
        else
        {
            m_renderPath->rewind();
        }
        m_renderPath->addRawPath(rawPath);
    }

    return m_renderPath.get();
}

ScriptedPathData::ScriptedPathData(const RawPath* path)
{
    rawPath.addPath(*path);
}

int ScriptedPathData::totalCommands() { return (int)rawPath.verbs().size(); }

static ScriptedPath* lua_pushpath(lua_State* L)
{
    return lua_newrive<ScriptedPath>(L);
}

static int path_new(lua_State* L)
{
    lua_pushpath(L);
    return 1;
}

static int path_moveTo(lua_State* L)
{
    auto scriptedPath = lua_torive<ScriptedPath>(L, 1);
    auto vec = lua_checkvec2d(L, 2);
    scriptedPath->rawPath.move(*vec);
    scriptedPath->markDirty();
    return 0;
}

static int path_lineTo(lua_State* L)
{
    auto scriptedPath = lua_torive<ScriptedPath>(L, 1);
    auto vec = lua_checkvec2d(L, 2);
    scriptedPath->rawPath.line(*vec);
    scriptedPath->markDirty();
    return 0;
}

static int path_quadTo(lua_State* L)
{
    auto scriptedPath = lua_torive<ScriptedPath>(L, 1);
    auto p1 = lua_checkvec2d(L, 2);
    auto p2 = lua_checkvec2d(L, 3);
    scriptedPath->rawPath.quad(*p1, *p2);
    scriptedPath->markDirty();
    return 0;
}

static int path_cubicTo(lua_State* L)
{
    auto scriptedPath = lua_torive<ScriptedPath>(L, 1);
    auto p1 = lua_checkvec2d(L, 2);
    auto p2 = lua_checkvec2d(L, 3);
    auto p3 = lua_checkvec2d(L, 4);
    scriptedPath->rawPath.cubic(*p1, *p2, *p3);
    scriptedPath->markDirty();
    return 0;
}

static int path_close(lua_State* L)
{
    auto scriptedPath = lua_torive<ScriptedPath>(L, 1);
    scriptedPath->rawPath.close();
    scriptedPath->markDirty();
    return 0;
}

static int path_reset(lua_State* L)
{
    auto scriptedPath = lua_torive<ScriptedPath>(L, 1);
    scriptedPath->rawPath.reset();
    scriptedPath->markDirty();
    return 0;
}

static int path_add(lua_State* L)
{
    auto scriptedPath = (ScriptedPathData*)lua_touserdata(L, 1);
    auto scriptedPathToAdd = (ScriptedPathData*)lua_touserdata(L, 2);
    const Mat2D* transform = nullptr;
    int nargs = lua_gettop(L);
    if (nargs == 3)
    {
        auto matrix = lua_torive<ScriptedMat2D>(L, 3);
        transform = &matrix->value;
    }
    scriptedPath->rawPath.addPath(scriptedPathToAdd->rawPath, transform);
    scriptedPath->markDirty();
    return 0;
}

static int path_command(lua_State* L)
{
    auto scriptedPath = (ScriptedPathData*)lua_touserdata(L, 1);
    auto rawPath = scriptedPath->rawPath;
    auto verbs = rawPath.verbs();
    auto points = rawPath.points();
    auto verbIndex = int(luaL_checknumber(L, 2));
    auto verbName = "none";
    std::vector<Vec2D> verbPoints;

    if (verbIndex >= 0 && verbIndex < (int)verbs.size())
    {
        auto verb = verbs[verbIndex];

        // Calculate the point index for this verb by summing point counts of
        // previous verbs
        int pointIndex = 0;
        for (int i = 0; i < verbIndex; i++)
        {
            pointIndex += path_verb_to_point_count(verbs[i]);
        }

        switch (verb)
        {
            case PathVerb::move:
                verbName = "moveTo";
                if (pointIndex < (int)points.size())
                {
                    verbPoints.push_back(points[pointIndex]);
                }
                break;
            case PathVerb::line:
                verbName = "lineTo";
                if (pointIndex < (int)points.size())
                {
                    verbPoints.push_back(points[pointIndex]);
                }
                break;
            case PathVerb::quad:
                verbName = "quadTo";
                if (pointIndex + 1 < (int)points.size())
                {
                    verbPoints.push_back(points[pointIndex]);
                    verbPoints.push_back(points[pointIndex + 1]);
                }
                break;
            case PathVerb::cubic:
                verbName = "cubicTo";
                if (pointIndex + 2 < (int)points.size())
                {
                    verbPoints.push_back(points[pointIndex]);
                    verbPoints.push_back(points[pointIndex + 1]);
                    verbPoints.push_back(points[pointIndex + 2]);
                }
                break;
            case PathVerb::close:
                verbName = "close";
                // close has no points
                break;
        }
    }
    lua_newrive<ScriptedPathCommand>(L, verbName, verbPoints);
    return 1;
}

static int path_contours(lua_State* L)
{
    auto scriptedPath = (ScriptedPathData*)lua_touserdata(L, 1);
    // Use the copy constructor to ensure ContourMeasure outlives the path
    auto iter = make_rcp<RefCntContourMeasureIter>(scriptedPath->rawPath);
    auto firstContour = iter->get()->next();
    if (firstContour)
    {
        lua_newrive<ScriptedContourMeasure>(L, firstContour, iter);
        return 1;
    }
    lua_pushnil(L);
    return 1;
}

static int path_measure(lua_State* L)
{
    auto scriptedPath = (ScriptedPathData*)lua_touserdata(L, 1);
    PathMeasure pathMeasure(&scriptedPath->rawPath);
    lua_newrive<ScriptedPathMeasure>(L, std::move(pathMeasure));
    return 1;
}

static int path_namecall(lua_State* L)
{
    int atom;
    const char* str = lua_namecallatom(L, &atom);
    if (str != nullptr)
    {
        switch (atom)
        {
            case (int)LuaAtoms::moveTo:
                return path_moveTo(L);
            case (int)LuaAtoms::lineTo:
                return path_lineTo(L);
            case (int)LuaAtoms::quadTo:
                return path_quadTo(L);
            case (int)LuaAtoms::cubicTo:
                return path_cubicTo(L);
            case (int)LuaAtoms::close:
                return path_close(L);
            case (int)LuaAtoms::reset:
                return path_reset(L);
            case (int)LuaAtoms::add:
                return path_add(L);
            case (int)LuaAtoms::contours:
                return path_contours(L);
            case (int)LuaAtoms::measure:
                return path_measure(L);
        }
    }

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

// ContourMeasure methods
static int contour_measure_length(lua_State* L)
{
    auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
    lua_pushnumber(L, scripted->measure()->length());
    return 1;
}

static int contour_measure_isClosed(lua_State* L)
{
    auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
    lua_pushboolean(L, scripted->measure()->isClosed());
    return 1;
}

static int contour_measure_positionAndTangent(lua_State* L)
{
    auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
    float distance = (float)luaL_checknumber(L, 2);
    auto posTan = scripted->measure()->getPosTan(distance);
    lua_pushvec2d(L, posTan.pos);
    lua_pushvec2d(L, posTan.tan);
    return 2;
}

static int contour_measure_warp(lua_State* L)
{
    auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
    auto src = lua_checkvec2d(L, 2);
    Vec2D result = scripted->measure()->warp(*src);
    lua_pushvec2d(L, result);
    return 1;
}

static int contour_measure_extract(lua_State* L)
{
    auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
    float startDistance = (float)luaL_checknumber(L, 2);
    float endDistance = (float)luaL_checknumber(L, 3);
    auto destPath = lua_torive<ScriptedPath>(L, 4);
    bool startWithMove = lua_isboolean(L, 5) ? lua_toboolean(L, 5) : true;
    scripted->measure()->getSegment(startDistance,
                                    endDistance,
                                    &destPath->rawPath,
                                    startWithMove);
    destPath->markDirty();
    return 0;
}

static int contour_measure_next(lua_State* L)
{
    auto scripted = lua_torive<ScriptedContourMeasure>(L, 1);
    auto iter = scripted->iter();
    if (iter)
    {
        auto nextContour = iter->get()->next();
        if (nextContour)
        {
            // Create new ScriptedContourMeasure with the same rcp iter
            // The iter is already advanced, so we can reuse it
            lua_newrive<ScriptedContourMeasure>(L, nextContour, iter);
            return 1;
        }
    }
    lua_pushnil(L);
    return 1;
}

static int path_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);

    // If it's not a string/atom, treat it as a numeric index
    if (!key)
    {
        int index = luaL_checkinteger(L, 2);
        // Convert from 1-based Lua index to 0-based C++ index
        // Replace the index at position 2 with the 0-based version
        lua_pushinteger(L, index - 1); // Push 0-based index
        lua_replace(L, 2);             // Replace the value at position 2
        // Now stack has: 1=path, 2=0-based index, which is what path_command
        // expects
        return path_command(L);
    }

    // String indices are handled by __namecall for methods
    return 0;
}

static int path_length(lua_State* L)
{
    auto scriptedPath = (ScriptedPathData*)lua_touserdata(L, 1);

    auto totalCommands = scriptedPath->totalCommands();
    lua_pushnumber(L, totalCommands);
    return 1;
}

static int contour_measure_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;
    }

    switch (atom)
    {
        case (int)LuaAtoms::length:
            return contour_measure_length(L);
        case (int)LuaAtoms::isClosed:
            return contour_measure_isClosed(L);
        case (int)LuaAtoms::next:
            return contour_measure_next(L);
        default:
            return 0;
    }
}

static int contour_measure_namecall(lua_State* L)
{
    int atom;
    const char* str = lua_namecallatom(L, &atom);
    if (str != nullptr)
    {
        switch (atom)
        {
            case (int)LuaAtoms::positionAndTangent:
                return contour_measure_positionAndTangent(L);
            case (int)LuaAtoms::warp:
                return contour_measure_warp(L);
            case (int)LuaAtoms::extract:
                return contour_measure_extract(L);
        }
    }

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

// PathMeasure methods
static int path_measure_length(lua_State* L)
{
    auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
    lua_pushnumber(L, scripted->measure()->length());
    return 1;
}

static int path_measure_isClosed(lua_State* L)
{
    auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
    lua_pushboolean(L, scripted->measure()->isClosed());
    return 1;
}

static int path_measure_positionAndTangent(lua_State* L)
{
    auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
    float distance = (float)luaL_checknumber(L, 2);
    auto posTanDist = scripted->measure()->atDistance(distance);
    lua_pushvec2d(L, posTanDist.pos);
    lua_pushvec2d(L, posTanDist.tan);
    return 2;
}

static int path_measure_warp(lua_State* L)
{
    auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
    auto src = lua_checkvec2d(L, 2);
    // Use atDistance to get position and tangent, then apply warp formula
    auto posTanDist = scripted->measure()->atDistance(src->x);
    Vec2D result = {
        posTanDist.pos.x - posTanDist.tan.y * src->y,
        posTanDist.pos.y + posTanDist.tan.x * src->y,
    };
    lua_pushvec2d(L, result);
    return 1;
}

static int path_measure_extract(lua_State* L)
{
    auto scripted = lua_torive<ScriptedPathMeasure>(L, 1);
    float startDistance = (float)luaL_checknumber(L, 2);
    float endDistance = (float)luaL_checknumber(L, 3);
    auto destPath = lua_torive<ScriptedPath>(L, 4);
    bool startWithMove = lua_isboolean(L, 5) ? lua_toboolean(L, 5) : true;
    scripted->measure()->getSegment(startDistance,
                                    endDistance,
                                    &destPath->rawPath,
                                    startWithMove);
    destPath->markDirty();
    return 0;
}

static int path_measure_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;
    }

    switch (atom)
    {
        case (int)LuaAtoms::length:
            return path_measure_length(L);
        case (int)LuaAtoms::isClosed:
            return path_measure_isClosed(L);
        default:
            return 0;
    }
}

static int path_measure_namecall(lua_State* L)
{
    int atom;
    const char* str = lua_namecallatom(L, &atom);
    if (str != nullptr)
    {
        switch (atom)
        {
            case (int)LuaAtoms::positionAndTangent:
                return path_measure_positionAndTangent(L);
            case (int)LuaAtoms::warp:
                return path_measure_warp(L);
            case (int)LuaAtoms::extract:
                return path_measure_extract(L);
        }
    }

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

static int property_namecall_atom(lua_State* L,
                                  ScriptedPathCommand* pathCommand,
                                  uint8_t tag,
                                  int atom,
                                  bool& error)
{
    switch (atom)
    {
        case (int)LuaAtoms::type:
        {
            lua_pushstring(L, pathCommand->type().c_str());
            return 1;
        }
    }
    error = true;
    return 0;
}

static int pathCommand_index(lua_State* L)
{
    int atom;
    const char* key = lua_tostringatom(L, 2, &atom);

    // If it's not a string/atom, treat it as a numeric index
    if (!key)
    {
        int luaIndex = luaL_checkinteger(L, 2);
        int index = luaIndex - 1;

        auto pathCommand = (ScriptedPathCommand*)lua_touserdata(L, 1);
        auto points = pathCommand->points();
        auto size = (int)points.size();
        if (index >= 0 && index < size)
        {
            auto point = points[index];
            lua_pushvec2d(L, point);
            return 1;
        }
    }
    else
    {
        int atom;
        lua_tostringatom(L, 2, &atom);

        size_t namelen = 0;
        const char* name = luaL_checklstring(L, 2, &namelen);

        auto tag = lua_userdatatag(L, 1);
        auto pathCommand = (ScriptedPathCommand*)lua_touserdata(L, 1);

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

        luaL_error(L, "'%s' is not a valid index of PathCommand", name);
        return 0;
    }
    return 0;
}

static int pathCommand_length(lua_State* L)
{
    auto pathCommand = (ScriptedPathCommand*)lua_touserdata(L, 1);

    auto points = pathCommand->points();
    lua_pushnumber(L, (int)points.size());
    return 1;
}

static int pathCommand_namecall(lua_State* L)
{
    int atom;
    const char* str = lua_namecallatom(L, &atom);

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

static const luaL_Reg pathStaticMethods[] = {
    {"new", path_new},
    {NULL, NULL},
};

int luaopen_rive_path(lua_State* L)
{
    {
        lua_register_rive<ScriptedPathData>(L);

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

        lua_pushcfunction(L, path_length, nullptr);
        lua_setfield(L, -2, "__len");

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

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }
    {
        luaL_register(L, ScriptedPath::luaName, pathStaticMethods);
        lua_register_rive<ScriptedPath>(L);

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

        lua_pushcfunction(L, path_length, nullptr);
        lua_setfield(L, -2, "__len");

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

        lua_setreadonly(L, -1, true);
        lua_pop(L, 1); // pop the metatable
    }
    {
        lua_register_rive<ScriptedPathCommand>(L);

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

        lua_pushcfunction(L, pathCommand_length, nullptr);
        lua_setfield(L, -2, "__len");

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

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

    // Register ContourMeasure
    lua_register_rive<ScriptedContourMeasure>(L);
    lua_pushcfunction(L, contour_measure_index, nullptr);
    lua_setfield(L, -2, "__index");
    lua_pushcfunction(L, contour_measure_namecall, nullptr);
    lua_setfield(L, -2, "__namecall");
    lua_setreadonly(L, -1, true);
    lua_pop(L, 1); // pop the metatable

    // Register PathMeasure
    lua_register_rive<ScriptedPathMeasure>(L);
    lua_pushcfunction(L, path_measure_index, nullptr);
    lua_setfield(L, -2, "__index");
    lua_pushcfunction(L, path_measure_namecall, nullptr);
    lua_setfield(L, -2, "__namecall");
    lua_setreadonly(L, -1, true);
    lua_pop(L, 1); // pop the metatable

    return 1;
}

#endif
