# psx-fma: program to display (later speak) the PSX Flight Mode Annunciator.
# https://www.hoppie.nl/

# Jeroen Hoppenbrouwers <hoppie@hoppie.nl> July 2025

import asyncio
from datetime import datetime # use dt for the modified variant
from datetime import timezone
from datetime import timedelta
from enum import Enum
from math import ceil
import os
import sys
import textwrap as tw
import time
import traceback

import modules.psx

try:
  import pyttsx3
except ModuleNotFoundError:
  print("Failed to load module 'pyttsx3'. Please try:")
  print("   python -m pip install pyttsx3")
  exit(1)


##### Global variables ###################################################

VERSION = "beta-00"
ZULU    = None


##### Classes and Functions ##############################################

class dt(datetime):
  """Subclass of datetime to override some default methods."""

  def __str__(self):
    """When converted to default string, replace UTC time zone with Zulu."""
    if self.tzname()=="UTC":
      return self.strftime("%Y-%b-%d %H:%M:%SZ")
    else:
      return self.strftime("%Y-%b-%d %H:%M:%S%z")
  # str()

# dt


def setup():
  # This is a synchronous (normal) function, so don't use await.
  # The only things you can do are changing flags and such that get picked
  # up by truly async coroutines/tasks.
  print(f"{gr}Setup{df}")
  # Nothing to do here, yet.
# setup();


def teardown(reason=None):
  # This is a synchronous (normal) function, so don't use await.
  # The only things you can do are changing flags and such that get picked
  # up by truly async coroutines/tasks.
  if reason is None:
    print(f"{gr}Teardown{df}")
  else:
    print(f"{gr}Teardown ({reason}){df}")
# teardown()


def set_zulu(key, time_earth):
    """Remember the PSX planet time (not necessarily real UTC)."""
    global ZULU

    epoch = int(time_earth)/1000    # PSX thinks in milliseconds
    first = (ZULU is None)
    ZULU = dt.fromtimestamp(epoch, timezone.utc)
    if first:
      print(f"{cy}  psx: simulator time is {ZULU}{df}")
# set_utc()


def update_afds(key, afds):
  m = ["blank","attitude","heading hold","heading select",
         "L naav","localizer",
       "rollout","toe-gaa","toe-gaa","altitude","flare",
       "F L change speed","glideslope","vertical speed",
         "v naav altitude","v naav path",
       "v naav speed","v naav","idle","speed","thrust",
       "hold","thrust ref","no autoland","land two","land three",
       "command","flight director","test","v naav fail","v naav off"]

  print(f"{cy}  psx: AFDS {afds}{df}")
  f = afds.split(";")
  print(f"{m[abs(int(f[0]))]}      "
        f"{m[abs(int(f[1]))]}      "
        f"{m[abs(int(f[2]))]}")
  tts.say(m[abs(int(f[0]))])
  tts.runAndWait()
  tts.say(m[abs(int(f[1]))])
  tts.runAndWait()
  tts.say(m[abs(int(f[2]))])
  tts.runAndWait()
# update_afds()


def bg_exception(loop, context):
  """The background exception handler is used to catch things that go wrong
     on background tasks (such as created with "after") that have no natural
     call stack to backtrack looking for a handler. If this background
     exception handler is not installed, an additional "Task exception was
     never retrieved" usually is the result.
  """
  ex = context.get("exception")
  print(f"\n{rd}[BG] {ex}{yl}")
  traceback.print_exception(type(ex), ex, ex.__traceback__)
  print(df)
  psx.send("FreeMsgS", "DATALINK SYS")
# bg_exception()


async def supervisor():
  """This is the inittab, so to say."""
  loop = asyncio.get_event_loop()
  loop.set_exception_handler(bg_exception)
  try:
    await asyncio.gather(
      psx.connect(),
    )
  except Exception as ex:
    # This is the emergency exit.
    print(f"\n{rd}{ex}{yl}")
    traceback.print_exception(type(ex), ex, ex.__traceback__)
    print(df)
    teardown("supervisor exception")
    # Give the system a second to cleanly shut down and then just quit.
    await asyncio.sleep(1)
    os._exit(1)
# supervisor()


##### MAIN ###############################################################

# To be fancy, we use colours. Windows terminals understand ANSI by now.
df = "\033[0m"  # default colour
bk = "\033[30m"
rd = "\033[31m"
gr = "\033[32m"
yl = "\033[33m"
bl = "\033[34m"
mg = "\033[35m"
cy = "\033[36m"
wh = "\033[37m"

print(f"{df}Hoppie's FMA Reader for Aerowinx PSX, version {VERSION}")

tts = pyttsx3.init()
tts.say("Hoppie's F M A Reader")
tts.runAndWait()

with modules.psx.Client() as psx:
  # psx.logger = lambda msg: print(f"{cy}  psx: {msg}{df}")
  psx.onResume     = setup
  psx.onPause      = teardown
  psx.onDisconnect = teardown

  psx.subscribe("id")
  psx.subscribe("version", lambda key, value:
    print(f"{cy}  psx: connected to PSX {value} as client "
          f"#{psx.get('id')}{df}"))
  psx.subscribe("TimeEarth", set_zulu)
  psx.subscribe("Afds", update_afds)

  try:
    asyncio.run(supervisor())
  except KeyboardInterrupt:
    print("\nStopped by keyboard interrupt (Ctrl-C)")

# EOF
