import settings
from sources import operating_system, STOP
import glob
import os
import sys
try:
    sys.path.append(glob.glob(settings.CARLA_PATH + f'/PythonAPI/carla/dist/carla-*{sys.version_info.major}.{sys.version_info.minor}-{"win-amd64" if os.name == "nt" else "linux-x86_64"}.egg')[0])
except IndexError:
    pass
import carla
import time
import random
import numpy as np
import math
from dataclasses import dataclass
import psutil
import subprocess
from queue import Queue


@dataclass
class ACTIONS:
    forward = 0
    left = 1
    right = 2
    forward_left = 3
    forward_right = 4
    brake = 5
    brake_left = 6
    brake_right = 7
    no_action = 8

ACTION_CONTROL = {
    0: [1, 0, 0],
    1: [0, 0, -1],
    2: [0, 0, 1],
    3: [1, 0, -1],
    4: [1, 0, 1],
    5: [0, 1, 0],
    6: [0, 1, -1],
    7: [0, 1, 1],
    8: None,
}

ACTIONS_NAMES = {
    0: 'forward',
    1: 'left',
    2: 'right',
    3: 'forward_left',
    4: 'forward_right',
    5: 'brake',
    6: 'brake_left',
    7: 'brake_right',
    8: 'no_action',
}


