# SPDX-FileCopyrightText: 2020 Brent Rubell for Adafruit Industries
# 
#  08-17-2023 DWE
#
# SPDX-License-Identifier: MIT
import time
import gc
import ipaddress
import displayio
import ssl
import wifi
import busio
import socketpool
import adafruit_requests
import board
import adafruit_imageload
import dotclockframebuffer
import neopixel
import pwmio
from rtc import RTC
from adafruit_ht16k33 import segments
from framebufferio import FramebufferDisplay
from displayio import release_displays

release_displays()
i2c = busio.I2C(board.SCL, board.SDA)

num_pixels = 60

pixels = neopixel.NeoPixel(board.A0, num_pixels, auto_write=False)
pixels.brightness = 0.01

TWELVE_HOUR = True  # If set, use 12-hour time vs 24-hour (e.g. 3:00 vs 15:00)

GREEN = (0, 255, 0)
RED = (255, 0, 0)
BLACK = (0, 0, 0)
BLUE = (0, 0, 255)

refresh_time_update = 60

weekname = {
    0: "Sun  ",
    1: "Mon  ",
    2: "Tue  ",
    3: "Wed  ",
    4: "Thurs ",
    5: "Fri  ",
    6: "Sat  ",
}

init_sequence_tl021wvc02 = bytes((
    0xff, 0x05, 0x77, 0x01, 0x00, 0x00, 0x10,
    0xc0, 0x02, 0x3b, 0x00,
    0xc1, 0x02, 0x0b, 0x02,
    0xc2, 0x02, 0x00, 0x02,
    0xcc, 0x01, 0x10,
    0xcd, 0x01, 0x08,
    0xb0, 0x10, 0x02, 0x13, 0x1b, 0x0d, 0x10, 0x05, 0x08, 0x07, 0x07, 0x24, 0x04, 0x11, 0x0e, 0x2c, 0x33, 0x1d,
    0xb1, 0x10, 0x05, 0x13, 0x1b, 0x0d, 0x11, 0x05, 0x08, 0x07, 0x07, 0x24, 0x04, 0x11, 0x0e, 0x2c, 0x33, 0x1d,
    0xff, 0x05, 0x77, 0x01, 0x00, 0x00, 0x11,
    0xb0, 0x01, 0x5d,
    0xb1, 0x01, 0x43,
    0xb2, 0x01, 0x81,
    0xb3, 0x01, 0x80,
    0xb5, 0x01, 0x43,
    0xb7, 0x01, 0x85,
    0xb8, 0x01, 0x20,
    0xc1, 0x01, 0x78,
    0xc2, 0x01, 0x78,
    0xd0, 0x01, 0x88,
    0xe0, 0x03, 0x00, 0x00, 0x02,
    0xe1, 0x0b, 0x03, 0xa0, 0x00, 0x00, 0x04, 0xa0, 0x00, 0x00, 0x00, 0x20, 0x20,
    0xe2, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xe3, 0x04, 0x00, 0x00, 0x11, 0x00,
    0xe4, 0x02, 0x22, 0x00,
    0xe5, 0x10, 0x05, 0xec, 0xa0, 0xa0, 0x07, 0xee, 0xa0, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xe6, 0x04, 0x00, 0x00, 0x11, 0x00,
    0xe7, 0x02, 0x22, 0x00,
    0xe8, 0x10, 0x06, 0xed, 0xa0, 0xa0, 0x08, 0xef, 0xa0, 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0xeb, 0x07, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00,
    0xed, 0x10, 0xff, 0xff, 0xff, 0xba, 0x0a, 0xbf, 0x45, 0xff, 0xff, 0x54, 0xfb, 0xa0, 0xab, 0xff, 0xff, 0xff,
    0xef, 0x06, 0x10, 0x0d, 0x04, 0x08, 0x3f, 0x1f,
    0xff, 0x05, 0x77, 0x01, 0x00, 0x00, 0x13,
    0xef, 0x01, 0x08,
    0xff, 0x05, 0x77, 0x01, 0x00, 0x00, 0x00,
    0x36, 0x01, 0x00,
    0x3a, 0x01, 0x60,
    0x11, 0x80, 0x64,
    0x29, 0x80, 0x32,
))

