
#include "cbase.h"

#include "vjolt_layers.h"

#include "vjolt_controller_vehicle.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

//------------------------------------------------------------------------------------------------

static ConVar vjolt_vehicle_wheel_debug( "vjolt_vehicle_wheel_debug", "0", FCVAR_CHEAT );

static ConVar vjolt_vehicle_throttle_opposition_limit( "vjolt_vehicle_throttle_opposition_limit", "5", FCVAR_NONE,
	"Below what speed should we be attempting to drive/climb with handbrake on to avoid falling down." );

//------------------------------------------------------------------------------------------------

static const JPH::Vec3 VehicleUpVector		= JPH::Vec3( 0, 0, 1 );
static const JPH::Vec3 VehicleForwardVector	= JPH::Vec3( 0, 1, 0 );

static const char *VehicleTypeToName( unsigned int VehicleType )
{
	switch ( VehicleType )
	{
		case VEHICLE_TYPE_CAR_WHEELS:		return "Car Wheels";
		case VEHICLE_TYPE_CAR_RAYCAST:		return "Car Raycast";
		case VEHICLE_TYPE_JETSKI_RAYCAST:	return "Jetski Raycast";
		case VEHICLE_TYPE_AIRBOAT_RAYCAST:	return "Airboat Raycast";
		default:							return "Unknown";
	}
}

JPH::Ref< JPH::VehicleCollisionTester > CreateVehicleCollisionTester( unsigned int VehicleType, float LargestWheelRadius )
{
	switch ( VehicleType )
	{
	default:
		Log_Warning( LOG_VJolt, "Don't know how to make vehicle type: %s (%u).\n", VehicleTypeToName( VehicleType ), VehicleType );
		[[ fallthrough ]];
	case VEHICLE_TYPE_CAR_WHEELS:
		return new JPH::VehicleCollisionTesterCastSphere( Layers::MOVING, LargestWheelRadius, VehicleUpVector );
	}
}

//------------------------------------------------------------------------------------------------

JoltPhysicsVehicleController::JoltPhysicsVehicleController( JoltPhysicsEnvironment* pEnvironment, JPH::PhysicsSystem* pPhysicsSystem, JoltPhysicsObject* pVehicleBodyObject, const vehicleparams_t& params, unsigned int nVehicleType, IPhysicsGameTrace* pGameTrace )
	: m_pEnvironment( pEnvironment )
	, m_pPhysicsSystem( pPhysicsSystem )
	, m_pCarBodyObject( pVehicleBodyObject )
	, m_VehicleType( nVehicleType )
	, m_VehicleParams( params )
{
	JPH::VehicleConstraintSettings vehicle;
	vehicle.mUp					= VehicleUpVector;
	vehicle.mForward			= VehicleForwardVector;
	vehicle.mDrawConstraintSize = 0.1f;
	CreateWheels( vehicle );
	vehicle.mController			= CreateVehicleController();

	m_Tester = CreateVehicleCollisionTester( nVehicleType, m_InternalState.LargestWheelRadius );

	m_pCarBodyObject->AddDestroyedListener( this );
	m_VehicleConstraint = new JPH::VehicleConstraint( *m_pCarBodyObject->GetBody(), vehicle );
	m_pPhysicsSystem->AddConstraint( m_VehicleConstraint );
	m_pPhysicsSystem->AddStepListener( m_VehicleConstraint );
}

JoltPhysicsVehicleController::~JoltPhysicsVehicleController()
{
	DetachObject();

	for ( auto &wheel : m_Wheels )
		m_pEnvironment->DestroyObject( wheel.pObject );
	m_Wheels.clear();
}

//------------------------------------------------------------------------------------------------

void JoltPhysicsVehicleController::Update( float dt, vehicle_controlparams_t &controls )
{
	m_ControlParams = controls;

	UpdateBooster( dt );

	HandleBoostKey();
}

const vehicle_operatingparams_t &JoltPhysicsVehicleController::GetOperatingParams()
{
	return m_OperatingParams;
}

const vehicleparams_t &JoltPhysicsVehicleController::GetVehicleParams()
{
	return m_VehicleParams;
}

vehicleparams_t &JoltPhysicsVehicleController::GetVehicleParamsForChange()
{
	return m_VehicleParams;
}