# Carla environment
class CarlaEnv:

    # How much steering to apply
    STEER_AMT = 1.0

    # Image dimensions (observation space)
    im_width = settings.IMG_WIDTH
    im_height = settings.IMG_HEIGHT

    # Action space size
    action_space_size = len(settings.ACTIONS)

    def __init__(self, carla_instance, seconds_per_episode=None, playing=False):

        # Set a client and timeouts
        self.client = carla.Client(*settings.CARLA_HOSTS[carla_instance][:2])
        self.client.set_timeout(2.0)

        # Get currently running world
        self.world = self.client.get_world()

        # Get list of actor blueprints
        blueprint_library = self.world.get_blueprint_library()

        # Get Tesla model 3 blueprint
        self.model_3 = blueprint_library.filter('model3')[0]

        # Sensors and helper lists
        self.collision_hist = []
        self.actor_list = []
        self.front_camera = None
        self.preview_camera = None

        # Used to determine if Carla simulator is still working as it crashes quite often
        self.last_cam_update = time.time()

        # Updated by agents for statistics
        self.seconds_per_episode = seconds_per_episode

        # A flag indicating that we are just going to play
        self.playing = playing

        # Used with additional preview feature
        self.preview_camera_enabled = False

        # Sets actually configured actions
        self.actions = [getattr(ACTIONS, action) for action in settings.ACTIONS]

    # Resets environment for new episode
    def reset(self):

        # Car, sensors, etc. We create them every episode then destroy
        self.actor_list = []

        # When Carla breaks (stopps working) or spawn point is already occupied, spawning a car throws an exception
        # We allow it to try for 3 seconds then forgive (will cause episode restart and in case of Carla broke inform
        # main thread that Carla simulator needs a restart)
        spawn_start = time.time()
        while True:
            try:
                # Get random spot from a list from predefined spots and try to spawn a car there
                self.transform = random.choice(self.world.get_map().get_spawn_points())
                self.vehicle = self.world.spawn_actor(self.model_3, self.transform)
                break
            except:
                time.sleep(0.01)

            # If that can't be done in 3 seconds - forgive (and allow main process to handle for this problem)
            if time.time() > spawn_start + 3:
                raise Exception('Can\'t spawn a car')

        # Append actor to a list of spawned actors, we need to remove them later
        self.actor_list.append(self.vehicle)

        # Get the blueprint for the camera
        self.rgb_cam = self.world.get_blueprint_library().find('sensor.camera.rgb')
        # Set sensor resolution and field of view
        self.rgb_cam.set_attribute('image_size_x', f'{self.im_width}')
        self.rgb_cam.set_attribute('image_size_y', f'{self.im_height}')
        self.rgb_cam.set_attribute('fov', '110')

        # Set camera sensor relative to a car
        transform = carla.Transform(carla.Location(x=2.5, z=0.7))

        # Attach camera sensor to a car, so it will keep relative difference to it
        self.sensor = self.world.spawn_actor(self.rgb_cam, transform, attach_to=self.vehicle)

        # Register a callback called every time sensor sends a new data
        self.sensor.listen(self._process_img)

        # Add camera sensor to the list of actors
        self.actor_list.append(self.sensor)

        # Preview ("above the car") camera
        if self.preview_camera_enabled is not False:
            # Get the blueprint for the camera
            self.preview_cam = self.world.get_blueprint_library().find('sensor.camera.rgb')
            # Set sensor resolution and field of view
            self.preview_cam.set_attribute('image_size_x', f'{self.preview_camera_enabled[0]:0f}')
            self.preview_cam.set_attribute('image_size_y', f'{self.preview_camera_enabled[1]:0f}')
            self.preview_cam.set_attribute('fov', '110')

            # Set camera sensor relative to a car
            transform = carla.Transform(carla.Location(x=self.preview_camera_enabled[2], y=self.preview_camera_enabled[3], z=self.preview_camera_enabled[4]))

            # Attach camera sensor to a car, so it will keep relative difference to it
            self.preview_sensor = self.world.spawn_actor(self.preview_cam, transform, attach_to=self.vehicle)

            # Register a callback called every time sensor sends a new data
            self.preview_sensor.listen(self._process_preview_img)

            # Add camera sensor to the list of actors
            self.actor_list.append(self.preview_sensor)

        # Here's first workaround. If we do not apply any control it takes almost a second for car to start moving
        # after episode restart. That starts counting once we aplly control for a first time.
        # Workarounf here is to apply both throttle and brakes and disengage brakes once we are ready to start an episode.
        self.vehicle.apply_control(carla.VehicleControl(throttle=1.0, brake=1.0))

        # And here's another workaround - it takes a bit over 3 seconds for Carla simulator to start accepting any
        # control commands. Above time adds to this one, but by those 2 tricks we start driving right when we start an episode
        # But that adds about 4 seconds of a pause time between episodes.
        time.sleep(4)

        # Collision history is a list callback is going to append to (we brake simulation on a collision)
        self.collision_hist = []

        # Get the blueprint of the sensor
        colsensor = self.world.get_blueprint_library().find('sensor.other.collision')

        # Create the collision sensor and attach ot to a car
        self.colsensor = self.world.spawn_actor(colsensor, carla.Transform(), attach_to=self.vehicle)

        # Register a callback called every time sensor sends a new data
        self.colsensor.listen(self._collision_data)

        # Add the collision sensor to the list of actors
        self.actor_list.append(self.colsensor)

        # Almost ready to start an episide, reset camera update variable
        self.last_cam_update = time.time()

        # Wait for a camera to send first image (important at the beginning of first episode)
        while self.front_camera is None or (self.preview_camera_enabled is not False and self.preview_camera is None):
            time.sleep(0.01)

        # Disengage brakes
        self.vehicle.apply_control(carla.VehicleControl(brake=0.0))

        # Remember a time of episode start (used to measure duration and set a terminal state)
        self.episode_start = time.time()

        # Return first observation space - current image from the camera sensor
        return [self.front_camera, 0]

    # Collidion data callback handler
    def _collision_data(self, event):

        # What we collided with and what was the impulse
        collision_actor_id = event.other_actor.type_id
        collision_impulse = math.sqrt(event.normal_impulse.x ** 2 + event.normal_impulse.y ** 2 + event.normal_impulse.z ** 2)

        # Filter collisions
        for actor_id, impulse in settings.COLLISION_FILTER:
            if actor_id in collision_actor_id and (impulse == -1 or collision_impulse <= impulse):
                return

        # Add collision
        self.collision_hist.append(event)

    # Camera sensor data callback handler
    def _process_img(self, image):

        # Get image, reshape and drop alpha channel
        image = np.array(image.raw_data)
        image = image.reshape((self.im_height, self.im_width, 4))
        image = image[:, :, :3]

        # Set as a current frame in environment
        self.front_camera = image

        # Measure frametime using a time of last camera update (displayed as Carla FPS)
        if self.playing:
            self.frametimes.append(time.time() - self.last_cam_update)
        else:
            self.frametimes.put_nowait(time.time() - self.last_cam_update)
        self.last_cam_update = time.time()

    # Preview camera sensor data callback handler
    def _process_preview_img(self, image):

        # If camera is disabled - do not process images
        if self.preview_camera_enabled is False:
            return

        # Get image, reshape and drop alpha channel
        image = np.array(image.raw_data)
        try:
            image = image.reshape((int(self.preview_camera_enabled[1]), int(self.preview_camera_enabled[0]), 4))
        except:
            return
        image = image[:, :, :3]

        # Set as a current frame in environment
        self.preview_camera = image

    # Steps environment
    def step(self, action):

        # Monitor if carla stopped sending images for longer than a second. If yes - it broke
        if time.time() > self.last_cam_update + 1:
            raise Exception('Missing updates from Carla')

        # Apply control to the vehicle based on an action
        if self.actions[action] != ACTIONS.no_action:
            self.vehicle.apply_control(carla.VehicleControl(throttle=ACTION_CONTROL[self.actions[action]][0], steer=ACTION_CONTROL[self.actions[action]][2]*self.STEER_AMT, brake=ACTION_CONTROL[self.actions[action]][1]))

        # Calculate speed in km/h from car's velocity (3D vector)
        v = self.vehicle.get_velocity()
        kmh = 3.6 * math.sqrt(v.x**2 + v.y**2 + v.z**2)

        done = False

        # If car collided - end and episode and send back a penalty
        if len(self.collision_hist) != 0:
            done = True
            reward = -1

        # Reward
        elif settings.WEIGHT_REWARDS_WITH_SPEED == 'discrete':
            reward = settings.SPEED_MIN_REWARD if kmh < 50 else settings.SPEED_MAX_REWARD

        elif settings.WEIGHT_REWARDS_WITH_SPEED == 'linear':
            reward = kmh * (settings.SPEED_MAX_REWARD - settings.SPEED_MIN_REWARD) / 100 + settings.SPEED_MIN_REWARD

        elif settings.WEIGHT_REWARDS_WITH_SPEED == 'quadratic':
            reward = (kmh / 100) ** 1.3 * (settings.SPEED_MAX_REWARD - settings.SPEED_MIN_REWARD) + settings.SPEED_MIN_REWARD

        # If episode duration limit reached - send back a terminal state
        if not self.playing and self.episode_start + self.seconds_per_episode.value < time.time():
            done = True

        # Weights rewards (not for terminal state)
        if not self.playing and settings.WEIGHT_REWARDS_WITH_EPISODE_PROGRESS and not done:
            reward *= (time.time() - self.episode_start) / self.seconds_per_episode.value

        return [self.front_camera, kmh], reward, done, None

    # Destroys all agents created from last .reset() call
    def destroy_agents(self):

        for actor in self.actor_list:

            # If it has a callback attached, remove it first
            if hasattr(actor, 'is_listening') and actor.is_listening:
                actor.stop()

            # If it's still alive - desstroy it
            if actor.is_alive:
                actor.destroy()

        self.actor_list = []