tft_io_expander = dict(board.TFT_IO_EXPANDER)
#tft_io_expander['i2c_address'] = 0x38 # uncomment for rev B
dotclockframebuffer.ioexpander_send_init_sequence(i2c, init_sequence_tl021wvc02, **tft_io_expander)
i2c.deinit()

tft_pins = dict(board.TFT_PINS)

tft_timings = {
    "frequency": 16_000_000,
    "width": 480,
    "height": 480,
    "hsync_pulse_width": 20,
    "hsync_front_porch": 40,
    "hsync_back_porch": 40,
    "vsync_pulse_width": 10,
    "vsync_front_porch": 40,
    "vsync_back_porch": 40,
    "hsync_idle_low": False,
    "vsync_idle_low": False,
    "de_idle_high": False,
    "pclk_active_high": True,
    "pclk_idle_high": False,
}

# Loop through each sprite in the sprite sheet
source_index = 0
displayBL = pwmio.PWMOut(board.A1)

i2c = busio.I2C(board.SCL, board.SDA)

# Create the LED segment class.
# This creates a 7 segment 4 character display:
display = segments.Seg7x4(i2c, address=0x74)
display14 = segments.Seg14x4(i2c, address=(0x71, 0x72))
#  alphanumeric segment displpay setup
#  using two displays together

# Reset Display
display.fill(0)
display14.fill(0)
pixels.full = BLACK
pixels.show()

# Get wifi details and more from a secrets.py file
try:
    from secrets import secrets
except ImportError:
    print("WiFi secrets are kept in secrets.py, please add them there!")
    raise

print("Connecting to %s" % secrets["ssid"])
wifi.radio.connect(secrets["ssid"], secrets["password"])
# print("Connected to %s!" % secrets["ssid"])
# print("My IP address is", wifi.radio.ipv4_address)

pool = socketpool.SocketPool(wifi.radio)
requests = adafruit_requests.Session(pool, ssl.create_default_context())

try:
    TIMEZONE = secrets["timezone"]  # e.g. 'America/New_York'
except:
    TIMEZONE = None  # IP geolocation

print("TIME ZONE IS")
print(TIMEZONE)

try:
    LATITUDE = secrets['latitude']
    LONGITUDE = secrets['longitude']
    print('Using stored geolocation: ', LATITUDE, LONGITUDE)
except:
    LATITUDE = None
    LONGITUDE = None

# Set initial clock time, also fetch initial UTC offset while
# here (NOT stored in secrets.py as it may change with DST).
# pylint: disable=bare-except


def MoonData(year, month, day, lat, log):
    """ Class holding lunar data for a given 24-hour period. App uses two
        of these -- one for the current day, and one for the following day,
        then some interpolations and such can be made. Elements include:
        age               : Moon phase 'age' at start of period, expressed
                            from 0.0 (new moon) through 0.5 (full moon) to
                            1.0 (next new moon).
        start_utc_seconds : Epoch time at start of period, UTC
        end_utc_seconds   : Epoch time at end of period, "
        rise_utc_seconds  : Epoch time of moon rise within this 24-hour period
        set_utc_seconds   : Epoch time of moon set within this 24-hour period
    """

    # URL does not contain local or UTC time, only date. strftime() is
    # not available in CircuitPython, manual conversion to time string
    # is needed. Response is moon data for a 24-hour period, based on
    # longitude and requested date. Some values within are UTC time,
    # others are local. Anything we parse out of this will be converted
    # to UTC epoch seconds, period.
    utc_offset_string = "-07:00"
    moon_url = ('https://api.met.no/weatherapi/sunrise/3.0/moon?lat=' +
           str(lat) + '&lon=' + str(log) +
           '&date=' + str(year) + '-' +
           '{0:0>2}'.format(month) + '-' +
           '{0:0>2}'.format(day) +
           '&offset=' + utc_offset_string)

    # pylint: disable=bare-except


    for _ in range(2): # Retries
        print('Fetching moon data via', moon_url)
        try:
            moon_data = requests.get(moon_url)

            properties = moon_data.json()['properties']
            # 0 = new moon, 90 = Q1, 180 = full moon, 270 = LQ
            age = float(properties['moonphase'])
            # age = 169
            print("AGE :", age)
            return int(age)
        except:
            # Moon server error (maybe), try again after 15 seconds.
            # (Might be a memory error, that should be handled different)
            time.sleep(5.5)
    age = 0
    return age


