# A standalone script to control lights according to events in Elite: Dangerous.
# Coded in two afternoons with lots of coffee by Katie.
# This is meant to be a personal project, so I didn't really bother with expandability and such.
# Perhaps I'll revisit this in the future and make it so it can be expended upon easier. Don't count on it.
#
# (C) Innovation Science 2022

try:
    import tinytuya
except ModuleNotFoundError:
    print("You MUST install tinytuya first.")
    print("pip install tinytuya OR python -m pip install tinytuya")
    exit()

import time
import random
import json
import os

Version = "0.1.1"

# Use the classes below to configure your lights.
# You MUST go through the following setup process: https://github.com/jasonacox/tinytuya#setup-wizard---getting-local-keys
class PortLight:
    # Actual light config.
    DeviceID: str =     "DEVICEID HERE"
    IP: str =           "BULB.IP.ADDRESS.HERE"
    LocalID: str =      "LOCALID HERE"
    # Color config. I've left it as my own preferences, but commented out the defaults as well.
    red: int =          230
    green: int =        100
    blue: int =         235

class StarboardLight:
    DeviceID: str =     "DEVICEID HERE"
    IP: str =           "BULB.IP.ADDRESS.HERE"
    LocalID: str =      "LOCALID HERE"
    red: int =          230
    green: int =        100
    blue: int =         235

class HazardLight:
    DeviceID: str =     "DEVICEID HERE"
    IP: str =           "BULB.IP.ADDRESS.HERE"
    LocalID: str =      "LOCALID HERE"
    red: int =          255
    green: int =        0
    blue: int =         0

# Below are the colors for the default hud colors. I'm estimating again, but it's close enough.
# red: int =            255
# green: int =          100
# blue: int =           0


# Elite Dangerous flags

#ED_Docked =                 0x0000000000000001
#ED_Landed =                 0x0000000000000002
#ED_LandingGearDown =        0x0000000000000004
#ED_ShieldsUp =              0x0000000000000008
#ED_Supercruise =            0x0000000000000010
#ED_FlightAssistOff =        0x0000000000000020
#ED_HardpointsDeployed =     0x0000000000000040
#ED_InWing =                 0x0000000000000080

#ED_LightsOn =               0x0000000000000100
#ED_CargoScoopDeployed =     0x0000000000000200
#ED_SilentRunning =          0x0000000000000400
#ED_ScoopingFuel =           0x0000000000000800
#ED_SRVHandbrake =           0x0000000000001000
#ED_SRVTurret =              0x0000000000002000
#ED_SRVTurretRetracted =     0x0000000000004000
#ED_SRVDriveAssist =         0x0000000000008000

#ED_FSDMassLocked =          0x0000000000010000
#ED_FSDCharging =            0x0000000000020000
#ED_FSDCooldown =            0x0000000000040000
#ED_LowFuel =                0x0000000000080000
#ED_OverHeating =            0x0000000000100000
#ED_HasLatLong =             0x0000000000200000
#ED_IsInDanger =             0x0000000000400000
#ED_BeingInterdicted =       0x0000000000800000

#ED_InMainShip =             0x0000000001000000
#ED_InFighter =              0x0000000002000000
#ED_InSRV =                  0x0000000004000000
#ED_HudInAnalysisMode =      0x0000000008000000
#ED_NightVision =            0x0000000010000000

# Status file stuff
# StatusDir will usually be: "C:\\Users\\{YOUR USERNAME HERE}\\Saved Games\\Frontier Developments\\Elite Dangerous"
StatusDir = "C:\\Users\\Sam\\Saved Games\\Frontier Developments\\Elite Dangerous"
CurrentLog = ""
StatusJSON = ""
StatusFlags = 0x00000000
StatusFlagsStr = ""

# Makes the infinate loop... loop.
ContinueFlag = True

# States
DoFlicker = False
DoHazards = False
LightsOn = True
LightsDim = False
LastHullHealth = 0
CurrentHullHealth = 0


# Initialize light variables and set version
port = tinytuya.BulbDevice(PortLight.DeviceID, PortLight.IP, PortLight.LocalID, dev_type="default")
starboard = tinytuya.BulbDevice(StarboardLight.DeviceID, StarboardLight.IP, StarboardLight.LocalID, dev_type="default")
hazards = tinytuya.BulbDevice(HazardLight.DeviceID, HazardLight.IP, HazardLight.LocalID, dev_type="default")

# Set Tuya versions
port.set_version(3.3)
starboard.set_version(3.3)
hazards.set_version(3.3)

