# GPLv3 or later
# ( C ) J.Y.Amihud 2024

# This module is to deal with racing.

import os
import bge
import bpy
import json
import datetime

from Scripts import Script
from Scripts import Money
from Scripts import Map
from Scripts import Reuse

from Scripts.Common import *

def intime(race):

    # Checks if the current ingame corresponds
    # to the time at which this race is available.

    TimeRange = race.get("TimeRange")
    timeis    = bge.logic.globalDict.get("time", 0)

    if not TimeRange:
        return True

    # Standard ranges, like 12 -> 14, or 6 -> 12
    elif TimeRange[0] < TimeRange[1]:
        return TimeRange[0] <= timeis <= TimeRange[1]

    # Other ranges, like 21 -> 6, 17 -> 12
    else:
        return timeis < TimeRange[1] or timeis > TimeRange[0]
        
def renderTimeRange(race):

    # Renders a time range of the race.

    TimeRange = race.get("TimeRange")

    min0 = str(int(60*(TimeRange[0]-int(TimeRange[0]))))
    if len(min0) < 2: min0 = "0"+min0
    min1 = str(int(60*(TimeRange[1]-int(TimeRange[1]))))
    if len(min1) < 2: min1 ="0"+min1

    hrs0 = str(int(TimeRange[0]))
    if len(hrs0) < 2: hrs0 = "0"+hrs0
    hrs1 = str(int(TimeRange[1]))
    if len(hrs1) < 2: hrs1 = "0"+hrs1
    
    r = "between "+hrs0+":"+min0+" and "+hrs1+":"+min1
    return r

def Races():

    return bge.logic.globalDict["races"]

def MainLoop(spawnAtDistance=250):

    # This runs in main() on every frame
    # to make sure races work.

    races = Races()

    scene = bge.logic.getCurrentScene()
    keys = bge.logic.globalDict["keys"]    
    dani = scene.objects["Dani_Box"]
    cam = scene.active_camera
    
    for racename in races:
        race = races[racename]

        # Show the race on the map
        if Available(race, formap=True):
            Map.Show(race["starters"][0]["location"], icon="Map_Circle", color=[0.01,0.01,1], ID=racename)
        
        # Starting the race!
        if Available(race):

            for starter in race["starters"]:
                
                # Blue cylinder
                if dani.getDistanceTo(starter["location"]) < spawnAtDistance:
                    if not starter["cylinder"]:
                        AddCylinder("Starter.Cylinder", starter)
                elif starter["cylinder"]:
                    DeleteCylinder(starter)
    
                if dani.getDistanceTo(starter["location"]) < starter["radius"]:
                    if CanStartRace(race, dani):

                        # Start the race
                        if keycodes["R"] in keys:                            
                            PayBid(race, dani)
                            StartRace(racename, dani)

        # When the race is going!
        elif dani.get("race") == racename:

            # Next checkpoint in the race
            nextcheck = race["checkpoints"][dani["checkpoint"]]
            nextcheckpoint = race["checkpoints"][ ( dani.get("checkpoint") + 1 ) % len(race["checkpoints"]) ]

            # UI objects
            pointer = scene.objects["PointingArrow"]
            posindicator = scene.objects["Dani_Pos_Indicator"]
            timeindicator = scene.objects["Dani_Time_Indicator"]
            lapindicator = scene.objects["Dani_Lap_Indicator"]
            
            # Alight the arrow to the checkpoint
            tocheck = pointer.getVectTo(nextcheck["location"])
            pointer.alignAxisToVect(tocheck[1], 1, 0.1)
            if cam.pointInsideFrustum(nextcheck["location"]) == cam.INSIDE:
                tonextcheck = pointer.getVectTo(nextcheckpoint["location"])
                pointer.alignAxisToVect(tonextcheck[1], 1, min(1, max(0, 1-(tocheck[0]/500))) ** 5 ) 
            pointer.alignAxisToVect( (0,0,1), 2, 1.0 )

            # Time of the race
            currentRaceTime = InRaceTime(race)
            formattedTime = FormatInRaceTime(currentRaceTime)
            timeindicator["Text"] = formattedTime

            # Current lap
            lapindicator["Text"] = str(dani["lap"]+1)+"/"+str(race["laps"])


            # Adding the next cylinder
            if not nextcheck["cylinder"] and dani.getDistanceTo(nextcheck["location"]) < spawnAtDistance:
                if dani["checkpoint"]+1 == len(race["checkpoints"]):
                    cylinderType = "Starter.Cylinder"
                else:
                    cylinderType = "Tag.Cylinder"
                AddCylinder(cylinderType, nextcheck)

            # Show two next cylinders on the map
            Map.Show(nextcheck["location"], icon="Map_Circle", color=[1,0.3,0], ID="RaceCheckpoint")
            Map.Show(nextcheckpoint["location"], icon="Map_Circle", color=[1,0.1,0], ID="RaceCheckpoint2")

            # Next checkpoint
            if dani.getDistanceTo(nextcheck["location"]) < nextcheck["radius"]*1.5:

                # Recording dani's resque point
                if not nextcheck.get("OnLoop"):
                    dani["prevrescue"] = dani.get("rescue")
                    dani["rescue"] = nextcheck["location"]
                    dani["rescueTo"] = dani["checkpoint"]

                DeleteCylinder(nextcheck, ignoreTimer=True)

                # Logging player data for NPC training
                try:
                    nextcheck["speed"] = -dani["driving"].localLinearVelocity[1] 
                    nextcheck["time"] = currentRaceTime
                    nextcheck["rot"] = list(dani["driving"].orientation.to_euler())
                except:pass

                # Advancing a checkpoint
                dani["checkpoint"] += 1

                # Playing a Ding
                bge.logic.globalDict["SoundDevice"].play(bge.logic.globalDict["sounds"]["checkpoint"]["sound"])

            # Next lap
            if dani["checkpoint"] == len(race["checkpoints"]):
                dani["checkpoint"] = 0
                dani["lap"] += 1

            # Finishing the race
            if dani["lap"] == race["laps"]:

                FinishRace(racename, dani)
                DeleteCylinder(nextcheck, ignoreTimer=False)
                continue

            UpdatePositions(race, dani)
            