def moonPhase(age):
    '''
    0 = Waxing cresent
    1 = First Quarter
    2 = Waxing gibbous
    3 = Waxing gibbous
    4 = Full Moon
    5 = Waning gibbous
    6 = Waning gibbous
    7 = Last Quarter
    8 = Waning cresent
    9 = New Moon
    '''
    print("THE MOON AGE IS: ", age)
    if (age >= 345) and (age <= 15): # New Moon #0
        moon_sprite = 9
    elif (age >= 16) and (age <= 52): # Waxing gibbous #9
        moon_sprite = 8
    elif (age >= 53) and (age <= 77): # Waxing gibbous #8
        moon_sprite = 7
    elif (age >= 78) and (age <= 91): # First Quarter # 7
        moon_sprite = 6
    elif (age >= 92) and (age <= 163): # Waxing gibbous #6
        moon_sprite = 5
    elif (age >= 163) and (age <= 197): # Full Moon #5
        moon_sprite = 4
    elif (age >= 188) and (age <= 251): # Waning gibbous #4
        moon_sprite = 3
    elif (age >= 252) and (age <= 273): # Last Quarter #3
        moon_sprite = 2
    elif (age >= 274) and (age <= 309): # Waning gibbous #2
        moon_sprite = 1
    elif (age >= 310) and (age <= 344): # Waning gibbous #1
        moon_sprite = 0
    else:
        moon_sprite = 9 # New Moon
    print("THE MOON PHASE IS: ", moon_sprite)

    return moon_sprite


def update_time(timezone=None):
    """Update system date/time from WorldTimeAPI public server;
    no account required. Pass in time zone string
    (http://worldtimeapi.org/api/timezone for list)
    or None to use IP geolocation. Returns current local time as a
    time.struct_time and UTC offset as string. This may throw an
    exception on fetch_data() - it is NOT CAUGHT HERE, should be
    handled in the calling code because different behaviors may be
    needed in different situations (e.g. reschedule for later).
    {
    "abbreviation": "MST",
    "client_ip": "174.24.76.33",
    "datetime": "2023-11-20T14:34:44.464927-07:00",
    "day_of_week": 1,
    "day_of_year": 324,
    "dst": false,
    "dst_from": null,
    "dst_offset": 0,
    "dst_until": null,
    "raw_offset": -25200,
    "timezone": "America/Denver",
    "unixtime": 1700516084,
    "utc_datetime": "2023-11-20T21:34:44.464927+00:00",
    "utc_offset": "-07:00",
    "week_number": 47
    }
    """
    if timezone:  # Use timezone api
        time_url = "http://worldtimeapi.org/api/timezone/" + timezone
        print(time_url)
    else:  # Use IP geolocation
        time_url = "http://worldtimeapi.org/api/ip"
    local_time_string = requests.get(time_url)
    print("-" * 40)
    time_data = (local_time_string.json()["datetime"], ["dst"], ["utc_offset"])
    dayofweek = local_time_string.json()["day_of_week"]
    print("UPDATE_TIME_DATA")
    print(time_data)
    time_struct = parse_time(time_data[0], time_data[1])
    print(time_struct)
    return time_struct, time_data[2], dayofweek


def parse_time(timestring, is_dst=-1):
    """Given a string of the format YYYY-MM-DDTHH:MM:SS.SS-HH:MM (and
    optionally a DST flag), convert to and return an equivalent
    time.struct_time (strptime() isn't available here). Calling function
    can use time.mktime() on result if epoch seconds is needed instead.
    Time string is assumed local time; UTC offset is ignored. If seconds
    value includes a decimal fraction it's ignored.
    """
    date_time = timestring.split("T")  # Separate into date and time
    year_month_day = date_time[0].split("-")  # Separate time into Y/M/D
    hour_minute_second = date_time[1].split("+")[0].split("-")[0].split(":")

    return time.struct_time(
        (
            int(year_month_day[0]),
            int(year_month_day[1]),
            int(year_month_day[2]),
            int(hour_minute_second[0]),
            int(hour_minute_second[1]),
            int(hour_minute_second[2].split(".")[0]),
            -1,
            -1,
            is_dst,
        )
    )