operating_system = operating_system()


# Returns binary
def get_binary():
    return 'CarlaUE4.exe' if operating_system == 'windows' else 'CarlaUE4.sh'


# Returns exec command
def get_exec_command():
    binary = get_binary()
    exec_command = binary if operating_system == 'windows' else ('./' + binary)

    return binary, exec_command


# tries to close, and if that does not work to kill all carla processes
def kill_processes():

    binary = get_binary()

    # Iterate processes and terminate carla ones
    for process in psutil.process_iter():
        if process.name().lower().startswith(binary.split('.')[0].lower()):
            try:
                process.terminate()
            except:
                pass

    # Check if any are still alive, create a list
    still_alive = []
    for process in psutil.process_iter():
        if process.name().lower().startswith(binary.split('.')[0].lower()):
            still_alive.append(process)

    # Kill process and wait until it's being killed
    if len(still_alive):
        for process in still_alive:
            try:
                process.kill()
            except:
                pass
        psutil.wait_procs(still_alive)


# Starts Carla simulator
def start(playing=False):
    # Kill Carla processes if there are any and start simulator
    if settings.CARLA_HOSTS_TYPE == 'local':
        print('Starting Carla...')
        kill_processes()
        for process_no in range(1 if playing else settings.CARLA_HOSTS_NO):
            subprocess.Popen(get_exec_command()[1] + f' -carla-rpc-port={settings.CARLA_HOSTS[process_no][1]}', cwd=settings.CARLA_PATH, shell=True)
            time.sleep(2)

    # Else just wait for it to be ready
    else:
        print('Waiting for Carla...')

    # Wait for Carla Simulator to be ready
    for process_no in range(1 if playing else settings.CARLA_HOSTS_NO):
        while True:
            try:
                client = carla.Client(*settings.CARLA_HOSTS[process_no][:2])
                map_name = client.get_world().get_map().name
                if len(settings.CARLA_HOSTS[process_no]) == 2 or not settings.CARLA_HOSTS[process_no][2]:
                    break
                if isinstance(settings.CARLA_HOSTS[process_no][2], int):
                    map_choice = random.choice([map.split('/')[-1] for map in client.get_available_maps()])
                else:
                    map_choice = settings.CARLA_HOSTS[process_no][2]
                if map_name != map_choice:
                    carla.Client(*settings.CARLA_HOSTS[process_no][:2]).load_world(map_choice)
                    while True:
                        try:
                            while carla.Client(*settings.CARLA_HOSTS[process_no][:2]).get_world().get_map().name != map_choice:
                                time.sleep(0.1)
                            break
                        except:
                            pass
                break
            except Exception as e:
                #print(str(e))
                time.sleep(0.1)


