#!/usr/bin/env python3
# Render HH MM SS Sat as two 4x8 glyphs per 8x8 MAX7219 module (no scrolling).
# Modules: [HH] [MM] [SS] [Sat]
# Modified: set system time from GPS when available; GPIO21 cycles timezone display.

import time
import subprocess
from datetime import datetime, timedelta

try:
    import serial
except Exception:
    serial = None

# Try to import RPi.GPIO; if not available, disable button functionality
try:
    import RPi.GPIO as GPIO
    GPIO_AVAILABLE = True
except Exception:
    GPIO_AVAILABLE = False

from luma.core.interface.serial import spi, noop
from luma.led_matrix.device import max7219
from luma.core.render import canvas

# -----------------------------
# CONFIG
# -----------------------------
GPS_DEVICE = "/dev/ttyACM0"
GPS_BAUD = 9600
GPS_TIMEOUT = 0.5

MODULES = 4
CASCADED = MODULES
BLOCK_ORIENTATION = 90
ROTATE = 0

MODULE_REVERSED = True
VERTICAL_OFFSET_UP = 0  # adjust if you want glyphs moved up (0..3)

RUN_TEST_AT_START = False

# Button config (BCM)
BUTTON_PIN = 21

# Timezone list and fixed offsets (hours from UTC)
# Adjust offsets if you want different DST handling.
TIMEZONES = ["SYD", "SDST", "CALF", "NYC", "TKYO", "BJNG", "GRCE"]
TZ_OFFSETS = {
    "SYD": 10,    # Sydney standard (UTC+10)
    "SDST": 11,   # Sydney DST (UTC+11)
    "CALF": -8,   # California (Pacific Standard) UTC-8
    "NYC": -5,    # New York (Eastern Standard) UTC-5
    "TKYO": 9,    # Tokyo UTC+9
    "BJNG": 8,    # Beijing UTC+8
    "GRCE": 2,    # Greece UTC+2
}

# -----------------------------
# DEVICE SETUP
# -----------------------------
serial_spi = spi(port=0, device=0, gpio=noop())

device = max7219(
    serial_spi,
    cascaded=CASCADED,
    block_orientation=BLOCK_ORIENTATION,
    rotate=ROTATE,
)

MODULE_WIDTH = 8
MODULE_HEIGHT = device.height  # typically 8

# -----------------------------
# 4x8 bitmap font (each entry: 8 rows, each row is 4-bit LSB->MSB)
# 1 = pixel on, 0 = pixel off
# Added uppercase letters used in timezone codes.
# -----------------------------
FONT_4x8 = {
    "0": [0b1111,0b1001,0b1001,0b1001,0b1001,0b1001,0b1111,0b0000],
    "1": [0b0010,0b0110,0b0010,0b0010,0b0010,0b0010,0b0111,0b0000],
    "2": [0b1111,0b0001,0b0001,0b1111,0b1000,0b1000,0b1111,0b0000],
    "3": [0b1111,0b0001,0b0001,0b0111,0b0001,0b0001,0b1111,0b0000],
    "4": [0b1001,0b1001,0b1001,0b1111,0b0001,0b0001,0b0001,0b0000],
    "5": [0b1111,0b1000,0b1000,0b1111,0b0001,0b0001,0b1111,0b0000],
    "6": [0b1111,0b1000,0b1000,0b1111,0b1001,0b1001,0b1111,0b0000],
    "7": [0b1111,0b0001,0b0001,0b0010,0b0100,0b0100,0b0100,0b0000],
    "8": [0b1111,0b1001,0b1001,0b1111,0b1001,0b1001,0b1111,0b0000],
    "9": [0b1111,0b1001,0b1001,0b1111,0b0001,0b0001,0b1111,0b0000],
    "H": [0b1001,0b1001,0b1001,0b1111,0b1001,0b1001,0b1001,0b0000],
    "M": [0b1001,0b1111,0b1111,0b1011,0b1001,0b1001,0b1001,0b0000],
    "S": [0b1111,0b1000,0b1000,0b1111,0b0001,0b0001,0b1111,0b0000],
    "E": [0b1111,0b1000,0b1000,0b1111,0b1000,0b1000,0b1111,0b0000],
    "-": [0b0000,0b0000,0b0000,0b1111,0b0000,0b0000,0b0000,0b0000],
    " ": [0b0000,0b0000,0b0000,0b0000,0b0000,0b0000,0b0000,0b0000],

    # Letters (simple 4x8 approximations)
    "A": [0b0110,0b1001,0b1001,0b1111,0b1001,0b1001,0b1001,0b0000],
    "B": [0b1110,0b1001,0b1001,0b1110,0b1001,0b1001,0b1110,0b0000],
    "C": [0b0111,0b1000,0b1000,0b1000,0b1000,0b1000,0b0111,0b0000],
    "D": [0b1110,0b1001,0b1001,0b1001,0b1001,0b1001,0b1110,0b0000],
    "F": [0b1111,0b1000,0b1000,0b1110,0b1000,0b1000,0b1000,0b0000],
    "G": [0b0111,0b1000,0b1000,0b1011,0b1001,0b1001,0b0111,0b0000],
    "J": [0b0011,0b0001,0b0001,0b0001,0b1001,0b1001,0b0110,0b0000],
    "K": [0b1001,0b1010,0b1100,0b1100,0b1010,0b1001,0b1001,0b0000],
    "L": [0b1000,0b1000,0b1000,0b1000,0b1000,0b1000,0b1111,0b0000],
    "N": [0b1001,0b1101,0b1111,0b1011,0b1001,0b1001,0b1001,0b0000],
    "O": [0b0110,0b1001,0b1001,0b1001,0b1001,0b1001,0b0110,0b0000],
    "R": [0b1110,0b1001,0b1001,0b1110,0b1010,0b1001,0b1001,0b0000],
    "T": [0b1111,0b0100,0b0100,0b0100,0b0100,0b0100,0b0100,0b0000],
    "Y": [0b1001,0b1001,0b1001,0b0110,0b0100,0b0100,0b0100,0b0000],
}

