/*****************************************************************************
 * 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.
 *****************************************************************************/

#include "../Diagnostic.h"
#include "../TrackImporter.h"
#include "../core/FileStream.h"
#include "../core/MemoryStream.h"
#include "../core/Path.hpp"
#include "../core/String.hpp"
#include "../object/ObjectRepository.h"
#include "../object/RideObject.h"
#include "../rct12/TD46.h"
#include "../rct2/RCT2.h"
#include "../ride/Ride.h"
#include "../ride/RideData.h"
#include "../ride/TrackDesign.h"
#include "../ride/TrackDesignRepository.h"
#include "../sawyer_coding/SawyerChunkReader.h"

#include <mutex>

using namespace OpenRCT2;
using namespace OpenRCT2::SawyerCoding;

using OpenRCT2::RCT12::TD46MazeElement;
using OpenRCT2::RCT12::TD46TrackElement;
using OpenRCT2::RCT12::TD46Version;

namespace OpenRCT2::RCT2
{
    static std::mutex _objectLookupMutex;

    /**
     * Class to import RollerCoaster Tycoon 2 track designs (*.TD6).
     */
    class TD6Importer final : public ITrackImporter
    {
    private:
        MemoryStream _stream;
        std::string _name;

        void importMazeElements(TrackDesign& td)
        {
            TD46MazeElement t6MazeElement;
            while (true)
            {
                _stream.Read(&t6MazeElement, sizeof(TD46MazeElement));
                if (t6MazeElement.all == 0)
                {
                    break;
                }

                importMazeElement(td, t6MazeElement);
            }
        }

        void importTrackElements(TrackDesign& td)
        {
            TD46TrackElement t6TrackElement{};
            for (uint8_t endFlag = _stream.ReadValue<uint8_t>(); endFlag != 0xFF; endFlag = _stream.ReadValue<uint8_t>())
            {
                _stream.SetPosition(_stream.GetPosition() - 1);
                _stream.Read(&t6TrackElement, sizeof(TD46TrackElement));
                TrackDesignTrackElement trackElement{};

                TrackElemType trackType;
                if (t6TrackElement.type == RCT12::TrackElemType::invertedUp90ToFlatQuarterLoopAlias)
                {
                    trackType = TrackElemType::multiDimInvertedUp90ToFlatQuarterLoop;
                }
                else
                {
                    auto rideType = td.trackAndVehicle.rtdIndex;
                    const bool isFlatRide = GetRideTypeDescriptor(rideType).flags.has(RtdFlag::isFlatRide);
                    trackType = RCT2TrackTypeToOpenRCT2(t6TrackElement.type, rideType, isFlatRide);
                }

                trackElement.type = trackType;
                RCT12::convertFromTD46Flags(trackElement, t6TrackElement.flags, td.version);
                td.trackElements.push_back(trackElement);
            }
        }

        void importTrackElementsTD7(TrackDesign& td)
        {
            TD7TrackElement t7TrackElement{};
            for (uint16_t endFlag = _stream.ReadValue<uint16_t>(); endFlag != 0xFFFF; endFlag = _stream.ReadValue<uint16_t>())
            {
                _stream.SetPosition(_stream.GetPosition() - 2);
                _stream.Read(&t7TrackElement, sizeof(TD7TrackElement));
                TrackDesignTrackElement trackElement{};
                trackElement.type = t7TrackElement.type;
                RCT12::convertFromTD46Flags(trackElement, t7TrackElement.flags, td.version);
                td.trackElements.push_back(trackElement);
            }
        }

        void importEntranceElements(TrackDesign& td)
        {
            TD6EntranceElement t6EntranceElement{};
            for (uint8_t endFlag = _stream.ReadValue<uint8_t>(); endFlag != 0xFF; endFlag = _stream.ReadValue<uint8_t>())
            {
                _stream.SetPosition(_stream.GetPosition() - 1);
                _stream.Read(&t6EntranceElement, sizeof(TD6EntranceElement));
                TrackDesignEntranceElement entranceElement{};
                auto xy = CoordsXY(t6EntranceElement.x, t6EntranceElement.y);
                auto z = (t6EntranceElement.z == -128) ? -1 : t6EntranceElement.z;
                entranceElement.location = TileCoordsXYZD(TileCoordsXY(xy), z, t6EntranceElement.GetDirection());
                entranceElement.isExit = t6EntranceElement.IsExit();
                td.entranceElements.push_back(entranceElement);
            }
        }