# Retarts Carla simulator
def restart(playing=False):
    # Kill Carla processes if there are any and start simulator
    if settings.CARLA_HOSTS_TYPE == 'local':
        for process_no in range(1 if playing else settings.CARLA_HOSTS_NO):
            subprocess.Popen(get_exec_command()[1] + f' -carla-rpc-port={settings.CARLA_HOSTS[process_no][1]}', cwd=settings.CARLA_PATH, shell=True)
            time.sleep(2)

    # Wait for Carla Simulator to be ready
    for process_no in range(1 if playing else settings.CARLA_HOSTS_NO):
        retries = 0
        while True:
            try:
                client = carla.Client(*settings.CARLA_HOSTS[process_no][:2])
                map_name = client.get_world().get_map().name
                if len(settings.CARLA_HOSTS[process_no]) == 2 or not settings.CARLA_HOSTS[process_no][2]:
                    break
                if isinstance(settings.CARLA_HOSTS[process_no][2], int):
                    map_choice = random.choice([map.split('/')[-1] for map in client.get_available_maps()])
                else:
                    map_choice = settings.CARLA_HOSTS[process_no][2]
                if map_name != map_choice:
                    carla.Client(*settings.CARLA_HOSTS[process_no][:2]).load_world(map_choice)
                    while True:
                        try:
                            while carla.Client(*settings.CARLA_HOSTS[process_no][:2]).get_world().get_map().name != map_choice:
                                time.sleep(0.1)
                                retries += 1
                                if retries >= 60:
                                    raise Exception('Couldn\'t change map [1]')
                            break
                        except Exception as e:
                            time.sleep(0.1)
                        retries += 1
                        if retries >= 60:
                            raise Exception('Couldn\'t change map [2]')

                break
            except Exception as e:
                #print(str(e))
                time.sleep(0.1)

            retries += 1
            if retries >= 60:
                break


# Parts of weather control code and npc car spawn code are copied from dynamic_weather.py and spawn_npc.py from examples, then modified
def clamp(value, minimum=0.0, maximum=100.0):
    return max(minimum, min(value, maximum))


class Sun(object):
    def __init__(self, azimuth, altitude):
        self.azimuth = azimuth
        self.altitude = altitude
        self._t = 0.0

    def tick(self, delta_seconds):
        self._t += 0.008 * delta_seconds
        self._t %= 2.0 * math.pi
        self.azimuth += 0.25 * delta_seconds
        self.azimuth %= 360.0
        self.altitude = 35.0 * (math.sin(self._t) + 1.0)


class Storm(object):
    def __init__(self, precipitation):
        self._t = precipitation if precipitation > 0.0 else -50.0
        self._increasing = True
        self.clouds = 0.0
        self.rain = 0.0
        self.puddles = 0.0
        self.wind = 0.0

    def tick(self, delta_seconds):
        delta = (1.3 if self._increasing else -1.3) * delta_seconds
        self._t = clamp(delta + self._t, -250.0, 100.0)
        self.clouds = clamp(self._t + 40.0, 0.0, 90.0)
        self.rain = clamp(self._t, 0.0, 80.0)
        delay = -10.0 if self._increasing else 90.0
        self.puddles = clamp(self._t + delay, 0.0, 75.0)
        self.wind = clamp(self._t - delay, 0.0, 80.0)
        if self._t == -250.0:
            self._increasing = True
        if self._t == 100.0:
            self._increasing = False