# -----------------------------
# DRAWING: draw a 4x8 glyph at (x,y) in the canvas
# -----------------------------
def draw_glyph(draw, glyph_char, x, y, fill=1):
    bmp = FONT_4x8.get(glyph_char, FONT_4x8[" "])
    for row in range(8):
        row_bits = bmp[row]
        for col in range(4):
            if (row_bits >> (3 - col)) & 1:
                px = x + col
                py = y + row - VERTICAL_OFFSET_UP
                if 0 <= px < device.width and 0 <= py < device.height:
                    draw.point((px, py), fill="white")

def show_pairs_per_module(pairs):
    s_pairs = [str(p)[:2].ljust(2) for p in pairs] + ["  "] * MODULES
    s_pairs = s_pairs[:MODULES]

    if MODULE_REVERSED:
        module_indices = list(reversed(range(MODULES)))
    else:
        module_indices = list(range(MODULES))

    with canvas(device) as draw:
        for i, pair in enumerate(s_pairs):
            module_index = module_indices[i]
            module_x = module_index * MODULE_WIDTH
            ch0 = pair[0] if len(pair) > 0 else " "
            ch1 = pair[1] if len(pair) > 1 else " "
            draw_glyph(draw, ch0, module_x + 0, 0)
            draw_glyph(draw, ch1, module_x + 4, 0)

def clear_display():
    with canvas(device):
        pass

# -----------------------------
# GPS PARSING
# -----------------------------
gps = None
if serial is not None:
    try:
        gps = serial.Serial(GPS_DEVICE, baudrate=GPS_BAUD, timeout=GPS_TIMEOUT)
    except Exception:
        gps = None

def parse_gprmc_time_hhmmss(line):
    if not line.startswith("$GPRMC"):
        return None
    parts = line.split(",")
    if len(parts) < 2:
        return None
    t = parts[1]
    if len(t) < 6:
        return None
    hh = t[0:2]; mm = t[2:4]; ss = t[4:6]
    if not (hh.isdigit() and mm.isdigit() and ss.isdigit()):
        return None
    hival = int(hh); mival = int(mm); sival = int(ss)
    if 0 <= hival < 24 and 0 <= mival < 60 and 0 <= sival < 60:
        return hh + mm + ss
    return None

def parse_gpgga_satellites(line):
    if not line.startswith("$GPGGA"):
        return None
    parts = line.split(",")
    if len(parts) < 8:
        return None
    nsat = parts[7]
    if nsat.isdigit():
        try:
            return int(nsat)
        except Exception:
            return None
    return None

def read_gps_line():
    if gps is None:
        return None
    try:
        raw = gps.readline()
    except Exception:
        return None
    if not raw:
        return None
    try:
        return raw.decode("ascii", errors="ignore").strip()
    except Exception:
        return None

def get_system_time_hhmmss():
    return datetime.now().strftime("%H%M%S")