float JoltPhysicsVehicleController::UpdateBooster( float dt )
{
	m_InternalState.BoostDelay				= Max( m_InternalState.BoostDelay - dt, 0.0f );
	m_InternalState.BoosterRemainingTime	= Max( m_InternalState.BoosterRemainingTime - dt, 0.0f );

	return m_InternalState.BoostDelay;
}

int JoltPhysicsVehicleController::GetWheelCount()
{
	return int( m_Wheels.size() );
}

IPhysicsObject *JoltPhysicsVehicleController::GetWheel( int index )
{
	if ( index >= int( m_Wheels.size() ) )
		return nullptr;

	return m_Wheels[ index ].pObject;
}

bool JoltPhysicsVehicleController::GetWheelContactPoint( int index, Vector *pContactPoint, int *pSurfaceProps )
{
	if ( index < int( m_Wheels.size() ) && m_VehicleConstraint->GetWheels()[ index ]->HasContact() )
	{
		if ( pContactPoint )
			*pContactPoint = JoltToSource::Distance( m_VehicleConstraint->GetWheels()[ index ]->GetContactPosition() );

		// TODO(Josh): This!
		if ( pSurfaceProps )
			*pSurfaceProps = 0;

		return true;
	}
	else
	{
		if ( pContactPoint )
			*pContactPoint = vec3_origin;

		if ( pSurfaceProps )
			*pSurfaceProps = 0;

		return false;
	}
}

void JoltPhysicsVehicleController::SetSpringLength( int wheelIndex, float length )
{

}

void JoltPhysicsVehicleController::SetWheelFriction( int wheelIndex, float friction )
{

}

//-------------------------------------------------------------------------------------------------

void JoltPhysicsVehicleController::OnVehicleEnter()
{
	// Undo any damping we may have set to slow the boat when
	// we got out.
	if ( m_VehicleType == VEHICLE_TYPE_AIRBOAT_RAYCAST )
	{
		float flDampSpeed = 0.0f;
		float flDampRotSpeed = 0.0f;
		m_pCarBodyObject->SetDamping( &flDampSpeed, &flDampRotSpeed );
	}
}

void JoltPhysicsVehicleController::OnVehicleExit()
{
	// If we are an airboat, set a bunch of damping to slow us down.
	if ( m_VehicleType == VEHICLE_TYPE_AIRBOAT_RAYCAST )
	{
		float flDampSpeed = 1.0f;
		float flDampRotSpeed = 1.0f;
		m_pCarBodyObject->SetDamping( &flDampSpeed, &flDampRotSpeed );
	}

	SetEngineDisabled( false );
}

//-------------------------------------------------------------------------------------------------

void JoltPhysicsVehicleController::SetEngineDisabled( bool bDisable )
{
	m_InternalState.EngineDisabled = bDisable;
}

bool JoltPhysicsVehicleController::IsEngineDisabled()
{
	return m_InternalState.EngineDisabled;
}

void JoltPhysicsVehicleController::GetCarSystemDebugData( vehicle_debugcarsystem_t &debugCarSystem )
{

}

void JoltPhysicsVehicleController::VehicleDataReload()
{

}

//-------------------------------------------------------------------------------------------------

float JoltPhysicsVehicleController::GetSpeed()
{
	const Vector orientation = GetColumn( GetBodyMatrix(), MatrixAxis::Left );
	return orientation.Dot( m_pCarBodyObject->GetVelocity() );
}

//------------------------------------------------------------------------------------------------

void JoltPhysicsVehicleController::HandleBoostKey()
{
	// Handle triggering boosting if the key is pressed and we aren't currently boosting or in cooldown.
	if ( m_ControlParams.boost && !m_InternalState.BoostDelay && !m_InternalState.BoosterRemainingTime )
	{
		m_InternalState.BoosterRemainingTime	= m_VehicleParams.engine.boostDuration;
		m_InternalState.BoostDelay				= m_VehicleParams.engine.boostDuration + m_VehicleParams.engine.boostDelay;
	}
}