class Weather(object):
    def __init__(self, weather):
        self.weather = weather
        self.sun = Sun(weather.sun_azimuth_angle, weather.sun_altitude_angle)
        self.storm = Storm(weather.precipitation)

    def set_new_weather(self, weather):
        self.weather = weather

    def tick(self, delta_seconds):
        delta_seconds += random.uniform(-0.1, 0.1)
        self.sun.tick(delta_seconds)
        self.storm.tick(delta_seconds)
        self.weather.cloudyness = self.storm.clouds
        self.weather.precipitation = self.storm.rain
        self.weather.precipitation_deposits = self.storm.puddles
        self.weather.wind_intensity = self.storm.wind
        self.weather.sun_azimuth_angle = self.sun.azimuth
        self.weather.sun_altitude_angle = self.sun.altitude


# Carla settings states
@dataclass
class CARLA_SETTINGS_STATE:
    starting = 0
    working = 1
    restarting = 2
    finished = 3
    error = 4


# Carla settings state messages
CARLA_SETTINGS_STATE_MESSAGE = {
    0: 'STARTING',
    1: 'WORKING',
    2: 'RESTARING',
    3: 'FINISHED',
    4: 'ERROR',
}

# Carla settings class
class CarlaEnvSettings:

    def __init__(self, process_no, agent_pauses, stop=None, car_npcs=[0, 0], stats=[0., 0., 0., 0., 0., 0.]):

        # Speed factor changes how fast weather should change
        self.speed_factor = 1.0

        # Process number (Carla instane to use)
        self.process_no = process_no

        # Weather and NPC variables
        self.weather = None
        self.spawned_car_npcs = {}

        # Set stats (for Tensorboard)
        self.stats = stats

        # Set externally to restarts settings
        self.restart = False

        # Controls number of NPCs and reset interval
        self.car_npcs = car_npcs

        # State for stats
        self.state = CARLA_SETTINGS_STATE.starting

        # External stop object (used to "know" when to exit
        self.stop = stop

        # We want to track NPC collisions so we can remove and spawn new ones
        # Collisions are really not rare when using built-in autopilot
        self.collisions = Queue()

        # Name of current world
        self.world_name = None

        # Controls world reloads
        self.reload_world_every = None if len(settings.CARLA_HOSTS[process_no]) == 2 or not settings.CARLA_HOSTS[process_no][2] or not isinstance(settings.CARLA_HOSTS[process_no][2], int) else (settings.CARLA_HOSTS[process_no][2] + random.uniform(-settings.CARLA_HOSTS[process_no][2]/10, settings.CARLA_HOSTS[process_no][2]/10))*60
        self.next_world_reload = None if self.reload_world_every is None else time.time() + self.reload_world_every

        # List of communications objects allowing Carla to pause agents (on changes like world change)
        self.agent_pauses = agent_pauses

    # Collect NPC collision data
    def _collision_data(self, collision):
        self.collisions.put(collision)

    # Destroys given car NPC
    def _destroy_car_npc(self, car_npc):

        # First check if NPC is still alive
        if car_npc in self.spawned_car_npcs:

            # Iterate all agents (currently car itself and collision sensor)
            for actor in self.spawned_car_npcs[car_npc]:

                # If actor has any callback attached - stop it
                if hasattr(actor, 'is_listening') and actor.is_listening:
                    actor.stop()

                # And if is still alive - destroy it
                if actor.is_alive:
                    actor.destroy()

            # Remove from car NPCs' list
            del self.spawned_car_npcs[car_npc]

    def clean_carnpcs(self):

        # If there were any NPC cars - remove attached callbacks from it's agents
        for car_npc in self.spawned_car_npcs.keys():
            for actor in self.spawned_car_npcs[car_npc]:
                if hasattr(actor, 'is_listening') and actor.is_listening:
                    actor.stop()

        # Reset NPC car list
        self.spawned_car_npcs = {}

    # Main method, being run in a thread
    def update_settings_in_loop(self):

        # Reset world name
        self.world_name = None

        # Reset weather object
        self.weather = None

        # Run infinitively
        while True:

            # Release agent pause locks, if there are any
            for agent_pause in self.agent_pauses:
                agent_pause.value = 0

            # Carla might break, make sure we can handle for that
            try:

                # If stop flag - exit
                if self.stop is not None and self.stop.value == STOP.stopping:
                    self.state = CARLA_SETTINGS_STATE.finished
                    return

                # If restart flag is being set - wait
                if self.restart:
                    self.state = CARLA_SETTINGS_STATE.restarting
                    time.sleep(0.1)
                    continue

                # Clean car npcs
                self.clean_carnpcs()

                # Connect to Carla, get worls and map
                self.client = carla.Client(*settings.CARLA_HOSTS[self.process_no][:2])
                self.client.set_timeout(2.0)
                self.world = self.client.get_world()
                self.map = self.world.get_map()
                self.world_name = self.map.name

                # Create weather object or update it if exists
                if self.weather is None:
                    self.weather = Weather(self.world.get_weather())
                else:
                    self.weather.set_new_weather(self.world.get_weather())

                # Get car blueprints and filter them
                self.car_blueprints = self.world.get_blueprint_library().filter('vehicle.*')
                self.car_blueprints = [x for x in self.car_blueprints if int(x.get_attribute('number_of_wheels')) == 4]
                self.car_blueprints = [x for x in self.car_blueprints if not x.id.endswith('isetta')]
                self.car_blueprints = [x for x in self.car_blueprints if not x.id.endswith('carlacola')]

                # Get a list of all possible spawn points
                self.spawn_points = self.map.get_spawn_points()

                # Get collision sensor blueprint
                self.collision_sensor = self.world.get_blueprint_library().find('sensor.other.collision')

                # Used to know when to reset next NPC car
                car_despawn_tick = 0

                # Set state to working
                self.state = CARLA_SETTINGS_STATE.working

            # In case of error, report it, wait a second and try again
            except Exception as e:
                self.state = CARLA_SETTINGS_STATE.error
                time.sleep(1)
                continue

            # Steps all settings
            while True:

                # Used to measure sleep time at the loop end
                step_start = time.time()

                # If stop flag - exit
                if self.stop is not None and self.stop.value == STOP.stopping:
                    self.state = CARLA_SETTINGS_STATE.finished
                    return

                # Is restart flag is being set, break inner loop
                if self.restart:
                    break

                # Carla might break, make sure we can handle for that
                try:

                    # World reload
                    if self.next_world_reload and time.time() > self.next_world_reload:

                        # Set restart flag
                        self.state = CARLA_SETTINGS_STATE.restarting
                        # Clean car npcs
                        self.clean_carnpcs()

                        # Set pause lock flag
                        for agent_pause in self.agent_pauses:
                            agent_pause.value = 1

                        # Wait for agents to stop playing and acknowledge
                        for agent_pause in self.agent_pauses:
                            while agent_pause.value != 2:
                                time.sleep(0.1)

                        # Get random map and load it
                        map_choice = random.choice(list({map.split('/')[-1] for map in self.client.get_available_maps()} - {self.client.get_world().get_map().name}))
                        self.client.load_world(map_choice)

                        # Wait for world to be fully loaded
                        retries = 0
                        while self.client.get_world().get_map().name != map_choice:
                            retries += 1
                            if retries >= 600:
                                raise Exception('Timeout when waiting for new map to be fully loaded')
                            time.sleep(0.1)

                        # Inform agents that they can start playing
                        for agent_pause in self.agent_pauses:
                            agent_pause.value = 3

                        # Wait for agents to start playing
                        for agent_pause in self.agent_pauses:
                            retries = 0
                            while agent_pause.value != 0:
                                retries += 1
                                if retries >= 600:
                                    break
                            time.sleep(0.1)

                        self.next_world_reload += self.reload_world_every

                        break

                    # Handle all registered collisions
                    while not self.collisions.empty():

                        # Gets first collision from the queue
                        collision = self.collisions.get()

                        # Gets car NPC's id and destroys it
                        car_npc = collision.actor.id
                        self._destroy_car_npc(car_npc)

                    # Count tick
                    car_despawn_tick += 1

                    # Carla autopilot might cause cars to stop in the middle of intersections blocking whole traffic
                    # On some intersections there might be only one car moving
                    # We want to check for cars stopped at intersections and remove them
                    # Without that most of the cars can be waiting around 2 intersections
                    for car_npc in self.spawned_car_npcs.copy():

                        # First check if car is moving
                        # It;s a simple check, not proper velocity calculation
                        velocity = self.spawned_car_npcs[car_npc][0].get_velocity()
                        simple_speed = velocity.x + velocity.y + velocity.z

                        # If car is moving, continue loop
                        if simple_speed > 0.1 or simple_speed < -0.1:
                            continue

                        # Next get current location of the car, then a waypoint then check if it's intersection
                        location = self.spawned_car_npcs[car_npc][0].get_location()
                        waypoint = self.map.get_waypoint(location)
                        if not waypoint.is_intersection:
                            continue

                        # Car is not moving, it's intersection - destroy a car
                        self._destroy_car_npc(car_npc)

                    # If we reached despawn tick, remove oldest NPC
                    # The reson we want to do that is to rotate cars aroubd the map
                    if car_despawn_tick >= self.car_npcs[1] and len(self.spawned_car_npcs):

                        # Get id of the first car on a list and destroy it
                        car_npc = list(self.spawned_car_npcs.keys())[0]
                        self._destroy_car_npc(car_npc)
                        car_despawn_tick = 0

                    # If there is less number of car NPCs then desired amount - spawn remaining ones
                    # but up to 10 at the time
                    if len(self.spawned_car_npcs) < self.car_npcs[0]:

                        # How many cars to spawn (up to 10)
                        cars_to_spawn = min(10, self.car_npcs[0] - len(self.spawned_car_npcs))

                        # Sometimes we can;t spawn a car
                        # It might be because spawn point is being occupied or because Carla broke
                        # We count errores and break on 5
                        retries = 0

                        # Iterate over number of cars to spawn
                        for _ in range(cars_to_spawn):

                            # Break if too many errors
                            if retries >= 5:
                                break

                            # Get random car blueprint and randomize color and enable autopilot
                            car_blueprint = random.choice(self.car_blueprints)
                            if car_blueprint.has_attribute('color'):
                                color = random.choice(car_blueprint.get_attribute('color').recommended_values)
                                car_blueprint.set_attribute('color', color)
                            car_blueprint.set_attribute('role_name', 'autopilot')

                            # Try to spawn a car
                            for _ in range(5):
                                try:
                                    # Get random spot from a list from predefined spots and try to spawn a car there
                                    spawn_point = random.choice(self.spawn_points)
                                    car_actor = self.world.spawn_actor(car_blueprint, spawn_point)
                                    car_actor.set_autopilot()
                                    break
                                except:
                                    retries += 1
                                    time.sleep(0.1)
                                    continue

                            # Create the collision sensor and attach it to the car
                            colsensor = self.world.spawn_actor(self.collision_sensor, carla.Transform(), attach_to=car_actor)

                            # Register a callback called every time sensor sends a new data
                            colsensor.listen(self._collision_data)

                            # Add the car and collision sensor to the list of car NPCs
                            self.spawned_car_npcs[car_actor.id] = [car_actor, colsensor]

                    # Tick a weather and set it in Carla
                    self.weather.tick(self.speed_factor)
                    self.world.set_weather(self.weather.weather)

                    # Set stats for tensorboard
                    self.stats[0] = len(self.spawned_car_npcs)
                    self.stats[1] = self.weather.sun.azimuth
                    self.stats[2] = self.weather.sun.altitude
                    self.stats[3] = self.weather.storm.clouds
                    self.stats[4] = self.weather.storm.wind
                    self.stats[5] = self.weather.storm.rain

                    # In case of state being some other one report that everything is working
                    self.state = CARLA_SETTINGS_STATE.working

                    # Calculate how long to sleep and sleep
                    sleep_time = self.speed_factor - time.time() + step_start
                    if sleep_time > 0:
                        time.sleep(sleep_time)

                # In case of error, report it (reset flag set externally might break this loop only)
                except Exception as e:
                    #print(str(e))
                    self.state = CARLA_SETTINGS_STATE.error