# -----------------------------
# Set system time (attempt). Requires sudo privileges.
# We set only the time portion (UTC) using date -u --set="HH:MM:SS"
# -----------------------------
def set_system_time_utc(hhmmss):
    hh = hhmmss[0:2]; mm = hhmmss[2:4]; ss = hhmmss[4:6]
    timestr = f"{hh}:{mm}:{ss}"
    try:
        # Use date -u --set to set UTC time
        subprocess.run(["sudo", "date", "-u", "--set", timestr], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        return True
    except Exception:
        # fallback: try date --set (local)
        try:
            subprocess.run(["sudo", "date", "--set", timestr], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
            return True
        except Exception:
            return False

# -----------------------------
# Helper: show a short text (1..8 chars) centered across 8 glyph positions (4 modules)
# -----------------------------
def show_text_centered(text):
    # ensure uppercase and only allowed chars
    text = str(text).upper()
    # center into 8 characters
    if len(text) >= 8:
        s = text[:8]
    else:
        pad = (8 - len(text)) // 2
        s = " " * pad + text + " " * (8 - len(text) - pad)
    # split into pairs for modules
    pairs = [s[i:i+2] for i in range(0, 8, 2)]
    show_pairs_per_module(pairs)

# -----------------------------
# QUICK TEST
# -----------------------------
def run_quick_test():
    show_pairs_per_module(["12","34","56","04"])
    time.sleep(5)
    clear_display()

# -----------------------------
# GPIO button handling (cycle timezone)
# -----------------------------
current_tz_index = 0
display_tz_until = 0  # epoch seconds until which timezone text is shown
display_tz_code = TIMEZONES[current_tz_index]

def button_pressed_callback(channel):
    global current_tz_index, display_tz_until, display_tz_code
    current_tz_index = (current_tz_index + 1) % len(TIMEZONES)
    display_tz_code = TIMEZONES[current_tz_index]
    display_tz_until = time.time() + 1.0  # show timezone text for 1 second

def setup_button():
    if not GPIO_AVAILABLE:
        return
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(BUTTON_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    # falling edge (button to ground), debounce 200ms
    GPIO.add_event_detect(BUTTON_PIN, GPIO.FALLING, callback=button_pressed_callback, bouncetime=200)

def cleanup_button():
    if not GPIO_AVAILABLE:
        return
    try:
        GPIO.remove_event_detect(BUTTON_PIN)
    except Exception:
        pass
    try:
        GPIO.cleanup(BUTTON_PIN)
    except Exception:
        pass

# -----------------------------
# MAIN LOOP
# -----------------------------
def main():
    global current_tz_index, display_tz_until, display_tz_code

    print("Device:", device.width, "x", device.height, "Modules:", MODULES)
    if RUN_TEST_AT_START:
        run_quick_test()

    last_gps_time_str = None      # "HHMMSS" string from last GPS
    last_gps_datetime_utc = None  # datetime in UTC (date from system, time from GPS)
    last_sat_count = None

    setup_button()

    try:
        while True:
            # read a few lines to capture both sentence types
            for _ in range(6):
                line = read_gps_line()
                if not line:
                    break
                t = parse_gprmc_time_hhmmss(line)
                if t:
                    # store last GPS time string and build a datetime in UTC using today's date
                    last_gps_time_str = t
                    now_utc = datetime.utcnow()
                    try:
                        hh = int(t[0:2]); mm = int(t[2:4]); ss = int(t[4:6])
                        last_gps_datetime_utc = now_utc.replace(hour=hh, minute=mm, second=ss, microsecond=0)
                    except Exception:
                        last_gps_datetime_utc = now_utc.replace(microsecond=0)

                    # attempt to set system time to GPS UTC time (so short locks still correct clock)
                    ok = set_system_time_utc(t)
                    if ok:
                        print("System time set from GPS UTC:", t)
                    else:
                        print("Failed to set system time from GPS (need sudo privileges).")

                s = parse_gpgga_satellites(line)
                if s is not None:
                    last_sat_count = s

            # Determine base UTC datetime for conversions:
            if last_gps_datetime_utc:
                base_utc = last_gps_datetime_utc
                # advance base_utc by elapsed seconds since it was captured to keep it ticking
                # compute how long since we set it (approx): use system UTC now and difference
                # This keeps seconds advancing even if GPS not continuously streaming
                # We'll compute offset between stored base and current UTC and add that offset
                # to the stored base to keep it in sync.
                # Simpler: use datetime.utcnow() as authoritative if system clock was set.
                base_utc = datetime.utcnow()
            else:
                # no GPS yet: use system UTC
                base_utc = datetime.utcnow()

            # If button pressed recently, show timezone code for 1 second
            now_ts = time.time()
            if display_tz_until > now_ts:
                # show timezone code centered for 1 second
                show_text_centered(display_tz_code)
                # small sleep to avoid busy loop while showing
                time.sleep(0.05)
                continue

            # compute timezone time using offset
            tz_code = TIMEZONES[current_tz_index]
            offset_hours = TZ_OFFSETS.get(tz_code, 0)
            tz_dt = base_utc + timedelta(hours=offset_hours)

            hh = f"{tz_dt.hour:02d}"
            mm = f"{tz_dt.minute:02d}"
            ss = f"{tz_dt.second:02d}"

            if last_sat_count is None:
                sat = "--"
            else:
                sat_val = max(0, min(99, int(last_sat_count)))
                sat = f"{sat_val:02d}"

            pairs = [hh, mm, ss, sat]
            show_pairs_per_module(pairs)

            time.sleep(1)

    except KeyboardInterrupt:
        clear_display()
        print("Exiting...")

    finally:
        cleanup_button()
        if gps is not None:
            try:
                gps.close()
            except:
                pass

if __name__ == "__main__":
    main()