void JoltPhysicsVehicleController::HandleBoostDecay()
{
	// Decay the boost time if we are currently boosting or have a delay.
	if ( m_VehicleParams.engine.boostDuration || m_VehicleParams.engine.boostDelay )
	{
		m_OperatingParams.boostTimeLeft = m_InternalState.BoostDelay
			? 100.0f - ( 100.0f * ( m_InternalState.BoostDelay / ( m_VehicleParams.engine.boostDuration + m_VehicleParams.engine.boostDelay ) ) )
			: 100.0f;
	}
}

//------------------------------------------------------------------------------------------------

void JoltPhysicsVehicleController::OnPreSimulate( float flDeltaTime )
{
	JPH::BodyInterface &bodyInterface = m_pPhysicsSystem->GetBodyInterfaceNoLock();
	// With any user input, assure that the car is active
	if ( m_ControlParams.steering != 0.0f || m_ControlParams.throttle != 0.0f || m_ControlParams.brake != 0.0f || m_ControlParams.handbrake )
		bodyInterface.ActivateBody( m_pCarBodyObject->GetBodyID() );

	bool bHandbrake = m_ControlParams.handbrake;

	// Don't throttle when holding handbrake (like Source)
	float flThrottle = bHandbrake ? 0.0f : m_ControlParams.throttle;
	
	// Apply a little brake without throttle to stop the vehicle from coasting (like Source).
	const bool bCoasting = flThrottle == 0.0f && m_ControlParams.brake == 0.0f && !bHandbrake;
	const float flBrake = bCoasting ? 0.1f : m_ControlParams.brake;

	const float ThrottleOpositionSpeed = vjolt_vehicle_throttle_opposition_limit.GetFloat();

	// Enable the handbrake when going at low speeds to avoid slipping when going up hill.
	if ( ( flThrottle < 0.0f && m_OperatingParams.speed > ThrottleOpositionSpeed ) ||
		( flThrottle > 0.0f && m_OperatingParams.speed < -ThrottleOpositionSpeed ) )
		bHandbrake = true;

	// Are we boosting?
	float flTotalTorqueMultiplier = 1.0f;
	if ( m_InternalState.BoosterRemainingTime != 0.0f )
	{
		GetWheeledVehicleController()->GetEngine().SetCurrentRPM(m_VehicleParams.engine.maxRPM);
		// Slam the throttle to 1, neeeowm!
		m_ControlParams.throttle = 1.0f;
		flThrottle = 1.0f;

		const float flSpeedFactor = RemapValClamped( fabsf( m_OperatingParams.speed ), 0, m_VehicleParams.engine.maxSpeed, 0.1f, 1.0f );
		const float flTurnFactor = 1.0f - ( fabsf( m_ControlParams.steering ) * 0.95f );
		// Josh: * 2 as the original torque stuff in Source was based around 0.5 being the max, and 1.0 being boost.
		const float flDampedBoost = 2.0f * m_VehicleParams.engine.boostForce * flSpeedFactor * flTurnFactor;

		if ( flDampedBoost > flTotalTorqueMultiplier )
			flTotalTorqueMultiplier = flDampedBoost;
	}

	// Update the torque factors as we may be boosting and be > 1.
	// TODO(Josh): More than 2 wheels per axle.
	VJoltAssert( m_VehicleParams.wheelsPerAxle == 2 );
	for ( int i = 0; i < m_VehicleParams.axleCount; i++ )
		GetWheeledVehicleController()->GetDifferentials()[i].mEngineTorqueRatio = flTotalTorqueMultiplier * m_VehicleParams.axles[i].torqueFactor;

	// Pass the input on to the constraint
	GetWheeledVehicleController()->SetDriverInput( flThrottle, m_ControlParams.steering, flBrake, bHandbrake ? 1.0f : 0.0f );

	// Set the collision tester
	m_VehicleConstraint->SetVehicleCollisionTester( m_Tester );
}

//------------------------------------------------------------------------------------------------