def StartRace(racename, dani=None):

    
    race = Races()[racename]
    race["started"] = True
    race["start-time"] = bge.logic.getRealTime()

    # Deleting starters
    for starter in race["starters"]:
        if starter["cylinder"]:
            DeleteCylinder(starter, ignoreTimer=True)

    # Making the racers race
    for racer in race["racers"]:

        racer["racing"] = True
        racer["launchtime"] = 100
        racer["race"] = racename
        
        # Testing
        racer["npc"] = "racer"
        racer["active"] = False

        # Beam of energy
        racer["beam"] = Reuse.Create("Racer.Indicator")

    # If the race includes dani.
    if dani:
        dani["race"] = racename
        dani["checkpoint"] = 0   
        dani["lap"] = 0
        

        # Make the racing UI visible
        scene = bge.logic.getCurrentScene()
        pointer = scene.objects["PointingArrow"]
        posindicator = scene.objects["Dani_Pos_Indicator"]
        timeindicator = scene.objects["Dani_Time_Indicator"]
        lapindicator = scene.objects["Dani_Lap_Indicator"]
        
        posindicator.visible = True
        pointer.visible = True
        timeindicator.visible = True
        lapindicator.visible = True

        # Play the start the race sound.
        bge.logic.globalDict["SoundDevice"].play(bge.logic.globalDict["sounds"]["active"]["sound"]) # Play a Ding

