#!/usr/bin/env python

# Copyright (c) 2021 Computer Vision Center (CVC) at the Universitat Autonoma de
# Barcelona (UAB).
#
# This work is licensed under the terms of the MIT license.
# For a copy, see <https://opensource.org/licenses/MIT>.

"""
Tester for vehicle physics utility for CARLA
Uses:
    Basics scenearios (acceleration, brake, u-turn):
        python vehicle_physics_tester.py --filter vehicle_id --basics
    High-speed (100km/h) turn sceneario:
        python vehicle_physics_tester.py --filter vehicle_id --turn
"""

import glob
import os
import sys
import argparse
import time
import numpy as np

try:
    sys.path.append(glob.glob('../carla/dist/carla-*%d.%d-%s.egg' % (
        sys.version_info.major,
        sys.version_info.minor,
        'win-amd64' if os.name == 'nt' else 'linux-x86_64'))[0])
except IndexError:
    pass

import carla

class VehicleControlStop:
    def __init__(self, x_min = -100000, x_max = +100000, y_min = -100000, y_max = +100000,
            yaw_min = -500, yaw_max = +500, speed_min = -1, speed_max = +100000):

        self.x_min = x_min
        self.x_max = x_max
        self.y_min = y_min
        self.y_max = y_max
        self.yaw_min = yaw_min
        self.yaw_max = yaw_max
        self.speed_min = speed_min
        self.speed_max = speed_max

    def stop_control(self, vehicle):

        loc = vehicle.get_location()
        if loc.x > self.x_max :
            return True
        if loc.x < self.x_min:
            return True

        if loc.y > self.y_max :
            return True
        if loc.y < self.y_min:
            return True

        rot = vehicle.get_transform().rotation

        if rot.yaw > self.yaw_max :
            return True
        if rot.yaw < self.yaw_min:
            return True

        speed = norm(vehicle.get_velocity())
        if speed > self.speed_max :
            return True
        if speed < self.speed_min:
            return True

        return False


def change_physics_control(vehicle, tire_friction = None, drag = None, wheel_sweep = None, 
    long_stiff = None, lat_stiff = None, lat_load = None,
    clutch_strength = None, max_rpm = None):

    physics_control = vehicle.get_physics_control()

    if drag is not None:
        physics_control.drag_coefficient = drag

    if wheel_sweep is not None:
        physics_control.use_sweep_wheel_collision = wheel_sweep

    if clutch_strength is not None:
        physics_control.clutch_strength = clutch_strength

    if max_rpm is not None:
        physics_control.max_rpm = max_rpm

    if len(physics_control.forward_gears) == 1:
        gear = carla.GearPhysicsControl(1.0, 0.00, 0.00)
        physics_control.forward_gears = [gear]

    physics_control.damping_rate_full_throttle = 0.01

    front_left_wheel = physics_control.wheels[0]
    front_right_wheel = physics_control.wheels[1]
    rear_left_wheel = physics_control.wheels[2]
    rear_right_wheel = physics_control.wheels[3]

    if tire_friction is not None:
        front_left_wheel.tire_friction = tire_friction
        front_right_wheel.tire_friction = tire_friction
        rear_left_wheel.tire_friction = tire_friction
        rear_right_wheel.tire_friction = tire_friction

    if long_stiff is not None:
        front_left_wheel.long_stiff_value = long_stiff
        front_right_wheel.long_stiff_value = long_stiff
        rear_left_wheel.long_stiff_value = long_stiff
        rear_right_wheel.long_stiff_value = long_stiff

    if lat_stiff is not None:
        front_left_wheel.lat_stiff_value  = lat_stiff
        front_right_wheel.lat_stiff_value = lat_stiff
        rear_left_wheel.lat_stiff_value   = lat_stiff
        rear_right_wheel.lat_stiff_value  = lat_stiff

    if lat_load is not None:
        front_left_wheel.lat_stiff_max_load  = lat_load
        front_right_wheel.lat_stiff_max_load = lat_load
        rear_left_wheel.lat_stiff_max_load   = lat_load
        rear_right_wheel.lat_stiff_max_load  = lat_load

    wheels = [front_left_wheel, front_right_wheel, rear_left_wheel, rear_right_wheel]
    physics_control.wheels = wheels

    return physics_control

def print_step_info(world, vehicle):
    snapshot = world.get_snapshot()
    print("%d %06.03f %+8.03f %+8.03f %+8.03f %+8.03f %+8.03f %+8.03f %+8.03f %+8.03f %+8.03f" %
            (snapshot.frame, snapshot.timestamp.elapsed_seconds, \
            vehicle.get_acceleration().x, vehicle.get_acceleration().y, vehicle.get_acceleration().z, \
            vehicle.get_velocity().x, vehicle.get_velocity().y, vehicle.get_velocity().z, \
            vehicle.get_location().x, vehicle.get_location().y, vehicle.get_location().z))

def wait(world, frames=100):
    for _i in range(0, frames):
        world.tick()

def norm(vec):
    return np.sqrt(vec.x*vec.x + vec.y*vec.y + vec.z*vec.z)

