/*****************************************************************************
 * Copyright (c) 2014-2025 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.
 *****************************************************************************/

#include "TrackDesignAction.h"

#include "../Context.h"
#include "../Diagnostic.h"
#include "../GameState.h"
#include "../config/Config.h"
#include "../management/Finance.h"
#include "../management/Research.h"
#include "../object/ObjectManager.h"
#include "../object/ObjectRepository.h"
#include "../ride/RideConstruction.h"
#include "../ride/TrackDesign.h"
#include "RideCreateAction.h"
#include "RideDemolishAction.h"
#include "RideSetNameAction.h"
#include "RideSetSettingAction.h"
#include "RideSetVehicleAction.h"

using namespace OpenRCT2;

TrackDesignAction::TrackDesignAction(const CoordsXYZD& location, const TrackDesign& td)
    : _loc(location)
    , _td(td)
{
}

void TrackDesignAction::AcceptParameters(GameActionParameterVisitor& visitor)
{
    visitor.Visit(_loc);
    // TODO visit the track design (it has a lot of sub fields)
}

uint16_t TrackDesignAction::GetActionFlags() const
{
    return GameActionBase::GetActionFlags();
}

void TrackDesignAction::Serialise(DataSerialiser& stream)
{
    GameAction::Serialise(stream);

    stream << DS_TAG(_loc);
    _td.Serialise(stream);
}

GameActions::Result TrackDesignAction::Query() const
{
    auto res = GameActions::Result();
    res.Position.x = _loc.x + 16;
    res.Position.y = _loc.y + 16;
    res.Position.z = _loc.z;
    res.Expenditure = ExpenditureType::RideConstruction;

    if (!LocationValid(_loc))
    {
        return GameActions::Result(
            GameActions::Status::InvalidParameters, STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE, STR_OFF_EDGE_OF_MAP);
    }

    auto& gameState = getGameState();
    auto& objManager = GetContext()->GetObjectManager();
    auto entryIndex = objManager.GetLoadedObjectEntryIndex(_td.trackAndVehicle.vehicleObject);
    if (entryIndex == kObjectEntryIndexNull)
    {
        // Force a fallback if the entry is not invented yet a track design of it is selected,
        // which can happen in select-by-track-type mode
        if (!RideEntryIsInvented(entryIndex) && !gameState.cheats.ignoreResearchStatus)
        {
            entryIndex = kObjectEntryIndexNull;
        }
    }

    // Colours do not matter as will be overwritten
    auto rideCreateAction = RideCreateAction(_td.trackAndVehicle.rtdIndex, entryIndex, 0, 0, gameState.lastEntranceStyle);
    rideCreateAction.SetFlags(GetFlags());
    auto r = GameActions::ExecuteNested(&rideCreateAction);
    if (r.Error != GameActions::Status::Ok)
    {
        return GameActions::Result(GameActions::Status::NoFreeElements, STR_CANT_CREATE_NEW_RIDE_ATTRACTION, kStringIdNone);
    }

    const auto rideIndex = r.GetData<RideId>();
    auto ride = GetRide(rideIndex);
    if (ride == nullptr)
    {
        LOG_ERROR("Ride not found for rideIndex %d", rideIndex);
        return GameActions::Result(
            GameActions::Status::Unknown, STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE, STR_ERR_RIDE_NOT_FOUND);
    }

    bool placeScenery = true;

    uint32_t flags = 0;
    if (GetFlags() & GAME_COMMAND_FLAG_GHOST)
        flags |= GAME_COMMAND_FLAG_GHOST;
    if (GetFlags() & GAME_COMMAND_FLAG_REPLAY)
        flags |= GAME_COMMAND_FLAG_REPLAY;

    auto queryRes = TrackDesignPlace(_td, flags, placeScenery, *ride, _loc);
    if (_trackDesignPlaceStateSceneryUnavailable)
    {
        placeScenery = false;
        queryRes = TrackDesignPlace(_td, flags, placeScenery, *ride, _loc);
    }

    auto gameAction = RideDemolishAction(ride->id, RideModifyType::demolish);
    gameAction.SetFlags(GetFlags());

    GameActions::ExecuteNested(&gameAction);

    if (queryRes.Error != GameActions::Status::Ok)
    {
        res.Error = queryRes.Error;
        res.ErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
        res.ErrorMessage = queryRes.ErrorMessage;
        res.ErrorMessageArgs = queryRes.ErrorMessageArgs;
        return res;
    }

    res.Cost = queryRes.Cost;
    res.SetData(RideId{ RideId::GetNull() });

    return res;
}