def FinishRace(racename, dani=None):

    race = Races()[racename]
    race["started"] = False

    # Removing racers from the race

    race["racer_spawns"] = []
    for racer in race["racers"]:
        racer["npc"] = "npc"
        racer["beam"].endObject()
    race["racers"] = []

    if dani:

        # Make the racing UI invisible
        scene = bge.logic.getCurrentScene()
        pointer = scene.objects["PointingArrow"]
        posindicator = scene.objects["Dani_Pos_Indicator"]
        timeindicator = scene.objects["Dani_Time_Indicator"]
        lapindicator = scene.objects["Dani_Lap_Indicator"]

        posindicator.visible = False
        timeindicator.visible = False
        pointer.visible = False
        lapindicator.visible = False

        dani["race"] = None

        currentRaceTime = InRaceTime(race)
        formattedTime = FormatInRaceTime(currentRaceTime)
        
        # If won
        if race["positions"].index(dani) == 0:
            bge.logic.globalDict["print"] = "You've Won This Race!\nTime: "+formattedTime+"\nReward: $"+str(int(race.get("reward", 0)))
            Money.Recieve(race.get("reward", 0))
        # If finished
        else:
            bge.logic.globalDict["print"] = "You finished number "+str(race["positions"].index(dani)+1) + " Time: "+formattedTime
            
        Script.StatusText(" ")

def UpdatePositions(race, dani=None):

    positions = []
    for racer in race["racers"]:
        racer["into_race"] = [race["laps"] - racer["lap"], len(race["checkpoints"]) - racer["checkpoint"], racer.getDistanceTo(race["checkpoints"][racer["checkpoint"]]["location"])]
        positions.append([racer["into_race"], racer])

    if dani:
        dani["into_race"] = [race["laps"] - dani["lap"], len(race["checkpoints"]) - dani["checkpoint"], dani.getDistanceTo(race["checkpoints"][dani["checkpoint"]]["location"])]
        positions.append([dani["into_race"], dani])

    positions = sorted(positions)
    race["positions"] = []
    for position, racer in positions:
        race["positions"].append(racer)

    # Update the UI
    if dani:

        scene = bge.logic.getCurrentScene()
        posindicator = scene.objects["Dani_Pos_Indicator"]
        
        posindicator["Text"] = str(race["positions"].index(dani)+1)

        # The positions text ( with Dani's car being Bold and yellow )
            
        pt = ""
        bold = []
        for n, p in enumerate(race["positions"]):
            n += 1
            if p == dani:
                try: t = dani.get("driving", "On Foot")["specs"]["name"]+" | "+str(n)+"\n"
                except: t = str(dani.get("driving", "On Foot"))+" | "+str(n)+"\n"

                bold = range(len(pt), len(pt)+len(t))

            else:
                try: t = p["specs"]["name"]+" | "+str(n)+"\n"
                except: t = str(p)+" | "+str(n)+"\n"
            pt = pt + t
        Script.StatusText(pt, bold)
                      
def Available(race, formap=False, forspawn=False):

    # This function checks if the race is available.
    scene = bge.logic.getCurrentScene()
    dani = scene.objects["Dani_Box"]

    # Variable needed to check if the race is avaible

    # If the race is available at this time
    INTIME = intime(race)
    
    # If the race has any reward we check that dani
    # has the money to bid on the race.
    reward = race.get("reward", 1000)
    bid    = reward / (race.get("amount-racers", 1)+1)
    HASBID = Money.Have(bid)

    # If the race is unlocked
    after    = race.get("after")
    HASAFTER = after in Script.Story["passed"] or not after

    # If could be a during-mission race.
    during   = race.get("during")  
    ISDURING = dani.get("race") == after and Script.Story.get(during)
    
    # If the cars have spawned
    # We ignore this for if we want to spawn the cars
    havespawned  = len(race["racers"]) >= race.get("amount-racers", 1)
    SPAWNEDCHECK = ( havespawned or
                     ( forspawn and not havespawned )
                     or formap ) and INTIME

    # If the race already started
    NOTSTARTED = not race.get("started") and ( not dani.get("race") or ISDURING )

    
    
    available = (     NOTSTARTED
                      and SPAWNEDCHECK
                      and ( HASAFTER or ISDURING )
                      and ( HASBID   or ISDURING )
                 )

    return available

def InRaceTime(race):

    return bge.logic.getRealTime() - race["start-time"]

def FormatInRaceTime(currentRaceTime):

    formattedTime = str(datetime.timedelta(seconds=currentRaceTime))[:-4]
    while formattedTime.startswith("0") or formattedTime.startswith(":"):
        formattedTime = formattedTime[1:]
    if formattedTime.startswith("."):
        formattedTime = "0"+formattedTime
    return formattedTime
    