        void importSceneryElements(TrackDesign& td)
        {
            for (uint8_t endFlag = _stream.ReadValue<uint8_t>(); endFlag != 0xFF; endFlag = _stream.ReadValue<uint8_t>())
            {
                _stream.SetPosition(_stream.GetPosition() - 1);
                TD6SceneryElement t6SceneryElement{};
                _stream.Read(&t6SceneryElement, sizeof(TD6SceneryElement));
                TrackDesignSceneryElement sceneryElement{};
                sceneryElement.sceneryObject = ObjectEntryDescriptor(t6SceneryElement.SceneryObject);
                TileCoordsXYZ tileCoords = { t6SceneryElement.x, t6SceneryElement.y, t6SceneryElement.z };
                sceneryElement.loc = tileCoords.ToCoordsXYZ();
                sceneryElement.flags = t6SceneryElement.Flags;
                sceneryElement.primaryColour = t6SceneryElement.PrimaryColour;
                sceneryElement.secondaryColour = t6SceneryElement.SecondaryColour;
                if (t6SceneryElement.SceneryObject.GetType() == ObjectType::walls)
                    sceneryElement.tertiaryColour = t6SceneryElement.getTertiaryWallColour();

                td.sceneryElements.push_back(std::move(sceneryElement));
            }
        }

    public:
        TD6Importer()
        {
        }

        bool Load(const utf8* path) override
        {
            const auto extension = Path::GetExtension(path);
            if (String::iequals(extension, ".td6") || String::iequals(extension, ".td7"))
            {
                _name = GetNameFromTrackPath(path);
                auto fs = FileStream(path, FileMode::open);
                return LoadFromStream(&fs);
            }

            throw std::runtime_error("Invalid RCT2 track extension.");
        }

        bool LoadFromStream(IStream* stream) override
        {
            auto chunkReader = SawyerChunkReader(stream);
            auto data = chunkReader.ReadChunkTrack();
            _stream.WriteArray<const uint8_t>(reinterpret_cast<const uint8_t*>(data->GetData()), data->GetLength());
            _stream.SetPosition(0);
            return true;
        }