class TelemetryPoint:
    def __init__(self, curr_time=None, location=None, rotation=None, velocity=None):
        self.time = curr_time
        self.location = location
        self.rotation = rotation
        self.velocity = velocity

    def __str__(self):
        return "%.2f %s %s %s" % (self.time, str(self.location), str(self.rotation), str(self.velocity))

    def __sub__(self, other):
        t = self.time - other.time
        l = self.location - other.location
        r = carla.Rotation()
        v = self.velocity - other.velocity
        return TelemetryPoint(t, l, r, v)

class TelemetryData:
    def __init__(self, curr_time, vehicle):
        self.list_of_telemetries = []
        location = vehicle.get_location()
        rotation = vehicle.get_transform().rotation
        velocity = vehicle.get_velocity()
        self.list_of_telemetries.append(TelemetryPoint(curr_time, location, rotation, velocity))

    def __str__(self):
        ret_str = ""
        for idx, val in enumerate(self.list_of_telemetries):
            ret_str += "%d: %s\n" % (idx, str(val))

        return ret_str

    def add_telemetry(self, curr_time, vehicle):
        location = vehicle.get_location()
        rotation = vehicle.get_transform().rotation
        velocity = vehicle.get_velocity()
        self.list_of_telemetries.append(TelemetryPoint(curr_time, location, rotation, velocity))

    def number_of_telemetries(self):
        return len(self.list_of_telemetries)

    def get_telemetry(self, index):
        if index >= self.number_of_telemetries():
            return TelemetryPoint()

        return self.list_of_telemetries[index]

    def get_telemetry_delta(self, index):
        if index >= (self.number_of_telemetries()-1):
            return TelemetryPoint()

        return self.list_of_telemetries[index+1] - self.list_of_telemetries[index]

    def get_scalar_delta(self, index):
        if index >= (self.number_of_telemetries()-1):
            return TelemetryPoint()

        delta = self.list_of_telemetries[index+1] - self.list_of_telemetries[index]

        return delta.time, norm(delta.location), norm(delta.velocity)


def run_scenario(world, bp_veh, init_loc, init_speed = 0.0, init_frames=10,
        controls=[(150, carla.VehicleControl(), VehicleControlStop())],
        apply_phys_control = None):

    veh_transf = init_loc

    vehicle = world.spawn_actor(bp_veh, veh_transf)
    wait(world, 10)

    data = TelemetryData(world.get_snapshot().elapsed_seconds, vehicle)

    # Initialization at init_speed
    vehicle.enable_constant_velocity(carla.Vector3D(init_speed, 0, 0))
    wait(world, init_frames)
    vehicle.disable_constant_velocity()
    wait(world, 1)

    for i_control in controls:
        control_frames = i_control[0]
        control = i_control[1]
        stopper = i_control[2]

        data.add_telemetry(world.get_snapshot().elapsed_seconds, vehicle)

        # Apply control
        vehicle.apply_control(control)
        for _i in range(0, control_frames):
            world.tick()
            check = stopper.stop_control(vehicle)
            if check:
                break

    data.add_telemetry(world.get_snapshot().elapsed_seconds, vehicle)

    wait(world, 10)
    vehicle.destroy()

    return data

def brake_scenario(world, bp_veh, speed):

    spectator_transform = carla.Transform(carla.Location(20, -190, 10), carla.Rotation(yaw=67, pitch=-13))
    try:
        spectator = world.get_spectator()
        spectator.set_transform(spectator_transform)
    except:
        print("No spectator")

    init_loc = carla.Transform(carla.Location(32, -180, 0.5), carla.Rotation(yaw=90))

    controls = [
        (1000, carla.VehicleControl(brake=1.0), VehicleControlStop(speed_min=0.1))]
    
    data = run_scenario(world, bp_veh, init_loc, init_speed=speed/3.6, controls=controls)

    delta = data.get_scalar_delta(1)
    end_vel = 3.6*norm(data.get_telemetry(2).velocity)
    print("  %.0f -> 0 km/h: (%.1f s, %.1f m)" % (speed, delta[0], delta[1]), end="")

def accel_scenario(world, bp_veh, max_vel):

    spectator_transform = carla.Transform(carla.Location(20, -190, 10), carla.Rotation(yaw=67, pitch=-13))
    try:
        spectator = world.get_spectator()
        spectator.set_transform(spectator_transform)
    except:
        print("No spectator")

    init_loc = carla.Transform(carla.Location(32, -180, 0.5), carla.Rotation(yaw=90))

    controls = [
        (1000, carla.VehicleControl(throttle=1.0), VehicleControlStop(speed_max=max_vel/3.6))]

    data = run_scenario(world, bp_veh, init_loc=init_loc, controls=controls)

    delta = data.get_scalar_delta(1)
    end_vel = 3.6*norm(data.get_telemetry(2).velocity)
    print("  0 -> %.0f km/h: (%.1f s, %.1f m)" % (max_vel, delta[0], delta[1]), end="")

