import tminterface.util as util
import struct
from enum import IntEnum
from typing import Union

SIM_HAS_TIMERS = 0x1
SIM_HAS_DYNA = 0x2
SIM_HAS_SCENE_MOBIL = 0x4
SIM_HAS_SIMULATION_WHEELS = 0x8
SIM_HAS_PLUG_SOLID = 0x10
SIM_HAS_CMD_BUFFER_CORE = 0x20
SIM_HAS_INPUT_STATE = 0x40
SIM_HAS_PLAYER_INFO = 0x80

MODE_SIMULATION = 0
MODE_RUN = 1

BINARY_ACCELERATE_NAME = 'Accelerate'
BINARY_BRAKE_NAME = 'Brake'
BINARY_LEFT_NAME = 'Steer left'
BINARY_RIGHT_NAME = 'Steer right'
BINARY_RESPAWN_NAME = 'Respawn'
BINARY_HORN_NAME = 'Horn'
BINARY_RACE_START_NAME = '_FakeIsRaceRunning'
BINARY_RACE_FINISH_NAME = '_FakeFinishLine'
ANALOG_STEER_NAME = 'Steer (analog)'
ANALOG_ACCELERATE_NAME = 'Accelerate (analog)'


class Event(object):
    '''
    The Event class represents a game event (or input) with its respective time.

    Such event is stored in 8 bytes internally by the game. The first 4 bytes is the time
    of the event. This time is a stored time, which means it is offset by 100000ms.

    The last 4 bytes contain the event data. This data contains the actual event type
    (e.g. whether it was acceleration, braking, steering etc.) and the value of the event.

    The event type is 1 byte long and it signifes an index into an array of available event types.
    This array is variable based on the information contained within the replay file. As such,
    it is required to get index of the desired event type dynamically. You can easily get/set this property
    through the name_index accessors.

    The event value depends on the event type and is 3 bytes long. If the event type is binary
    (e.g. accelerate, brake, steer left), the value can be either 0 or 1. For managing this value type,
    use the binary_value getter/setter.

    If the event type is analog, the value is stored in a custom format. You can convert between this
    format and the format TMInterface uses by using util.data_to_analog_value and utils.analog_value_to_data.
    You can avoid using these functions simply by using the analog_value getter/setter.

    Args:
        time (int): the stored time of the event

    Atrributes:
        time (int): the stored time of the event
        data (int): the final data that is written into game's memory
    '''
    def __init__(self, time: int, data: int = 0):
        self.time = time
        self.data = data

    @property
    def name_index(self) -> int:
        return self.data >> 24

    @name_index.setter
    def name_index(self, index: int):
        self.data &= 0xFFFFFF
        self.data |= (index << 24)

    @property
    def binary_value(self) -> bool:
        return bool(self.data & 0xFFFFFF)

    @binary_value.setter
    def binary_value(self, value: bool):
        self.data = self.data & 0xFF000000 | int(value)

    @property
    def analog_value(self) -> int:
        return util.data_to_analog_value(self.data & 0xFFFFFF)

    @analog_value.setter
    def analog_value(self, value: int):
        self.data = self.data & 0xFF000000 | (util.analog_value_to_data(value) & 0xFFFFFF)