void JoltPhysicsVehicleController::OnPostSimulate( float flDeltaTime )
{
	// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)

	float flSteeringAngle = 0.0f;
	m_OperatingParams.wheelsInContact = 0;
	m_OperatingParams.wheelsNotInContact = 0;
	for ( int w = 0; w < GetWheelCount(); w++ )
	{
		const JPH::WheelSettings *settings = m_VehicleConstraint->GetWheels()[w]->GetSettings();
		// The cyclinder we draw is aligned with Y so we specify that as rotational axis
		JPH::Mat44 wheelTransform = m_VehicleConstraint->GetWheelWorldTransform( w, JPH::Vec3( 1, 0, 0 ),  JPH::Vec3( 0, 0, 1 ) );
		
		// Find our greatest steering angle.
		float flWheelSteeringAngle = JoltToSource::Angle( m_VehicleConstraint->GetWheels()[w]->GetSteerAngle() );
		if ( fabsf( flWheelSteeringAngle ) > fabsf( flSteeringAngle ) )
			flSteeringAngle = flWheelSteeringAngle;

		Vector newPos = JoltToSource::Distance( wheelTransform.GetTranslation() );
		// TODO(Josh): This triggers JPH_ASSERT(mCol[3] == Vec4(0, 0, 0, 1));
		// what to do about that?..
		// We just want the local rotation, and this seems to work (?)
		QAngle newQuat = JoltToSource::Angle( wheelTransform.GetQuaternion() );
		m_Wheels[ w ].pObject->EnableCollisions( false );
		// Set dummy wheel object pos/angles so the game code can update pose positions for wheels.
		m_Wheels[ w ].pObject->SetPosition( newPos, newQuat, true );
		// Wake it up so that the game bothers to do pose positions.
		m_Wheels[ w ].pObject->Wake();

		if ( m_VehicleConstraint->GetWheels()[w]->HasContact() )
			m_OperatingParams.wheelsInContact++;
		else
			m_OperatingParams.wheelsNotInContact++;

		IVJoltDebugOverlay *pDebugOverlay = JoltPhysicsInterface::GetInstance().GetDebugOverlay();
		if ( vjolt_vehicle_wheel_debug.GetBool() && pDebugOverlay )
		{
			const Vector vecWheelPos = JoltToSource::Distance( wheelTransform.GetTranslation() );
			const Vector vecWheelSize = JoltToSource::Distance( JPH::Vec3( settings->mWidth / 2.0f, settings->mRadius, settings->mRadius ) );

			pDebugOverlay->AddBoxOverlay(
				vecWheelPos,
				-vecWheelSize, vecWheelSize,
				newQuat,
				255, 0, 255, 100,
				-1.0f );
		}
	}

	m_OperatingParams.gear			= GetWheeledVehicleController()->GetTransmission().GetCurrentGear();
	m_OperatingParams.engineRPM		= GetWheeledVehicleController()->GetEngine().GetCurrentRPM();
	m_OperatingParams.speed			= GetSpeed();
	m_OperatingParams.steeringAngle = -flSteeringAngle;
	m_OperatingParams.boostDelay	= m_InternalState.BoostDelay;
	HandleBoostDecay();
}

//------------------------------------------------------------------------------------------------