GameActions::Result TrackDesignAction::Execute() const
{
    auto res = GameActions::Result();
    res.Position.x = _loc.x + 16;
    res.Position.y = _loc.y + 16;
    res.Position.z = _loc.z;
    res.Expenditure = ExpenditureType::RideConstruction;

    auto& gameState = getGameState();
    auto& objManager = GetContext()->GetObjectManager();
    auto entryIndex = objManager.GetLoadedObjectEntryIndex(_td.trackAndVehicle.vehicleObject);
    if (entryIndex != kObjectEntryIndexNull)
    {
        // Force a fallback if the entry is not invented yet a track design using it is selected.
        // This can happen on rides with multiple vehicles where some have been invented and some haven’t.
        if (!RideEntryIsInvented(entryIndex) && !gameState.cheats.ignoreResearchStatus)
        {
            entryIndex = kObjectEntryIndexNull;
        }
    }

    // Colours do not matter as will be overwritten
    auto rideCreateAction = RideCreateAction(_td.trackAndVehicle.rtdIndex, entryIndex, 0, 0, gameState.lastEntranceStyle);
    rideCreateAction.SetFlags(GetFlags());
    auto r = GameActions::ExecuteNested(&rideCreateAction);
    if (r.Error != GameActions::Status::Ok)
    {
        return GameActions::Result(GameActions::Status::NoFreeElements, STR_CANT_CREATE_NEW_RIDE_ATTRACTION, kStringIdNone);
    }

    const auto rideIndex = r.GetData<RideId>();
    auto ride = GetRide(rideIndex);
    if (ride == nullptr)
    {
        LOG_ERROR("Ride not found for rideIndex %d", rideIndex);
        return GameActions::Result(
            GameActions::Status::Unknown, STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE, STR_ERR_RIDE_NOT_FOUND);
    }

    // Query first, this is required again to determine if scenery is available.
    uint32_t flags = GetFlags() & ~GAME_COMMAND_FLAG_APPLY;

    bool placeScenery = true;

    auto queryRes = TrackDesignPlace(_td, flags, placeScenery, *ride, _loc);
    if (_trackDesignPlaceStateSceneryUnavailable)
    {
        placeScenery = false;
        queryRes = TrackDesignPlace(_td, flags, placeScenery, *ride, _loc);
    }

    if (queryRes.Error != GameActions::Status::Ok)
    {
        auto gameAction = RideDemolishAction(ride->id, RideModifyType::demolish);
        gameAction.SetFlags(GetFlags());
        GameActions::ExecuteNested(&gameAction);

        res.Error = queryRes.Error;
        res.ErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
        res.ErrorMessage = queryRes.ErrorMessage;
        res.ErrorMessageArgs = queryRes.ErrorMessageArgs;

        return res;
    }

    // Execute.
    flags |= GAME_COMMAND_FLAG_APPLY;

    auto execRes = TrackDesignPlace(_td, flags, placeScenery, *ride, _loc);
    if (execRes.Error != GameActions::Status::Ok)
    {
        auto gameAction = RideDemolishAction(ride->id, RideModifyType::demolish);
        gameAction.SetFlags(GetFlags());
        GameActions::ExecuteNested(&gameAction);

        res.Error = execRes.Error;
        res.ErrorTitle = STR_RIDE_CONSTRUCTION_CANT_CONSTRUCT_THIS_HERE;
        res.ErrorMessage = execRes.ErrorMessage;
        res.ErrorMessageArgs = execRes.ErrorMessageArgs;

        return res;
    }

    if (entryIndex != kObjectEntryIndexNull)
    {
        auto colour = RideGetUnusedPresetVehicleColour(entryIndex);
        auto rideSetVehicleAction = RideSetVehicleAction(ride->id, RideSetVehicleType::RideEntry, entryIndex, colour);
        GameActions::ExecuteNested(&rideSetVehicleAction);
    }

    SetOperatingSettingNested(ride->id, RideSetSetting::Mode, static_cast<uint8_t>(_td.operation.rideMode), flags);
    auto rideSetVehicleAction2 = RideSetVehicleAction(
        ride->id, RideSetVehicleType::NumTrains, _td.trackAndVehicle.numberOfTrains);
    GameActions::ExecuteNested(&rideSetVehicleAction2);

    auto rideSetVehicleAction3 = RideSetVehicleAction(
        ride->id, RideSetVehicleType::NumCarsPerTrain, _td.trackAndVehicle.numberOfCarsPerTrain);
    GameActions::ExecuteNested(&rideSetVehicleAction3);

    SetOperatingSettingNested(ride->id, RideSetSetting::Departure, _td.operation.departFlags, flags);
    SetOperatingSettingNested(ride->id, RideSetSetting::MinWaitingTime, _td.operation.minWaitingTime, flags);
    SetOperatingSettingNested(ride->id, RideSetSetting::MaxWaitingTime, _td.operation.maxWaitingTime, flags);
    SetOperatingSettingNested(ride->id, RideSetSetting::Operation, _td.operation.operationSetting, flags);
    SetOperatingSettingNested(ride->id, RideSetSetting::LiftHillSpeed, _td.operation.liftHillSpeed, flags);

    auto numCircuits = std::max<uint8_t>(1, _td.operation.numCircuits);
    SetOperatingSettingNested(ride->id, RideSetSetting::NumCircuits, numCircuits, flags);

    uint8_t defaultInspectionInterval = Config::Get().general.DefaultInspectionInterval;
    if (defaultInspectionInterval <= RIDE_INSPECTION_NEVER)
        SetOperatingSettingNested(ride->id, RideSetSetting::InspectionInterval, defaultInspectionInterval, flags);

    ride->lifecycleFlags |= RIDE_LIFECYCLE_NOT_CUSTOM_DESIGN;
    ride->vehicleColourSettings = _td.appearance.vehicleColourSettings;

    ride->entranceStyle = objManager.GetLoadedObjectEntryIndex(_td.appearance.stationObjectIdentifier);
    if (ride->entranceStyle == kObjectEntryIndexNull)
    {
        ride->entranceStyle = gameState.lastEntranceStyle;
    }

    for (size_t i = 0; i < std::min(std::size(ride->trackColours), std::size(_td.appearance.trackColours)); i++)
    {
        ride->trackColours[i] = _td.appearance.trackColours[i];
    }

    for (size_t i = 0; i < Limits::kMaxVehicleColours; i++)
    {
        ride->vehicleColours[i] = _td.appearance.vehicleColours[i];
    }

    for (int32_t count = 1; count == 1 || r.Error != GameActions::Status::Ok; ++count)
    {
        auto name = count == 1 ? _td.gameStateData.name : (_td.gameStateData.name + " " + std::to_string(count));
        auto gameAction = RideSetNameAction(ride->id, name);
        gameAction.SetFlags(GetFlags());
        r = GameActions::ExecuteNested(&gameAction);
    }
    res.Cost = execRes.Cost;
    res.SetData(RideId{ ride->id });

    return res;
}