class EventBufferData(object):
    '''
    The internal event buffer used to hold player inputs in simulation mode.

    While simulating a race, the game loads the inputs from a replay file
    into an internal buffer and begins to apply "events" (inputs) from this
    buffer. The buffer itself consists of 8 byte values, the first 4 bytes
    is used for the event time and the last 4 is used for event data.
    See the Event class for more information.

    The event time is so called a "stored" time. The stored time is
    defined as 100000 + race time. The stored time is saved in the
    replay file and is also used in the internal buffer itself.

    The buffer itself is stored in *decreasing* order. That means that the event
    at index 0 in the list is the last one simulated in the race. The start and end
    of the race is marked by special "_FakeIsRaceRunning" and "_FakeFinishLine" events.
    These events mark the start and finish of the race, note that without the presence
    of "_FakeIsRaceRunning" event, the race will not start at all. This event has a
    constant stored time of 100000. 
    
    Before the starting event, a "Respawn" event can be generated by the game, this
    event can also be saved in the replay file itself. The very first input that can be applied
    by the player happens at stored time of 100010.

    Arguments:
        events_duration (int): the duration of the events, equalling the finish time, mostly ignored and does not need to be set
    
    Attributes:
        events_duration (int): the duration of the events
        control_names (list): the list of supported event types by this buffer
        events (list): the list of events held by this buffer
    '''
    def __init__(self, events_duration: int):
        self.events_duration = events_duration
        self.control_names = []
        self.events = []

    '''
    Copies the event buffer with all its events.

    Returns:
        a deep copy of the original event buffer
    '''
    def copy(self):
        cpy = EventBufferData(self.events_duration)
        cpy.events = self.events[:]
        return cpy

    '''
    Removes all events in the current event buffer.
    '''
    def clear(self):
        self.events = []

    '''
    Sorts the event buffer by time in decreasing order.

    This is the order that events are stored internally by the game.
    '''
    def sort(self):
        self.events = sorted(self.events, key=lambda ev: ev.time, reverse=True)

    '''
    Adds an event to the event buffer.

    This is a wrapper function that provides easy API for adding new events.
    Depending on the event_name parameter, the method will interpret the value
    in different ways. If the event is an analog event, the value passed should
    be in the range of [-65536, 65536] where negative values represent left steering
    and postive, right steering.
    
    If the event is binary, the value should be False for disabling the input and 
    True for enabling it.

    The time parameter is zero based, where 0 is the first human input that can be injected.
    Internally, 0 translates to stored time 100010, which is the first simulation step
    after the countdown.

    Args:
        time (int): zero based timestamp when the input is injected
        event_name (str): the event name that specifies the input type
        value (Union[int, bool]): the value for the event, based on the event typ
    '''
    def add(self, time: int, event_name: str, value: Union[int, bool]):
        try:
            index = self.control_names.index(event_name)
        except ValueError:
            raise ValueError(f'Event name "{event_name}" does not exist in this event buffer')

        ev = Event(time + 100010)
        ev.name_index = index
        if event_name == ANALOG_ACCELERATE_NAME or event_name == ANALOG_STEER_NAME:
            ev.analog_value = value
        else:
            ev.binary_value = value

        self.events.append(ev)

    '''
    Finds matching events according to keyword arguments.

    Any unspecified parameter will be skipped in the search and will not be compared.
    You may use this method to filter events based on time, event type and value. 

    Find all analog steering events with value -65536:
        matching = event_buffer.find(event_name=structs.ANALOG_STEER_NAME, value=-65536)

    Find all events that happened at input time 0:
        matching = event_buffer.find(time=0)

    Find the finish line event:
        matching = event_buffer.find(event_name=structs.BINARY_RACE_FINISH_NAME, value=True)

    Args:
        **kwargs: the keyword arguments
    
    Keyword Args:
        event_name (str): match events with this event type
        time (int): match events with this time (zero based)
        value (Union[int, bool]): match events with this value, bool if the event type is binary, int if analog,
              this parameter can only be filtered if event_name is provided

    Returns:
        list of the events that matched the query
    '''
    def find(self, **kwargs):
        index = -1
        if 'event_name' in kwargs:
            try:
                index = self.control_names.index(kwargs['event_name'])
            except ValueError:
                raise ValueError(f'Event name "{kwargs["event_name"]}" does not exist in this event buffer')

        has_value = 'value' in kwargs
        has_time = 'time' in kwargs

        matched = []
        for ev in self.events:
            if has_time and ev.time - 100010 != kwargs['time']:
                continue

            if index >= 0:
                if ev.name_index != index:
                    continue

                if has_value:
                    if kwargs['event_name'] == ANALOG_STEER_NAME or kwargs['event_name'] == ANALOG_ACCELERATE_NAME:
                        if ev.analog_value != kwargs['value']:
                            continue
                    else:
                        if ev.binary_value != kwargs['value']:
                            continue

                matched.append(ev)

        return matched


class CheckpointData(object):
    '''
    The game keeps track of two arrays that contain checkpoint information.

    The first "state" array is an array of booleans (a boolean is 4 bytes long)
    and keeps track of which checkpoints were already passed. The length of the 
    array represents the real count of the checkpoints on current the map (including finish).
    This does not mean that to finish the race the player has to pass through this exact count
    of checkpoints. A map with 3 laps and 5 checkpoints will still contain only 5 checkpoint states.

    The second "times" array is an array of structures with 2 fields: time and an unknown field.
    This array holds the "logical" number of checkpoints that have to be passed (including finish).
    This means the total number of checkpoint passes, including the existence of laps.

    Arguments:
        cp_states (list): the checkpoint states array
        cp_times (list): the checkpoint times array, each element is a two element tuple of (time, flags)
    '''
    def __init__(self, cp_states: list, cp_times: list):
        self.cp_states = cp_states
        self.cp_times = cp_times