def CanStartRace(race, dani, printback=False):

    # Checks whether can start a race.
    # Based on a few things, like the type
    # of a car, or whether he is even with
    # a car.

    # Checking car type:
    NPC_type_cars = bge.logic.globalDict.get("NPC_type_cars", {})
    cartype = race.get("type")
    
    if dani["driving"] and dani["driving"].name in NPC_type_cars.get(cartype,[]):

        # Checking whether the 
        if not intime(race):
            
            if printback:
                bge.logic.globalDict["print"] = "Come here "+Racing.renderTimeRange(race)
                return False

        # Checking if this is during a mission
        if printback:
            DURING = dani.get("race") == race.get("after") and Script.Story.get(during)
            reward = race.get("reward", 1000)
            bid    = reward / (race.get("amount-racers", 1)+1)
            if not DURING:
                bge.logic.globalDict["print"] = "Press R to start the race.\nEverybody bids $"+str(int(bid))+"\nWinner gets $"+str(int(reward))
            else:
                bge.logic.globalDict["print"] = "Press R to start the race."
                
        return True

    elif printback:
        bge.logic.globalDict["print"] = "Come here with a "+cartype+" to race."

    return False
            
def PayBid(race, dani):

    during = race.get("during")
    DURING = dani.get("race") == race.get("after") and Script.Story.get(during)
    reward = race.get("reward", 1000)
    bid    = reward / (race.get("amount-racers", 1)+1)

    if not DURING:
        Money.Pay(bid)
    
def AddCylinder(cylinderType, starter):

    # Adds a race cylinder into the game world. 

    # Timer to prevent over, spawning / dispawning.
    if starter.get("make_delete_timer", 0) + 15 > bge.logic.getRealTime():
        return
    starter["make_delete_timer"] = bge.logic.getRealTime()
    
    starter["cylinder"] = Reuse.Create(cylinderType)
    starter["cylinder"].position = starter["location"]
    starter["cylinder"].orientation = starter["rotation"]
    starter["cylinder"].scaling[0] = starter["radius"]
    starter["cylinder"].scaling[1] = starter["radius"]
  
def DeleteCylinder(starter, ignoreTimer=False):

    # Timer to prevent over, spawning / dispawning.
    if not ignoreTimer:
        if starter.get("make_delete_timer", 0) + 15 > bge.logic.getRealTime():
            return
    starter["make_delete_timer"] = bge.logic.getRealTime()
    
    Reuse.Delete(starter["cylinder"])                        
    starter["cylinder"] = None

def Precalculate():

    # Precalculates racing data.

    bge.logic.globalDict["races"] = {}
    
    for collection in bpy.data.collections['Races'].children:

        race = {"starters":[],
                "checkpoints":[],
                "racers":[],
                "racer_spawns": [],
                "started":False }

        # Getting racing data
        rdf = os.listdir(bge.logic.expandPath("//racedata"))


        if collection.name in rdf:

            fol = "//racedata/"+collection.name
            race["raceData"] = {}
            for f in os.listdir(bge.logic.expandPath(fol)):
                if f.endswith(".json"):
                    with open(bge.logic.expandPath(fol+"/"+f)) as jf:
                        rd = json.load(jf)
                        race["raceData"][f.replace(".json", "")] = rd





        for object in collection.objects:
            tag = {"location":object.location,
                    "rotation":object.rotation_euler,
                    "radius":object.scale[0],
                    "OnLoop":object.get("OnLoop"),
                    "IgnoreVision":object.get("IgnoreVision"),
                    "Uturn":object.get("Uturn"),
                    "cylinder":None}

            # Race starters ( the blue cylinder )

            if object.name.startswith("Starter"):
                race["starters"].append(tag)
                race["laps"]  = object["laps"]
                race["after"]  = object.get("after")
                race["during"] = object.get("during")
                race["type"]   = object.get("type", "race-car")
                race["reward"] = object.get("reward", 1000)
                race["TimeRange"] = list(object.get("TimeRange",[]))



            # Race checkpoints ( the yellow cylinder )

            else:
                race["checkpoints"].append(tag)



        bge.logic.globalDict["races"][collection.name] = race
        print(collection.name, race["TimeRange"])