void JoltPhysicsVehicleController::CreateWheel( JPH::VehicleConstraintSettings &vehicleSettings, matrix3x4_t& bodyMatrix, int axleIdx, int wheelIdx )
{
	const vehicle_axleparams_t &axle = m_VehicleParams.axles[ axleIdx ];

	const Vector wheelPositionLocal = axle.offset +
		( ( wheelIdx % 2 == 1 ) ? axle.wheelOffset : -axle.wheelOffset );

	Vector wheelPositionWorld;
	VectorTransform( wheelPositionLocal, bodyMatrix, wheelPositionWorld );

	// Josh: Good enough heuristic.
	const float wheelRadius = axle.wheels.radius;
	const float wheelWidth = wheelRadius / 2.0f;

	// Josh: Area of a cylinder = π.h.r^2
	// Using radius in terms of Source units as we pass this to CreateSphereObject.
	const float wheelVolume = M_PI * wheelWidth * Cube( wheelRadius );

	{
		objectparams_t wheelParams =
		{
			.mass		= axle.wheels.mass,
			.inertia	= axle.wheels.inertia,
			.damping	= axle.wheels.damping,
			.rotdamping	= axle.wheels.rotdamping,
			.pName		= "VehicleWheel",
			.pGameData	= m_pCarBodyObject->GetGameData(),
			.volume		= wheelVolume,
		};
		IPhysicsObject *pWheelObject = m_pEnvironment->CreateSphereObject(
			wheelRadius, axle.wheels.materialIndex,
			wheelPositionWorld, QAngle(),
			&wheelParams, false );

		JoltPhysicsObject *pJoltWheelObject = static_cast< JoltPhysicsObject * >( pWheelObject );

		pJoltWheelObject->SetGameFlags( m_pCarBodyObject->GetGameFlags() );
		pJoltWheelObject->SetCallbackFlags( CALLBACK_IS_VEHICLE_WHEEL );
		// Josh: The wheel is a fake object, so disable collisions on it.
		pJoltWheelObject->EnableCollisions( false );

		m_Wheels.push_back( JoltPhysicsWheel{ .pObject = pJoltWheelObject } );
	}

	const float steeringAngle = DEG2RAD( Max( m_VehicleParams.steering.degreesSlow, m_VehicleParams.steering.degreesFast ) );
	const float additionalLength = SourceToJolt::Distance( axle.wheels.springAdditionalLength );

	Vector gravity;
	m_pEnvironment->GetGravity( &gravity );

	JPH::WheelSettingsWV *wheelSettings = new JPH::WheelSettingsWV;
	wheelSettings->mPosition			= SourceToJolt::Distance( wheelPositionLocal );
	wheelSettings->mDirection			= JPH::Vec3( 0, 0, -1 );
	wheelSettings->mAngularDamping		= axle.wheels.rotdamping;
	// TODO(Josh): What about more than 4 wheels?
	wheelSettings->mMaxSteerAngle		= axleIdx == 0 ? steeringAngle : 0.0f;
	wheelSettings->mRadius				= SourceToJolt::Distance( axle.wheels.radius );
	wheelSettings->mWidth				= SourceToJolt::Distance( wheelWidth );
	wheelSettings->mInertia				= 0.5f * axle.wheels.mass * ( wheelSettings->mRadius * wheelSettings->mRadius );
	wheelSettings->mSuspensionMinLength = 0;
	wheelSettings->mSuspensionMaxLength = additionalLength;
	wheelSettings->mSuspensionDamping	= axle.suspension.springDamping;
	// Josh:
	// so to go from K (Spring Constant) -> freq we do
	// sqrtf( K / Mass ) / ( 2.0f * PI )
	// but it seems like it already has mass divided in Source so...
	// sqrtf( K ) / ( 2.0f * PI )
	wheelSettings->mSuspensionFrequency = sqrtf( axle.suspension.springConstant ) / ( 2.0f * M_PI_F );
	// Josh: I don't know why but it looks and feels really wrong without this:
	// TODO(Josh): Investigate more later, doesn't make much sense.
	// May be related to mass of wheel or something.
	wheelSettings->mSuspensionFrequency *= M_PI_F;
	if ( axle.wheels.frictionScale )
	{
		wheelSettings->mLateralFriction.AddPoint( 1.0f, axle.wheels.frictionScale );
		wheelSettings->mLongitudinalFriction.AddPoint( 1.0f, axle.wheels.frictionScale );
	}

	// TODO: We may want to update this every pre-simulation to account for changing gravity.
	wheelSettings->mMaxBrakeTorque =
		0.5f *
		SourceToJolt::Distance( gravity.Length() ) *
		( m_pCarBodyObject->GetMass() + m_TotalWheelMass ) *
		axle.brakeFactor *
		SourceToJolt::Distance( axle.wheels.radius );

	vehicleSettings.mWheels.push_back( wheelSettings );
	m_InternalState.LargestWheelRadius = Max( m_InternalState.LargestWheelRadius, SourceToJolt::Distance( wheelWidth ) );
}

void JoltPhysicsVehicleController::CreateWheels( JPH::VehicleConstraintSettings &vehicleSettings )
{
	matrix3x4_t carBodyMtx = GetBodyMatrix();

	m_Wheels.reserve( m_VehicleParams.axleCount * m_VehicleParams.wheelsPerAxle );
	vehicleSettings.mAntiRollBars.reserve( m_VehicleParams.axleCount );

	m_TotalWheelMass = 0.0f;
	for ( int axle = 0; axle < m_VehicleParams.axleCount; axle++ )
		m_TotalWheelMass += m_VehicleParams.axles[ axle ].wheels.mass * m_VehicleParams.wheelsPerAxle;

	for ( int axle = 0; axle < m_VehicleParams.axleCount; axle++ )
	{
		for ( int wheel = 0; wheel < m_VehicleParams.wheelsPerAxle; wheel++ )
			CreateWheel( vehicleSettings, carBodyMtx, axle, wheel );

		// TODO(Josh): More than 2 wheels per axle.
		VJoltAssert( m_VehicleParams.wheelsPerAxle == 2 );
		JPH::VehicleAntiRollBar rollbar;
		rollbar.mLeftWheel	= ( axle * m_VehicleParams.wheelsPerAxle );
		rollbar.mRightWheel	= ( axle * m_VehicleParams.wheelsPerAxle ) + 1;
		vehicleSettings.mAntiRollBars.push_back( rollbar );
	}
}