        std::unique_ptr<TrackDesign> Import() override
        {
            std::unique_ptr<TrackDesign> td = std::make_unique<TrackDesign>();

            TD6Track td6{};
            // Rework td6 so that it is just the fields
            _stream.Read(&td6, 0xA3);

            td->trackAndVehicle.rtdIndex = td6.Type; // 0x00

            td->operation.rideMode = static_cast<RideMode>(td6.RideMode);
            td->appearance.vehicleColourSettings = static_cast<VehicleColourSettings>(td6.VersionAndColourScheme & 0x3);
            for (auto i = 0; i < Limits::kMaxVehicleColours; ++i)
            {
                td->appearance.vehicleColours[i].Body = static_cast<Drawing::Colour>(td6.VehicleColours[i].BodyColour);
                td->appearance.vehicleColours[i].Trim = static_cast<Drawing::Colour>(td6.VehicleColours[i].TrimColour);
                td->appearance.vehicleColours[i].Tertiary = static_cast<Drawing::Colour>(td6.VehicleAdditionalColour[i]);
            }
            td->appearance.stationObjectIdentifier = GetStationIdentifierFromStyle(td6.EntranceStyle);
            td->statistics.totalAirTime = (td6.TotalAirTime * 1024) / 123;
            td->operation.departFlags = td6.DepartFlags;
            td->trackAndVehicle.numberOfTrains = td6.NumberOfTrains;
            td->trackAndVehicle.numberOfCarsPerTrain = td6.NumberOfCarsPerTrain;
            td->operation.minWaitingTime = td6.MinWaitingTime;
            td->operation.maxWaitingTime = td6.MaxWaitingTime;
            td->operation.operationSetting = td6.OperationSetting;
            td->statistics.maxSpeed = td6.MaxSpeed;
            td->statistics.averageSpeed = td6.AverageSpeed;
            td->statistics.rideLength = td6.RideLength;
            td->statistics.maxPositiveVerticalG = td6.MaxPositiveVerticalG * kTD46GForcesMultiplier;
            td->statistics.maxNegativeVerticalG = td6.MaxNegativeVerticalG * kTD46GForcesMultiplier;
            td->statistics.maxLateralG = td6.MaxLateralG * kTD46GForcesMultiplier;

            if (td6.Type == RIDE_TYPE_MINI_GOLF)
                td->statistics.holes = td6.Holes & kRCT12InversionAndHoleMask;
            else
                td->statistics.inversions = td6.Inversions & kRCT12InversionAndHoleMask;

            td->statistics.drops = td6.Drops & kRCT12RideNumDropsMask;
            td->statistics.highestDropHeight = td6.HighestDropHeight;
            td->statistics.ratings.excitement = td6.Excitement * kTD46RatingsMultiplier;
            td->statistics.ratings.intensity = td6.Intensity * kTD46RatingsMultiplier;
            td->statistics.ratings.nausea = td6.Nausea * kTD46RatingsMultiplier;
            td->statistics.upkeepCost = ToMoney64(td6.UpkeepCost);
            for (auto i = 0; i < Limits::kNumColourSchemes; ++i)
            {
                td->appearance.trackColours[i].main = td6.TrackSpineColour[i];
                td->appearance.trackColours[i].additional = td6.TrackRailColour[i];
                td->appearance.trackColours[i].supports = td6.TrackSupportColour[i];
            }
            td->trackAndVehicle.vehicleObject = ObjectEntryDescriptor(td6.VehicleObject);
            td->statistics.spaceRequired = { td6.SpaceRequiredX, td6.SpaceRequiredY };
            td->operation.liftHillSpeed = td6.LiftHillSpeedNumCircuits & 0b00011111;
            td->operation.numCircuits = td6.LiftHillSpeedNumCircuits >> 5;

            auto version = static_cast<TD46Version>(td6.VersionAndColourScheme >> 2);
            if (version != TD46Version::td6 && version != TD46Version::td7)
            {
                LOG_ERROR("Unsupported track design.");
                return nullptr;
            }
            td->version = version;

            td->operation.operationSetting = std::min(
                td->operation.operationSetting, GetRideTypeDescriptor(td->trackAndVehicle.rtdIndex).OperatingSettings.MaxValue);

            const auto& rtd = GetRideTypeDescriptor(td->trackAndVehicle.rtdIndex);
            if (rtd.specialType == RtdSpecialType::maze)
            {
                importMazeElements(*td);
            }
            else
            {
                if (version == TD46Version::td7)
                    importTrackElementsTD7(*td);
                else
                    importTrackElements(*td);

                importEntranceElements(*td);
            }

            importSceneryElements(*td);

            td->gameStateData.name = _name;

            UpdateRideType(td);

            return td;
        }

        void UpdateRideType(std::unique_ptr<TrackDesign>& td)
        {
            if (RCT2RideTypeNeedsConversion(td->trackAndVehicle.rtdIndex))
            {
                std::scoped_lock<std::mutex> lock(_objectLookupMutex);
                auto rawObject = ObjectRepositoryLoadObject(&td->trackAndVehicle.vehicleObject.Entry, false);
                if (rawObject != nullptr)
                {
                    const auto& rideEntry = static_cast<RideObject*>(rawObject.get())->GetEntry();

                    td->trackAndVehicle.rtdIndex = RCT2RideTypeToOpenRCT2RideType(td->trackAndVehicle.rtdIndex, rideEntry);
                    rawObject->Unload();
                }
            }
        }
    };
} // namespace OpenRCT2::RCT2

std::unique_ptr<ITrackImporter> TrackImporter::CreateTD6()
{
    return std::make_unique<RCT2::TD6Importer>();
}
