"""A four-wheel vehicle driving from deformable terrain onto a rigid platform.

Each agxVehicle.Tire owns a GTire and a TerrainWheel.  Configure both child
models explicitly and give them separate materials: GTire handles rigid
geometry while TerrainWheel handles deformable AGX Terrain.
"""

import math

import agx
import agxCollide
import agxOSG
import agxRender
import agxSDK
import agxTerrain
import agxUtil
import agxVehicle

from agxPythonModules.utils.callbacks import StepEventCallback
from agxPythonModules.utils.environment import init_app, root, simulation
from agxPythonModules.utils.terrain import create_visual


WHEEL_RADIUS = 0.5
WHEEL_WIDTH = 0.4
WHEEL_MASS = 10.0
CHASSIS_HALF_EXTENTS = agx.Vec3(1.2, 2.0, 0.5)
CHASSIS_MASS = 1000.0
SUSPENSION_RANGE = 0.1
SUSPENSION_STIFFNESS = 1.0e6
SUSPENSION_DAMPING = 4.0e4


def create_g_tire_parameters():
    """Return an explicit GTire parameter set for the rigid-platform contacts."""
    parameters = agxVehicle.GTireParameters()
    parameters.nominalInflationPressure = 220000.0
    parameters.inflationPressure = 220000.0
    parameters.verticalStiffness = 209651.0
    parameters.dKz_dP = 0.7098
    parameters.verticalDamping = 150.0
    parameters.longitudinalStiffness = 358066.0
    parameters.dKx_dP_lin = 0.17504
    parameters.dKx_dP_quad = 0.0
    parameters.lateralStiffness = 358066.0
    parameters.dKy_dP = 0.16365
    parameters.yawStiffness = 6000.0
    parameters.muStatic = 1.0
    parameters.muDynamic = 1.0
    parameters.rollingResistance = 0.00702
    parameters.rollingResistance_v = 0.001515
    parameters.rollingResistance_v4 = 8.514e-5
    parameters.rollingResistance_Fz = 0.0
    parameters.rollingResistance_P = 0.0
    parameters.minReferenceSpeed = 0.1
    return parameters


def configure_terrain_wheel(terrain_wheel):
    """Set the TerrainWheel model parameters used for deformable-terrain contacts."""
    settings = terrain_wheel.getTerrainWheelSettings()
    settings.setPressureSinkageModel(
        agxTerrain.TerrainWheelSettings.PressureSinkageModel_REECE)
    settings.setEnableComputeMaximumNormalStressAngleFromFrontAngle(True)
    settings.setEnableComputeRearAngleFromFrontAngle(False)