def SetupLights(): # Initializes lights
    # Set light modes to color
    port.set_mode(mode='colour')
    starboard.set_mode(mode='colour')
    hazards.set_mode(mode='colour')

    # Turn on the lights
    port.turn_on()
    starboard.turn_on()
    hazards.turn_on()

    # Set light colors
    port.set_colour(PortLight.red, PortLight.green, PortLight.blue)
    starboard.set_colour(StarboardLight.red, StarboardLight.green, StarboardLight.blue)
    hazards.set_colour(HazardLight.red, HazardLight.green, HazardLight.blue)

    # Set light brightness (port and starboard are dim, whereas hazard is bright.)
    port.set_brightness_percentage(brightness=1)
    starboard.set_brightness_percentage(brightness=1)
    hazards.set_brightness_percentage(brightness=100)

    # Turn off hazard.
    hazards.turn_off()

def NormalLights(): # Returns lights to normal operation (used when exiting)
    # Change lights back to white mode.
    port.set_mode(mode='white')
    starboard.set_mode(mode='white')
    hazards.set_mode(mode='white')

    # Set brightness to full to blind the user.
    port.set_brightness_percentage(brightness=100)
    starboard.set_brightness_percentage(brightness=100)
    hazards.set_brightness_percentage(brightness=100)

def FlickerLights():
    # So far, I've made it so there are four types of flickers possible (0 being no flicker.)
    choices = [0, 1, 2, 3, 4]
    choice = random.choice(choices) # Randomly choose a flicker type
    flickertime = random.random()   # Randomly choose a flicker time (0 to 1 sec)

    # Welcome to if-else hell! I'm using python 3.9.6 so no pattern matching for me, and I don't care enough to deal with dictionaries.
    # If you would like to add more flicker patterns, add them here and add a new number to the choices list.
    if(choice==0):
        # Nothing happens.
        pass
    elif(choice==1):
        # Flicker port
        port.turn_off()
        time.sleep(flickertime)
        port.turn_on()
    elif(choice==2):
        # Flicker starboard
        starboard.turn_off()
        time.sleep(flickertime)
        starboard.turn_on()
    elif(choice==3):
        # Flicker both, port first
        port.turn_off()
        starboard.turn_off()
        time.sleep(flickertime)
        port.turn_on()
        starboard.turn_on()
    elif(choice==4):
        starboard.turn_off()
        port.turn_off()
        time.sleep(flickertime)
        starboard.turn_on()
        port.turn_on()
    else:
        print("How did we get here?")

def MainLightsOn(): # Turns main (port and starboard) lights on
    port.turn_on()
    starboard.turn_on()

def MainLightsOff(): # Turns main (port and starboard) lights off
    port.turn_off()
    starboard.turn_off()

def DimAllLights():  # Dims all lights. Used when FSD is charging to make it look like it's heavily loading the powerplant.
    port.set_brightness_percentage(brightness=0)
    starboard.set_brightness_percentage(brightness=0)
    hazards.set_brightness_percentage(brightness=80)

def LightsStandard(): # Standard brightness configuration for lights.
    port.set_brightness_percentage(brightness=1)
    starboard.set_brightness_percentage(brightness=1)
    hazards.set_brightness_percentage(brightness=100)

def HazardsOn():      # Turns the hazard light on
    hazards.turn_on()

def HazardsOff():     # Turns the hazard light off
    hazards.turn_off()

def ReloadFlags():    # Reloads Elite: Dangerous status flags and sets flicker, hazard on/off, lights on/off, and dimming variables for later processing
    global StatusDir
    global StatusJSON
    global StatusFlags
    global StatusFlagsStr
    global DoFlicker
    global DoHazards
    global LightsOn
    global LightsDim
    global ContinueFlag
    
    # Open the status file
    f = open(StatusDir + "\\Status.json", "r")
    try:
        StatusJSON = json.loads(f.read()) # Read the json from the Status.json file. 
    except:
        #print("JSONDecodeError in ReloadFlags(). Continuing.")
        pass # Sometimes it guffs up reading (JSONDecodeError), and I can't really fix this.
    else:                                               # This makes the logic below use the previous status flags if an exception is raised
        StatusFlags = "0x%8x" % StatusJSON["Flags"]     # Extrapolate the flags from the json file and make it a known length (8 hex digits)
    f.close() # Close Status.json

    try:
        StatusFlagsStr = str(StatusFlags)
        
        # Convert the ship state (3rd from right) byte (LightsOn, Cargo Scoop Deployed, Silent Running, Scooping Fuel)
        ShipStateByte = bin(int(StatusFlagsStr[-3]))[2:].zfill(4)
        ShipStateByteStr = str(ShipStateByte)

        # Convert the FSD (5th from right) byte (FSD MassLocked, FSD Charging, FSD Cooldown, Low Fuel (<25%))
        FSDByte = bin(int(StatusFlagsStr[-5]))[2:].zfill(4)
        FSDByteStr = str(FSDByte)
        
        # Convert the hazards (6th from right) byte (Overheating (>100%), Has Lat Long, IsInDanger, Being Interdicted)
        HazardByte = bin(int(StatusFlagsStr[-6]))[2:].zfill(4)
        HazardByteStr = str(HazardByte)
        
        # If you would like to add more effects to the lights, add them here.

        if(HazardByteStr[-1] == "1" or LastHullHealth > CurrentHullHealth):
            DoFlicker = True                                # Overheating and taking damage causes lights to flicker
        else:
            DoFlicker = False

        if(int(StatusFlagsStr[-6]) > 0):                    # If there's any hazard, hazard light is turned on
            DoHazards = True
        else:
            DoHazards = False
        
        if(ShipStateByteStr[-3] == "1"):                    # Port and starboard lights turn off when rigged for silent running
            LightsOn = False
        else:
            LightsOn = True

        if(FSDByteStr[-2] == "1"):                          # Lights dim when the FSD is charging
            LightsDim = True
        else:
            LightsDim = False
    except ValueError:
        print("ValueError! More than likely, Elite: Dangerous is closed. Exiting.")
        ContinueFlag = False
    #except IndexError:
    #    print("IndexError encountered in ReloadFlags. This may be recoverable. Continuing.")

