/*****************************************************************************
 * Copyright (c) 2014-2026 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#ifdef ENABLE_SCRIPTING

    #include "ScStaff.hpp"

    #include "../../../Context.h"
    #include "../../../entity/PatrolArea.h"
    #include "../../../entity/Staff.h"
    #include "../../../object/ObjectManager.h"
    #include "../../../object/PeepAnimationsObject.h"
    #include "../../../peep/PeepAnimations.h"
    #include "../../../rct12/RCT12.h"
    #include "../../../world/Map.h"

namespace OpenRCT2::Scripting
{
    ScStaff gScStaff;

    JSValue ScStaff::New(JSContext* ctx, EntityId entityId)
    {
        return gScEntity.NewDerivedInstance(ctx, entityId, gScStaff.GetProto());
    }

    void ScStaff::Register(JSContext* ctx)
    {
        static constexpr JSCFunctionListEntry funcs[] = {
            JS_CGETSET_DEF("staffType", &ScStaff::staffType_get, &ScStaff::staffType_set),
            JS_CGETSET_DEF("colour", &ScStaff::colour_get, &ScStaff::colour_set),
            JS_CGETSET_DEF("availableCostumes", &ScStaff::availableCostumes_get, nullptr),
            JS_CGETSET_DEF("costume", &ScStaff::costume_get, &ScStaff::costume_set),
            JS_CGETSET_DEF("patrolArea", &ScStaff::patrolArea_get, nullptr),
            JS_CGETSET_DEF("orders", &ScStaff::orders_get, &ScStaff::orders_set),
            JS_CGETSET_DEF("availableAnimations", &ScStaff::availableAnimations_get, nullptr),
            JS_CGETSET_DEF("animation", &ScStaff::animation_get, &ScStaff::animation_set),
            JS_CGETSET_DEF("animationOffset", &ScStaff::animationOffset_get, &ScStaff::animationOffset_set),
            JS_CGETSET_DEF("animationLength", &ScStaff::animationLength_get, nullptr),
            JS_CFUNC_DEF("getAnimationSpriteIds", 2, &ScStaff::getAnimationSpriteIds),
            JS_CFUNC_DEF("getCostumeStrings", 0, &ScStaff::getCostumeStrings)
        };
        gScStaff.RegisterDerived(ctx, gScPeep, funcs);
    }

    Staff* ScStaff::GetStaff(JSValue thisVal)
    {
        auto id = GetEntityId(thisVal);
        return getGameState().entities.getEntity<Staff>(id);
    }

    JSValue ScStaff::staffType_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep != nullptr)
        {
            switch (peep->assignedStaffType)
            {
                case StaffType::handyman:
                    return JSFromStdString(ctx, "handyman");
                case StaffType::mechanic:
                    return JSFromStdString(ctx, "mechanic");
                case StaffType::security:
                    return JSFromStdString(ctx, "security");
                case StaffType::entertainer:
                    return JSFromStdString(ctx, "entertainer");
                case StaffType::count:
                    break;
            }
        }
        return JS_UNDEFINED;
    }

    JSValue ScStaff::staffType_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_STR(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetStaff(thisVal);
        if (peep != nullptr)
        {
            if (value == "handyman" && peep->assignedStaffType != StaffType::handyman)
            {
                peep->assignedStaffType = StaffType::handyman;
                peep->animationObjectIndex = findPeepAnimationsIndexForType(AnimationPeepType::handyman);
                peep->animationGroup = PeepAnimationGroup::normal;
            }
            else if (value == "mechanic" && !peep->isMechanic())
            {
                peep->assignedStaffType = StaffType::mechanic;
                peep->animationObjectIndex = findPeepAnimationsIndexForType(AnimationPeepType::mechanic);
                peep->animationGroup = PeepAnimationGroup::normal;
            }
            else if (value == "security" && peep->assignedStaffType != StaffType::security)
            {
                peep->assignedStaffType = StaffType::security;
                peep->animationObjectIndex = findPeepAnimationsIndexForType(AnimationPeepType::security);
                peep->animationGroup = PeepAnimationGroup::normal;
            }
            else if (value == "entertainer" && !peep->isEntertainer())
            {
                peep->assignedStaffType = StaffType::entertainer;
                peep->animationObjectIndex = findPeepAnimationsIndexForType(AnimationPeepType::entertainer);
                peep->animationGroup = PeepAnimationGroup::normal;
            }

            // Reset state to walking to prevent invalid actions from carrying over
            peep->action = PeepActionType::walking;
            peep->animationType = peep->nextAnimationType = PeepAnimationType::walking;
            peep->invalidate();
        }
        return JS_UNDEFINED;
    }

    JSValue ScStaff::colour_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? EnumValue(peep->tShirtColour) : 0);
    }

    JSValue ScStaff::colour_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetStaff(thisVal);
        if (peep != nullptr)
        {
            peep->tShirtColour = static_cast<Drawing::Colour>(value);
            peep->invalidate();
        }
        return JS_UNDEFINED;
    }

    static const std::vector<AnimationGroupResult> costumesByStaffType(StaffType staffType)
    {
        // TODO: shouldn't get hit repeatedly, but cache these if (and only if) it's too slow
        auto animPeepType = getAnimationPeepType(staffType);
        return getAnimationGroupsByPeepType(animPeepType);
    }

    JSValue ScStaff::availableCostumes_get(JSContext* ctx, JSValue thisVal)
    {
        JSValue availableCostumes = JS_NewArray(ctx);
        auto peep = GetStaff(thisVal);
        if (peep != nullptr)
        {
            auto idx = 0;
            for (auto& costume : costumesByStaffType(peep->assignedStaffType))
            {
                JS_SetPropertyInt64(ctx, availableCostumes, idx++, JSFromStdString(ctx, costume.scriptName));
            }
        }
        return availableCostumes;
    }

    JSValue ScStaff::getCostumeStrings(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        auto peep = GetStaff(thisVal);
        auto animPeepType = getAnimationPeepType(peep->assignedStaffType);

        JSValue availableCostumes = JS_NewArray(ctx);
        auto idx = 0;

        for (auto& costume : getAvailableCostumeStrings(animPeepType))
        {
            JS_SetPropertyInt64(ctx, availableCostumes, idx++, JSFromStdString(ctx, costume.friendlyName));
        }
        return availableCostumes;
    }

    JSValue ScStaff::costume_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep == nullptr)
        {
            return JS_UNDEFINED;
        }

        auto& costumes = costumesByStaffType(peep->assignedStaffType);

        auto costume = std::find_if(costumes.begin(), costumes.end(), [peep](auto& candidate) {
            return candidate.objectId == peep->animationObjectIndex;
        });

        if (costume != costumes.end())
        {
            return JSFromStdString(ctx, costume->scriptName);
        }
        else
            return JS_UNDEFINED;
    }

    JSValue ScStaff::costume_set(JSContext* ctx, JSValue thisVal, JSValue value)
    {
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();

        auto peep = GetStaff(thisVal);
        if (peep == nullptr)
        {
            return JS_UNDEFINED;
        }

        auto& costumes = costumesByStaffType(peep->assignedStaffType);
        auto costume = costumes.end();

        // Split by type passed so as to not break old plugins
        if (JS_IsString(value))
        {
            JS_UNPACK_STR(valueString, ctx, value);
            costume = std::find_if(costumes.begin(), costumes.end(), [valueString](auto& candidate) {
                return candidate.scriptName == valueString;
            });
        }
        else if (JS_IsNumber(value))
        {
            JS_UNPACK_UINT32(number, ctx, value);
            auto target = RCT12PeepAnimationGroup(number + EnumValue(RCT12PeepAnimationGroup::entertainerPanda));
            costume = std::find_if(
                costumes.begin(), costumes.end(), [target](auto& candidate) { return candidate.legacyPosition == target; });
        }

        if (costume == costumes.end())
        {
            JS_ThrowPlainError(ctx, "Invalid costume for this staff member");
            return JS_EXCEPTION;
        }

        peep->animationObjectIndex = costume->objectId;
        peep->animationGroup = costume->group;
        peep->invalidate();
        return JS_UNDEFINED;
    }

    JSValue ScStaff::patrolArea_get(JSContext* ctx, JSValue thisVal)
    {
        auto staffId = GetEntityId(thisVal);
        return gScPatrolArea.New(ctx, staffId);
    }

    JSValue ScStaff::orders_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->staffOrders : 0);
    }

    JSValue ScStaff::orders_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetStaff(thisVal);
        if (peep != nullptr)
        {
            peep->staffOrders = value;
        }
        return JS_UNDEFINED;
    }

    EnumMap<PeepAnimationType> ScStaff::animationsByStaffType(StaffType staffType)
    {
        AnimationPeepType animPeepType{};
        switch (staffType)
        {
            case StaffType::handyman:
                animPeepType = AnimationPeepType::handyman;
                break;
            case StaffType::mechanic:
                animPeepType = AnimationPeepType::mechanic;
                break;
            case StaffType::security:
                animPeepType = AnimationPeepType::security;
                break;
            case StaffType::entertainer:
            default:
                animPeepType = AnimationPeepType::entertainer;
        }
        return getAnimationsByPeepType(animPeepType);
    }

    JSValue ScStaff::availableAnimations_get(JSContext* ctx, JSValue thisVal)
    {
        JSValue availableAnimations = JS_NewArray(ctx);

        auto* peep = GetStaff(thisVal);
        if (peep != nullptr)
        {
            auto idx = 0;
            for (auto& animation : animationsByStaffType(peep->assignedStaffType))
            {
                JS_SetPropertyInt64(ctx, availableAnimations, idx++, JSFromStdString(ctx, animation.first));
            }
        }

        return availableAnimations;
    }

    JSValue ScStaff::getAnimationSpriteIds(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_UNPACK_STR(groupKey, ctx, argv[0]);
        JS_UNPACK_UINT32(rotation, ctx, argv[1]);
        JSValue spriteIds = JS_NewArray(ctx);

        auto* peep = GetStaff(thisVal);
        if (peep == nullptr)
        {
            return spriteIds;
        }

        auto animationGroups = animationsByStaffType(peep->assignedStaffType);
        auto animationType = animationGroups.TryGet(groupKey);
        if (animationType == std::nullopt)
        {
            return spriteIds;
        }

        auto& objManager = GetContext()->GetObjectManager();
        auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(peep->animationObjectIndex);

        const auto& animationGroup = animObj->GetPeepAnimation(peep->animationGroup, *animationType);
        auto idx = 0;
        for (auto frameOffset : animationGroup.frameOffsets)
        {
            auto imageId = animationGroup.baseImage;
            if (animationType != PeepAnimationType::hanging)
                imageId += rotation + frameOffset * 4;
            else
                imageId += frameOffset;

            JS_SetPropertyInt64(ctx, spriteIds, idx++, JS_NewUint32(ctx, imageId));
        }

        return spriteIds;
    }

    JSValue ScStaff::animation_get(JSContext* ctx, JSValue thisVal)
    {
        auto* peep = GetStaff(thisVal);
        if (peep == nullptr)
        {
            return JSFromStdString(ctx, "");
        }

        auto animationGroups = animationsByStaffType(peep->assignedStaffType);
        return JSFromStdString(ctx, animationGroups[peep->animationType]);
    }

    JSValue ScStaff::animation_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_STR(groupKey, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();

        auto* peep = GetStaff(thisVal);
        auto animationGroups = animationsByStaffType(peep->assignedStaffType);
        auto newType = animationGroups.TryGet(groupKey);
        if (newType == std::nullopt)
        {
            JS_ThrowPlainError(ctx, "Invalid animation for this staff member (%s)", groupKey.data());
            return JS_EXCEPTION;
        }

        peep->animationType = peep->nextAnimationType = *newType;

        auto offset = 0;
        if (peep->isActionWalking())
            peep->walkingAnimationFrameNum = offset;
        else
            peep->animationFrameNum = offset;

        auto& objManager = GetContext()->GetObjectManager();
        auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(peep->animationObjectIndex);

        const auto& animationGroup = animObj->GetPeepAnimation(peep->animationGroup, peep->animationType);
        peep->animationImageIdOffset = animationGroup.frameOffsets[offset];
        peep->invalidate();
        peep->updateSpriteBoundingBox();
        peep->invalidate();
        return JS_UNDEFINED;
    }

    JSValue ScStaff::animationOffset_get(JSContext* ctx, JSValue thisVal)
    {
        auto* peep = GetStaff(thisVal);
        if (peep == nullptr)
        {
            return JS_NewUint32(ctx, 0);
        }

        auto frame = peep->isActionWalking() ? peep->walkingAnimationFrameNum : peep->animationFrameNum;
        return JS_NewUint32(ctx, frame);
    }

    JSValue ScStaff::animationOffset_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(offset, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();

        auto* peep = GetStaff(thisVal);

        auto& objManager = GetContext()->GetObjectManager();
        auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(peep->animationObjectIndex);

        const auto& animationGroup = animObj->GetPeepAnimation(peep->animationGroup, peep->animationType);
        auto length = animationGroup.frameOffsets.size();
        offset %= length;

        if (peep->isActionWalking())
            peep->walkingAnimationFrameNum = offset;
        else
            peep->animationFrameNum = offset;

        peep->animationImageIdOffset = animationGroup.frameOffsets[offset];
        peep->invalidate();
        peep->updateSpriteBoundingBox();
        peep->invalidate();
        return JS_UNDEFINED;
    }

    JSValue ScStaff::animationLength_get(JSContext* ctx, JSValue thisVal)
    {
        auto* peep = GetStaff(thisVal);
        if (peep == nullptr)
        {
            return JS_NewUint32(ctx, 0);
        }

        auto& objManager = GetContext()->GetObjectManager();
        auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(peep->animationObjectIndex);

        const auto& animationGroup = animObj->GetPeepAnimation(peep->animationGroup, peep->animationType);
        auto length = static_cast<uint8_t>(animationGroup.frameOffsets.size());
        return JS_NewUint32(ctx, length);
    }

    ScHandyman gScHandyman;

    JSValue ScHandyman::New(JSContext* ctx, EntityId entityId)
    {
        return gScEntity.NewDerivedInstance(ctx, entityId, gScHandyman.GetProto());
    }

    void ScHandyman::Register(JSContext* ctx)
    {
        static constexpr JSCFunctionListEntry funcs[] = {
            JS_CGETSET_DEF("lawnsMown", &ScHandyman::lawnsMown_get, nullptr),
            JS_CGETSET_DEF("gardensWatered", &ScHandyman::gardensWatered_get, nullptr),
            JS_CGETSET_DEF("litterSwept", &ScHandyman::litterSwept_get, nullptr),
            JS_CGETSET_DEF("binsEmptied", &ScHandyman::binsEmptied_get, nullptr),
        };
        gScHandyman.RegisterDerived(ctx, gScStaff, funcs);
    }

    JSValue ScHandyman::lawnsMown_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep != nullptr && peep->assignedStaffType == StaffType::handyman)
        {
            return JS_NewUint32(ctx, peep->staffLawnsMown);
        }
        else
        {
            return JS_NULL;
        }
    }

    JSValue ScHandyman::gardensWatered_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep != nullptr && peep->assignedStaffType == StaffType::handyman)
        {
            return JS_NewUint32(ctx, peep->staffGardensWatered);
        }
        else
        {
            return JS_NULL;
        }
    }

    JSValue ScHandyman::litterSwept_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep != nullptr && peep->assignedStaffType == StaffType::handyman)
        {
            return JS_NewUint32(ctx, peep->staffLitterSwept);
        }
        else
        {
            return JS_NULL;
        }
    }

    JSValue ScHandyman::binsEmptied_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep != nullptr && peep->assignedStaffType == StaffType::handyman)
        {
            return JS_NewUint32(ctx, peep->staffBinsEmptied);
        }
        else
        {
            return JS_NULL;
        }
    }

    ScMechanic gScMechanic;

    JSValue ScMechanic::New(JSContext* ctx, EntityId entityId)
    {
        return gScEntity.NewDerivedInstance(ctx, entityId, gScMechanic.GetProto());
    }

    void ScMechanic::Register(JSContext* ctx)
    {
        static constexpr JSCFunctionListEntry funcs[] = {
            JS_CGETSET_DEF("ridesFixed", &ScMechanic::ridesFixed_get, nullptr),
            JS_CGETSET_DEF("ridesInspected", &ScMechanic::ridesInspected_get, nullptr),
        };
        gScMechanic.RegisterDerived(ctx, gScStaff, funcs);
    }

    JSValue ScMechanic::ridesFixed_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep != nullptr && peep->isMechanic())
        {
            return JS_NewUint32(ctx, peep->staffRidesFixed);
        }
        else
        {
            return JS_NULL;
        }
    }

    JSValue ScMechanic::ridesInspected_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep != nullptr && peep->isMechanic())
        {
            return JS_NewUint32(ctx, peep->staffRidesInspected);
        }
        else
        {
            return JS_NULL;
        }
    }

    ScSecurity gScSecurity;

    JSValue ScSecurity::New(JSContext* ctx, EntityId entityId)
    {
        return gScEntity.NewDerivedInstance(ctx, entityId, gScSecurity.GetProto());
    }

    void ScSecurity::Register(JSContext* ctx)
    {
        static constexpr JSCFunctionListEntry funcs[] = {
            JS_CGETSET_DEF("vandalsStopped", &ScSecurity::vandalsStopped_get, nullptr),
        };
        gScSecurity.RegisterDerived(ctx, gScStaff, funcs);
    }

    JSValue ScSecurity::vandalsStopped_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetStaff(thisVal);
        if (peep != nullptr && peep->assignedStaffType == StaffType::security)
        {
            return JS_NewUint32(ctx, peep->staffVandalsStopped);
        }
        else
        {
            return JS_NULL;
        }
    }

    using OpaquePatrolAreaData = struct
    {
        EntityId staffId;
    };

    JSValue ScPatrolArea::New(JSContext* ctx, EntityId staffId)
    {
        return MakeWithOpaque(ctx, new OpaquePatrolAreaData{ staffId });
    }

    void ScPatrolArea::Register(JSContext* ctx)
    {
        static constexpr JSCFunctionListEntry funcs[] = {
            JS_CGETSET_DEF("tiles", &ScPatrolArea::tiles_get, &ScPatrolArea::tiles_set),
            JS_CFUNC_DEF("clear", 0, &ScPatrolArea::clear),
            JS_CFUNC_DEF("add", 1, &ScPatrolArea::add),
            JS_CFUNC_DEF("remove", 1, &ScPatrolArea::remove),
            JS_CFUNC_DEF("contains", 1, &ScPatrolArea::contains),
        };
        RegisterBase(ctx, "PatrolArea", Finalize, funcs);
    }

    void ScPatrolArea::Finalize(JSRuntime* rt, JSValue thisVal)
    {
        OpaquePatrolAreaData* data = gScPatrolArea.GetOpaque<OpaquePatrolAreaData*>(thisVal);
        if (data)
            delete data;
    }

    Staff* ScPatrolArea::GetStaff(JSValue thisVal)
    {
        OpaquePatrolAreaData* data = gScPatrolArea.GetOpaque<OpaquePatrolAreaData*>(thisVal);
        return getGameState().entities.getEntity<Staff>(data->staffId);
    }

    void ScPatrolArea::ModifyArea(JSContext* ctx, JSValue thisVal, JSValue coordsOrRange, bool reset)
    {
        auto staff = GetStaff(thisVal);
        if (staff != nullptr)
        {
            if (JS_IsArray(coordsOrRange))
            {
                JSIterateArray(ctx, coordsOrRange, [staff, reset](JSContext* ctx2, JSValue v) {
                    auto coord = JSToCoordsXY(ctx2, v);
                    staff->setPatrolArea(coord, reset);
                    MapInvalidateTileFull(coord);
                });
            }
            else
            {
                MapRange mapRange = { JSToCoordXY(ctx, coordsOrRange, "leftTop"),
                                      JSToCoordXY(ctx, coordsOrRange, "rightBottom") };
                for (int32_t y = mapRange.GetY1(); y <= mapRange.GetY2(); y += kCoordsXYStep)
                {
                    for (int32_t x = mapRange.GetX1(); x <= mapRange.GetX2(); x += kCoordsXYStep)
                    {
                        CoordsXY coord(x, y);
                        staff->setPatrolArea(coord, reset);
                        MapInvalidateTileFull(coord);
                    }
                }
            }
            UpdateConsolidatedPatrolAreas();
        }
    }

    JSValue ScPatrolArea::tiles_get(JSContext* ctx, JSValue thisVal)
    {
        auto array = JS_NewArray(ctx);

        auto staff = GetStaff(thisVal);
        if (staff != nullptr && staff->patrolInfo != nullptr)
        {
            auto tiles = staff->patrolInfo->ToVector();

            auto index = 0;
            for (const auto& tile : tiles)
            {
                auto coords = ToJSValue(ctx, tile);
                JS_SetPropertyInt64(ctx, array, index, coords);
                index++;
            }
        }

        return array;
    }

    JSValue ScPatrolArea::tiles_set(JSContext* ctx, JSValue thisVal, JSValue value)
    {
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();

        auto staff = GetStaff(thisVal);
        if (staff != nullptr)
        {
            staff->clearPatrolArea();
            if (JS_IsArray(value))
            {
                ModifyArea(ctx, thisVal, value, true);
            }
        }
        return JS_UNDEFINED;
    }

    JSValue ScPatrolArea::clear(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();

        auto staff = GetStaff(thisVal);
        if (staff != nullptr)
        {
            staff->clearPatrolArea();
            UpdateConsolidatedPatrolAreas();
        }
        return JS_UNDEFINED;
    }

    JSValue ScPatrolArea::add(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_UNPACK_OBJECT(coordsOrRange, ctx, argv[0])
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        ModifyArea(ctx, thisVal, coordsOrRange, true);
        return JS_UNDEFINED;
    }

    JSValue ScPatrolArea::remove(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_UNPACK_OBJECT(coordsOrRange, ctx, argv[0])
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        ModifyArea(ctx, thisVal, coordsOrRange, false);
        return JS_UNDEFINED;
    }

    JSValue ScPatrolArea::contains(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_UNPACK_OBJECT(coord, ctx, argv[0])
        auto staff = GetStaff(thisVal);
        if (staff != nullptr)
        {
            auto pos = JSToCoordsXY(ctx, coord);
            return JS_NewBool(ctx, staff->isLocationInPatrol(pos));
        }
        return JS_NewBool(ctx, false);
    }

} // namespace OpenRCT2::Scripting

#endif