def uturn_scenario(world, bp_veh):

    spectator_transform = carla.Transform(carla.Location(30, -180, 20), carla.Rotation(yaw=-140, pitch=-36))
    try:
        spectator = world.get_spectator()
        spectator.set_transform(spectator_transform)
    except:
        pass

    init_pos = carla.Transform(carla.Location(15, -190, 0.2), carla.Rotation(yaw=0))
    controls = [
        (1000, carla.VehicleControl(throttle=1.0), VehicleControlStop(x_max=19)),
        (1000, carla.VehicleControl(throttle=0.25, steer=-0.4), VehicleControlStop(yaw_min=-170)),
        (100, carla.VehicleControl(throttle=0.4), VehicleControlStop(x_min =10))
        ]

    data = run_scenario(world, bp_veh, init_loc=init_pos, controls=controls)
    end_vel = 3.6*norm(data.get_telemetry(3).velocity)

def highspeed_turn_scenario(world, bp_veh, steer):
    spectator_transform = carla.Transform(carla.Location(70, -200, 15), carla.Rotation(yaw=0, pitch=-12))

    try:
        spectator = world.get_spectator()
        spectator.set_transform(spectator_transform)
    except:
        pass

    init_pos = carla.Transform(carla.Location(50, -204, 0.2), carla.Rotation(yaw=0))
    init_frames = 2
    init_speed = 100 / 3.6
    controls = [(100, carla.VehicleControl(throttle=0.5), VehicleControlStop(x_max=100)),
        (200, carla.VehicleControl(throttle=1.0, steer = steer), VehicleControlStop(yaw_max=45, speed_min=3)),
        (200, carla.VehicleControl(brake=1), VehicleControlStop())]

    data = run_scenario(world, bp_veh, init_loc=init_pos, init_speed=init_speed, init_frames = init_frames, controls=controls)

    time.sleep(1)

def main(arg):
    """Main function of the script"""
    client = carla.Client(arg.host, arg.port)
    client.set_timeout(30.0)
    world = client.get_world()

    try:
        # Setting the world and the spawn properties
        original_settings = world.get_settings()
        settings = world.get_settings()

        delta = 0.05
        settings.fixed_delta_seconds = delta
        settings.synchronous_mode = True
        world.apply_settings(settings)

        if world.get_map().name != "Town05":
            client.load_world("Town05", False)

        for bp_veh in world.get_blueprint_library().filter(args.filter):
            print("-------------------------------------------")
            print(bp_veh.id, end="", flush=True)

            if args.show_physics_control:
                try:
                    veh_transf = carla.Transform()
                    veh_transf.location.z = 100
                    vehicle = world.spawn_actor(bp_veh, veh_transf)
                    print()
                    print(vehicle.get_physics_control())
                    vehicle.destroy()
                except:
                    pass

            if args.none:
                continue

            if args.accel or args.all:
                accel_scenario(world, bp_veh, 50)
                accel_scenario(world, bp_veh, 100)

            if args.brake or args.all:
                brake_scenario(world, bp_veh, 80)
                brake_scenario(world, bp_veh, 100)

            if args.uturn or args.all:
                uturn_scenario(world, bp_veh)

            if args.turn or args.all:
                highspeed_turn_scenario(world, bp_veh, 0.2)

            print()

        print("-------------------------------------------")


    finally:
        world.apply_settings(original_settings)



if __name__ == "__main__":

    argparser = argparse.ArgumentParser(
        description=__doc__)
    argparser.add_argument(
        '--host',
        metavar='H',
        default='localhost',
        help='IP of the host CARLA Simulator (default: localhost)')
    argparser.add_argument(
        '-p', '--port',
        metavar='P',
        default=2000,
        type=int,
        help='TCP port of CARLA Simulator (default: 2000)')
    argparser.add_argument(
        '--filter',
        metavar='PATTERN',
        default='vehicle.*',
        help='actor filter (default: "vehicle.*")')
    argparser.set_defaults(accel=False)
    argparser.add_argument(
        '--accel',
        dest='accel',
        action='store_true',
        help='Execute accel scenarios')
    argparser.set_defaults(brake=False)
    argparser.add_argument(
        '--brake',
        dest='brake',
        action='store_true',
        help='Execute brake scenarios')
    argparser.set_defaults(uturn=False)
    argparser.add_argument(
        '--uturn',
        dest='uturn',
        action='store_true',
        help='Execute brake scenarios')
    argparser.set_defaults(turn=False)
    argparser.add_argument(
        '--turn',
        dest='turn',
        action='store_true',
        help='Execute basic scenarios')
    argparser.set_defaults(all=True)
    argparser.add_argument(
        '--all',
        dest='all',
        action='store_true',
        help='Execute all scenarios')
    argparser.set_defaults(none=False)
    argparser.add_argument(
        '--none',
        dest='none',
        action='store_true',
        help='Do not execute any scenarios')
    argparser.set_defaults(show_physics_control=False)
    argparser.add_argument(
        '--show_physics_control',
        dest='show_physics_control',
        action='store_true',
        help='Show default physics control of cars')

    args = argparser.parse_args()
    if args.accel or args.brake or args.uturn or args.turn:
        args.all = False

    try:
        main(args)
    except KeyboardInterrupt:
        print(' - Exited by user.')