class SimStateData(object):
    '''
    The SimStateData object represents a full save state of the simulation state,
    including checkpoint and input information.

    The simulation state consists of raw memory buffers representing various
    information about the race state. This includes the entirety of the vehicle
    state as well as the player info and other state variables such as current
    checkpoint count and input state.

    The memory regions themselves are monitored by TMInterface itself and are used 
    for functionality like save states or fast rewind in the bruteforce script.
    TMInterface may use additional native game methods to restore the state based
    on information present in some of these memory regions. It is important to note
    that the buffers contain instance specific fields such as pointers and array sizes.
    These are masked out automatically by TMInterface when restoring the state
    (and when calling TMInterface.rewind_to_state).
    '''
    def __init__(self):
        self.version = 0
        self.context_mode = MODE_RUN
        self.flags = 0
        self.timers = bytearray()
        self.dyna = bytearray()
        self.scene_mobil = bytearray()
        self.simulation_wheels = bytearray()
        self.plug_solid = bytearray()
        self.cmd_buffer_core = bytearray()
        self.player_info = bytearray()
        self.internal_input_state = bytearray()
        self.input_running_state = Event(0)
        self.input_finish_state = Event(0)
        self.input_accelerate_state = Event(0)
        self.input_brake_state  = Event(0)
        self.input_left_state = Event(0)
        self.input_right_state = Event(0)
        self.input_steer_state = Event(0)
        self.input_gas_state = Event(0)
        self.num_respawns = 0
        self.cp_data = None

    def get_time(self) -> int:
        if (self.flags & SIM_HAS_TIMERS) == 0:
            return 0

        return self.__get_int(self.timers, 4)

    def get_position(self) -> list:
        if (self.flags & SIM_HAS_DYNA) == 0:
            return [0, 0, 0]
        
        x = struct.unpack('f', self.dyna[500:504])[0]
        y = struct.unpack('f', self.dyna[504:508])[0]
        z = struct.unpack('f', self.dyna[508:512])[0]
        return [x, y, z]

    def get_velocity(self) -> list:
        if (self.flags & SIM_HAS_DYNA) == 0:
            return [0, 0, 0]
        
        x = struct.unpack('f', self.dyna[512:516])[0]
        y = struct.unpack('f', self.dyna[516:520])[0]
        z = struct.unpack('f', self.dyna[520:524])[0]
        return [x, y, z]

    def get_aim_direction(self) -> list:
        if (self.flags & SIM_HAS_DYNA) == 0:
            return [0, 0, 0]
        
        x = struct.unpack('f', self.dyna[488:492])[0]
        y = struct.unpack('f', self.dyna[492:496])[0]
        z = struct.unpack('f', self.dyna[496:500])[0]
        return [x, y, z]

    # Available only in run context
    def get_display_speed(self) -> int:
        if (self.flags & SIM_HAS_PLAYER_INFO) == 0:
            return 0

        return self.__get_int(self.player_info, 832)

    def set_position(self, pos: list) -> bool:
        if (self.flags & SIM_HAS_DYNA) == 0:
            return False

        self.dyna[500:504] = list(struct.pack('f', pos[0]))
        self.dyna[504:508] = list(struct.pack('f', pos[1]))
        self.dyna[508:512] = list(struct.pack('f', pos[2]))
        return True

    def set_velocity(self, vel: list) -> bool:
        if (self.flags & SIM_HAS_DYNA) == 0:
            return False

        self.dyna[512:516] = list(struct.pack('f', vel[0]))
        self.dyna[516:520] = list(struct.pack('f', vel[1]))
        self.dyna[520:524] = list(struct.pack('f', vel[2]))
        return True

    def set_aim_direction(self, aim: list) -> bool:
        if (self.flags & SIM_HAS_DYNA) == 0:
            return False

        self.dyna[488:492] = list(struct.pack('f', aim[0]))
        self.dyna[492:496] = list(struct.pack('f', aim[1]))
        self.dyna[496:500] = list(struct.pack('f', aim[2]))
        return True

    def get_race_time(self) -> int:
        if (self.flags & SIM_HAS_PLAYER_INFO) == 0:
            return False

        return self.__get_int(self.player_info, 688)

    def get_rewind_time(self) -> int:
        return self.get_race_time() + 10

    @staticmethod
    def __get_int(buffer, offset: int) -> int:
        return int.from_bytes(buffer[offset:offset+4], byteorder='little')


class BFPhase(IntEnum):
    INITIAL = 0
    SEARCH = 1


class BFTarget(IntEnum):
    FINISH_TIME = 0
    CHECKPOINT_TIME = 1
    TRIGGER = 2


class BFEvaluationDecision(IntEnum):
    CONTINUE = 0
    DO_NOTHING = 1
    ACCEPT = 2
    REJECT = 3
    STOP = 4


class BFEvaluationInfo(object):
    def __init__(self) -> None:
        self.phase = BFPhase.INITIAL
        self.target = BFTarget.FINISH_TIME
        self.time = 0
        self.modified_inputs_num = -1
        self.inputs_min_time = -1
        self.inputs_max_time = -1
        self.max_steer_diff = -1
        self.max_time_diff = -1
        self.override_stop_time = -1
        self.search_forever = False
        self.inputs_extend_steer = False


class BFEvaluationResponse(object):
    def __init__(self) -> None:
        self.decision = BFEvaluationDecision.CONTINUE
        self.rewind_time = -1