class BoxCar(agxSDK.Assembly):
    """A simple box car with four tires using the unified tire class."""

    def __init__(self):
        super().__init__()
        self.g_tire_material = agx.Material("unified_g_tire")
        self.terrain_wheel_material = agx.Material("unified_terrain_wheel")
        self.drive_motors = []
        self.wheels = []
        self.wheel_joints = []

        self.chassis = agx.RigidBody("chassis")
        self.chassis.add(agxCollide.Geometry(agxCollide.Box(CHASSIS_HALF_EXTENTS)))
        self.chassis.getMassProperties().setMass(CHASSIS_MASS)
        self.add(self.chassis)
        chassis_visual = agxOSG.createVisual(self.chassis, root())
        agxOSG.setDiffuseColor(chassis_visual, agxRender.Color.Blue())
        agxOSG.setAlpha(chassis_visual, 0.3)

        wheel_axis = -agx.Vec3.X_AXIS()
        steering_axis = agx.Vec3.Z_AXIS()
        for index, local_position in enumerate(self.wheel_positions()):
            tire = self.create_tire()
            tire.setPosition(local_position)
            tire.setRotation(agx.Quat(agx.Vec3.Y_AXIS(), agx.Vec3.X_AXIS()))
            tire.getTireBody().getMassProperties().setMass(WHEEL_MASS)
            self.add(tire)
            self.wheels.append(tire)

            anchor = agx.Vec3(
                math.copysign(CHASSIS_HALF_EXTENTS.x(), local_position.x()),
                math.copysign(CHASSIS_HALF_EXTENTS.y(), local_position.y()),
                local_position.z(),
            )
            joint = agxVehicle.WheelJoint(
                agxVehicle.WheelJointFrame(anchor, wheel_axis, steering_axis),
                tire.getGTire().getTireCarrier(), self.chassis)
            # The front wheels are steerable; the Ackermann constraint below
            # keeps their steering angles coupled.
            joint.getLock1D(agxVehicle.WheelJoint.STEERING).setEnable(index >= 2)
            joint.getMotor1D(agxVehicle.WheelJoint.STEERING).setEnable(False)
            joint.getLock1D(agxVehicle.WheelJoint.WHEEL).setEnable(True)
            joint.getRange1D(agxVehicle.WheelJoint.SUSPENSION).setEnable(True)
            joint.getRange1D(agxVehicle.WheelJoint.SUSPENSION).setRange(
                -SUSPENSION_RANGE, SUSPENSION_RANGE)
            suspension = joint.getLock1D(agxVehicle.WheelJoint.SUSPENSION)
            suspension.setEnable(True)
            suspension.setCompliance(
                agxUtil.convertSpringConstantToCompliance(SUSPENSION_STIFFNESS))
            suspension.setAttenuation(agxUtil.convertDampingCoefficientToAttenuation(
                SUSPENSION_DAMPING, SUSPENSION_STIFFNESS, simulation().getTimeStep()))
            self.add(joint)
            self.wheel_joints.append(joint)
            agxUtil.setEnableCollisions(self.chassis, tire.getTireBody(), False)

            motor = tire.getGTire().getCarcassHinge().getMotor1D()
            motor.setSpeed(-2.0 / WHEEL_RADIUS)
            motor.setForceRange(agx.RangeReal(1.0e5))
            motor.setEnable(False)
            self.drive_motors.append(motor)

            # The two tire geometries overlap, so render only the GTire geometry.
            for geometry in tire.getTireBody().getGeometries():
                if geometry.getName() == "GTireGeometry":
                    agxOSG.createVisual(geometry, root())

        # The first two wheels are the front pair. Without this constraint,
        # their unlocked steering DOFs can rotate independently about Z.
        self.steering = agxVehicle.Ackermann(
            self.wheel_joints[1], self.wheel_joints[0])
        self.add(self.steering)

        self.setPosition(-12.0, 0.0, 1.5)
        self.setRotation(agx.EulerAngles(0.0, 0.0, -0.5 * agx.PI))

    @staticmethod
    def wheel_positions():
        z = -CHASSIS_HALF_EXTENTS.z() - 0.6 + WHEEL_RADIUS
        x = CHASSIS_HALF_EXTENTS.x() + WHEEL_WIDTH
        y = CHASSIS_HALF_EXTENTS.y()
        return (agx.Vec3(+x, +y, z), agx.Vec3(-x, +y, z),
                agx.Vec3(+x, -y, z), agx.Vec3(-x, -y, z))

    def create_tire(self):
        tire = agxVehicle.Tire(WHEEL_RADIUS, WHEEL_WIDTH)
        g_tire = tire.getGTire()
        terrain_wheel = tire.getTerrainWheel()

        # These materials must be different: the two models configure their
        # respective material pairs differently.
        g_tire.setMaterial(self.g_tire_material)
        terrain_wheel.setMaterial(self.terrain_wheel_material)
        # Configure each model independently.
        g_tire.setGTireParameters(create_g_tire_parameters())
        configure_terrain_wheel(terrain_wheel)
        g_tire.getCarcassHinge().getLock1D().setEnable(False)
        return tire

    def configure_terrain_contacts(self, terrain):
        material_manager = simulation().getMaterialManager()
        for tire in self.wheels:
            contact_material = material_manager.getOrCreateContactMaterial(
                terrain.getMaterial(), tire.getTerrainWheel().getMaterial())
            agxTerrain.TerrainWheel.configureContactMaterial(contact_material)
            contact_material.setRestitution(0.0)

    def enable_drive(self):
        for motor in self.drive_motors:
            motor.setEnable(True)


def buildScene():
    simulation().setTimeStep(1.0 / 120.0)
    simulation().setUniformGravity(agx.Vec3(0.0, 0.0, -9.81))

    terrain = agxTerrain.Terrain(281, 281, 0.15, 2.0)
    terrain.setPosition(agx.Vec3(-1.0, 0.0, 0.0))
    terrain.loadLibraryMaterial("terrain_wheel_sand_1")
    # TerrainWheel reads terramechanics parameters from the terrain material.
    # Change one parameter explicitly as an example: the Bekker frictional
    # modulus k_phi [N/m^(n+2)].
    terrain.getTerrainMaterial().getTerramechanicsProperties().setFrictionalModulusBekker(
        1.5e6)
    terrain.setMaterial(agx.Material("terrain"))
    simulation().add(terrain)
    create_visual(terrain, render_particles=True, render_height_field=True)

    platform = agx.RigidBody("rigid platform")
    platform.add(agxCollide.Geometry(agxCollide.Box(4.0, 4.0, 0.05)))
    platform.setPosition(0.0, 0.0, -0.025)
    platform.setMotionControl(agx.RigidBody.STATIC)
    simulation().add(platform)
    platform_visual = agxOSG.createVisual(platform, root())
    agxOSG.setDiffuseColor(platform_visual, agxRender.Color.DimGray())

    vehicle = BoxCar()
    simulation().add(vehicle)
    vehicle.configure_terrain_contacts(terrain)
    StepEventCallback.callAt(1.0, vehicle.enable_drive)


def set_camera(app):
    app.setCameraHome(agx.Vec3(-12.0, -15.0, 8.0), agx.Vec3(0.0, 0.0, 0.5), agx.Vec3.Z_AXIS())


init = init_app(name=__name__, scenes=[(buildScene, "1")], autoStepping=True, onInitialized=set_camera)