def GetCurrentLog(): # Gets the most recent log file left by Elite: Dangerous.
    global StatusDir


    LogName = ""
    # Changes working dir to the saved games dir temporarily, remembering the original working dir
    OriginalDir = os.getcwd()
    os.chdir(StatusDir)
    StatusDirContents = sorted(os.listdir(StatusDir), key=os.path.getmtime) # Get and sort the "saved game" directory from newest first.
    os.chdir(OriginalDir)

    for f in reversed(StatusDirContents):
        if(f[-4:] == ".log"): # Search for the newest .log file.
            LogName = f   # We found the file!
            break
    
    print("Discovered current log file: " + f)
    
    return LogName # Return the newest log file.
    
def GetDamageStats(): # Extrapolates damage taken from the log file. I'm not sure if this works quite yet.
    global StatusDir
    global CurrentLog
    try:
        f = open(StatusDir + "\\" + CurrentLog, "r") # Open the log file
    except:
        print("Error opening log. Continuing.")      # Error checking for if the log file cannot be opened.
        return
    try:
        Lines = f.readlines()                        # This should never happen.
    except:
        print("Error reading lines from log. Continuing")
    f.close() # Close the log file

    i = 0
    tempjson = ""
    for line in reversed(Lines): # Scan in reversed for the newest loadout readings
        i += 1
        try:
            tempjson = json.loads(line) # Attempt to load the line as JSON.
        except:
            continue                    # I'm willing to bet that we will encounter a similar JSONDecodeError here on occasion, so we skip that line.
                                        # This will probably lead to issues, but I'm not too worried about it.

        if(tempjson["event"] == "Loadout"):
            return tempjson["HullHealth"] # Returns HullHealth from Loadout event.
    

        

def RunStates(): # Processes light effect variables set by ReloadFlags()
    if(DoFlicker):
        FlickerLights()

    if(DoHazards):
        HazardsOn()
    else:
        HazardsOff()

    if(LightsOn):
        MainLightsOn()
    else:
        MainLightsOff()

    if(LightsDim):
        DimAllLights()
    else:
        LightsStandard()


# Program initialization

print("Elite Lighting v. " + Version)
print("If the program crashes in this stage, you either need to open Elite: Dangerous first or configure the program.")
print("To configure the program, follow the instructions at: https://git.innovation-inc.org/Innovation/elitelighting")
print("You will also need to read alongside: https://github.com/jasonacox/tinytuya#setup-wizard---getting-local-keys")
print("Failure to do so will cause a crash and may kill your cat(s).")
print("\n")

print("Setting up lights")
SetupLights() # Set up lights

print("Lights set up.")

print("Getting current Elite: Dangerous log")
CurrentLog = GetCurrentLog() # Get current Elite: Dangerous log

print("Ready. Press CTRL+C at any time to set lights back to normal operation.")

while ContinueFlag == True:
    try:
        LastHullHealth = CurrentHullHealth
        CurrentHullHealth = GetDamageStats()
        ReloadFlags()
        RunStates()
        time.sleep(0.25)
    except KeyboardInterrupt: # This makes it so CTRL+C begins the program exiting sequence, and so the program exits gracefully at any point during execution.
        break

HazardsOn() # Turn on hazards so that it isn't off when lights return to normal operation.

lightschoice = input("Would you like your lights off? Y/n > ") # Would you like to be light mode'd?
if(lightschoice != "n" and lightschoice != "N"):
    print("Turning off lights.")                               # No, I like having eyes.
    MainLightsOff()
    HazardsOff()

print("Returning lights to normal operation.")
NormalLights()                                                 # Return lights to normal operation (white mode, full brightness)
print("Goodbye.")