JPH::WheeledVehicleControllerSettings *JoltPhysicsVehicleController::CreateVehicleController()
{
	static constexpr float HorsePowerToWatts = 745.7f;

	JPH::WheeledVehicleControllerSettings *pController = new JPH::WheeledVehicleControllerSettings;
	// Josh:
	// T = ( 745.7 * P ) / ( 2 * PI * ( RPM / 60 ) )
	pController->mEngine.mMaxTorque = ( HorsePowerToWatts * m_VehicleParams.engine.horsepower ) / ( 2.0f * M_PI * ( m_VehicleParams.engine.maxRPM / 60.0f ) );
	// Josh: Fudge
	pController->mEngine.mMinRPM = Max( m_VehicleParams.engine.shiftDownRPM - 300, 0.0f );
	pController->mEngine.mMaxRPM = m_VehicleParams.engine.maxRPM;
	pController->mEngine.mAngularDamping = 0.0f;

	pController->mTransmission.mMode = m_VehicleParams.engine.isAutoTransmission ? JPH::ETransmissionMode::Auto : JPH::ETransmissionMode::Manual;
	pController->mTransmission.mGearRatios.clear();
	for ( int i = 0; i < m_VehicleParams.engine.gearCount; i++ )
		pController->mTransmission.mGearRatios.push_back( m_VehicleParams.engine.gearRatio[ i ] );

	pController->mTransmission.mReverseGearRatios.clear();
	pController->mTransmission.mReverseGearRatios.push_back( -m_VehicleParams.engine.gearRatio[0] );

	pController->mTransmission.mShiftUpRPM = m_VehicleParams.engine.shiftUpRPM;
	pController->mTransmission.mShiftDownRPM = m_VehicleParams.engine.shiftDownRPM;

	pController->mDifferentials.reserve( m_VehicleParams.axleCount );
	for ( int i = 0; i < m_VehicleParams.axleCount; i++ )
	{
		// TODO(Josh): More than 2 wheels per axle.
		VJoltAssert( m_VehicleParams.wheelsPerAxle == 2 );
		JPH::VehicleDifferentialSettings differential;
		differential.mLeftWheel			= ( i * m_VehicleParams.wheelsPerAxle );
		differential.mRightWheel		= ( i * m_VehicleParams.wheelsPerAxle ) + 1;
		differential.mEngineTorqueRatio = m_VehicleParams.axles[ i ].torqueFactor;

		pController->mDifferentials.push_back( differential );
	}

	return pController;
}

JPH::WheeledVehicleController *JoltPhysicsVehicleController::GetWheeledVehicleController()
{
	return static_cast<JPH::WheeledVehicleController *>( m_VehicleConstraint->GetController() );
}

//------------------------------------------------------------------------------------------------

matrix3x4_t JoltPhysicsVehicleController::GetBodyMatrix() const
{
	matrix3x4_t value;
	m_pCarBodyObject->GetPositionMatrix( &value );
	return value;
}

//------------------------------------------------------------------------------------------------

void JoltPhysicsVehicleController::OnJoltPhysicsObjectDestroyed( JoltPhysicsObject *pObject )
{
	if ( m_pCarBodyObject == pObject )
		DetachObject();
}

void JoltPhysicsVehicleController::DetachObject()
{
	if ( m_pCarBodyObject )
	{
		m_pCarBodyObject->RemoveDestroyedListener( this );

		// Remove the listeners and constraint now, we can never
		// attach to another body.
		m_pPhysicsSystem->RemoveConstraint( m_VehicleConstraint );
		m_pPhysicsSystem->RemoveStepListener( m_VehicleConstraint );

		m_pCarBodyObject = nullptr;
	}
}