def dim_display(status):

    if status == True:
        display.brightness = 0.01
        display14.brightness = 0.01
        pixels.brightness = 0.01
        displayBL.duty_cycle = 30000
    else:
        display.brightness = 0.6
        display14.brightness = 0.6
        pixels.brightness = 0.1
        displayBL.duty_cycle = 50000

def display_date(day, dayname):
    dayname = weekname[dayname]
    display14.marquee(dayname, 0.3, False)
    display14.print(f"{day:02d}")

def display_time(hours, mins):
    display.fill(0)
    if hours < 10:
        display.print("0" + str(hours))
    else:
        if hours < 13:
            display.print(hours)
        else:
            display.print(hours - 12)
    display.print(":")
    if mins < 10:
        display.print("0" + str(mins))
    else:
        display.print(mins)


def display_sec(secs):
    neo_seconds = secs
    neo_seconds += 30
    neo_seconds = neo_seconds % 60
    if secs == 60:
        pixels[neo_seconds] = RED
    else:
        pixels[neo_seconds] = GREEN
    pixels[neo_seconds - 1] = BLACK
    for x in range(20):
        x=x+1
        pixels[neo_seconds - x] = BLACK
  # account for Date Display update delay
    pixels.show()


def refesh_time():
    datetime, UTC_OFFSET, dayofweek = update_time(TIMEZONE)
    hours = datetime.tm_hour
    mins = datetime.tm_min
    secs = datetime.tm_sec
    month = datetime.tm_mon
    year= datetime.tm_year
    day = datetime.tm_mday
    dayofWeek = dayofweek

    return year, month, day, dayofWeek, hours, mins, secs

def displaySprite(spriteNumber=0):
    fb = dotclockframebuffer.DotClockFramebuffer(**tft_pins, **tft_timings)
    displaysprite = FramebufferDisplay(fb, auto_refresh=False, rotation = 90)

    if spriteNumber <= 8:
        sprite_sheet, palette = adafruit_imageload.load("/bmps/TheMoons720.bmp",
                                                    bitmap=displayio.Bitmap,
                                                    palette=displayio.Palette)
    else:
        sprite_sheet, palette = adafruit_imageload.load("/bmps/NEWBLUEMOON.bmp",
                                                    bitmap=displayio.Bitmap,
                                                    palette=displayio.Palette)

    # Create a sprite (tilegrid)
    sprite = displayio.TileGrid(sprite_sheet, pixel_shader=palette,
                                width = 1,
                                height = 1,
                                tile_width = 240,
                                tile_height = 240)

    # Create a Group to hold the sprite
    group = displayio.Group(scale=2)
    sprite[0] = spriteNumber % 9
    # Add the sprite to the Group
    group.append(sprite)

    # Add the Group to the Display
    displaysprite.root_group = group

    displaysprite.auto_refresh = True

    # Set sprite location
    group.x = 0
    group.y = 0

    # Loop through each sprite in the sprite sheet
    source_index = 0

# Initialize Clock and get it started

year, month, day, dayofWeek, hours, mins, secs = refesh_time()
display_sec(secs)
display_time(hours, mins)
display_date(day, dayofWeek)
print(year, month, day, dayofWeek, hours, mins, secs)
age = MoonData(year, month, day, LATITUDE, LONGITUDE)

print("MOONPHASE IS: ", moonPhase(age))

displaySprite(moonPhase(age))

# Start running the clock
while True:
    if hours >= 19 or hours <= 6:
        dim_display(True)
    else:
        dim_display(False)
    display_sec(secs)
    if secs == 60:
        mins = mins + 1
        if mins > 59:
            hours = hours + 1
            mins = 0
        if hours > 23:
            hours = 0
        secs = 0
        display_time(hours, mins)
    if refresh_time_update == 0:
        gc.collect()
        release_displays()
        year, month, day, dayofWeek, hours, mins, secs = refesh_time()
        display_time(hours, mins)
        display_date(day, dayofWeek)
        refresh_time_update = 7200 # Update every 2 hour
        age = MoonData(year, month, day, LATITUDE, LONGITUDE)
        displaySprite(moonPhase(age))

    #time.sleep(0.995)
    time.sleep(0.987)  # Adjust seconds to account display delays
    secs = secs + 1
    refresh_time_update = refresh_time_update - 1