"""
Based on the Bluefruit TFT Gizmo ANCS Notifier for iOS Learn guide:
https://learn.adafruit.com/ancs-gizmo?view=all
"""

import time
import math
import array
import board
import digitalio
import analogio
import audiobusio
import displayio
import adafruit_ble
import adafruit_imageload
import adafruit_lis3mdl
import adafruit_sht31d
from adafruit_ble.advertising.standard import SolicitServicesAdvertisement
from adafruit_ble_apple_notification_center import AppleNotificationCenterService
from babel.babel import Babel # used for Unifont support but you can BYO font
from adafruit_display_text import label

# apps we want to listen for
WHITELIST = ["com.apple.reminders", "com.grailr.CARROTweather", "com.google.Maps", "com.tinyspeck.chatlyio"]

# buttons (not currently used)
DELAY_AFTER_PRESS = 15
DEBOUNCE = 0.1
a = digitalio.DigitalInOut(board.BUTTON_A)
a.switch_to_input(pull=digitalio.Pull.DOWN)
b = digitalio.DigitalInOut(board.BUTTON_B)
b.switch_to_input(pull=digitalio.Pull.DOWN)

# sensors
i2c = board.I2C()
sht31 = adafruit_sht31d.SHT31D(i2c)
lis3mdl = adafruit_lis3mdl.LIS3MDL(i2c)

mic = audiobusio.PDMIn(board.MICROPHONE_CLOCK, board.MICROPHONE_DATA, sample_rate=16000, bit_depth=16)
samples = array.array('H', [0] * 160)

# fonts
babel = Babel()

def normalized_rms(values):
    mean_values = int(sum(values) / len(values))
    return math.sqrt(sum(float(sample - mean_values) * (sample - mean_values)
                         for sample in values) / len(values))

def find_connection():
    for connection in radio.connections:
        if AppleNotificationCenterService not in connection:
            continue
        if not connection.paired:
            connection.pair()
        return connection, connection[AppleNotificationCenterService]
    return None, None

# Start advertising before messing with the display so that we can connect immediately.
radio = adafruit_ble.BLERadio()
advertisement = SolicitServicesAdvertisement()
advertisement.solicited_services.append(AppleNotificationCenterService)

def wrap_in_tilegrid(open_file):
    odb = displayio.OnDiskBitmap(open_file)
    return displayio.TileGrid(odb, pixel_shader=displayio.ColorConverter())

display = board.DISPLAY
group = displayio.Group(max_size=10)
group.append(wrap_in_tilegrid(open("/background.bmp", "rb")))

compass_sheet, palette = adafruit_imageload.load("/compass.bmp",
                                                bitmap=displayio.Bitmap,
                                                palette=displayio.Palette)
compass = displayio.TileGrid(compass_sheet, pixel_shader=palette,
                            width = 1,
                            height = 1,
                            tile_width = 31,
                            tile_height = 31)
compass.x = 209
group.append(compass)

arrow_sheet, arrow_palette = adafruit_imageload.load("/arrows.bmp",
                                                bitmap=displayio.Bitmap,
                                                palette=displayio.Palette)
arrow = displayio.TileGrid(arrow_sheet, pixel_shader=arrow_palette,
                            width = 1,
                            height = 1,
                            tile_width = 64,
                            tile_height = 64)
arrow.y = 176
group.append(arrow)

heading_label = label.Label(babel.font, max_glyphs=30, color=0xFFFFFF)
heading_label.y = 8
group.append(heading_label)

temp_label = label.Label(babel.font, max_glyphs=10, color=0xFFFFFF)
temp_label.x = 0
temp_label.y = 24
group.append(temp_label)

humidity_label = label.Label(babel.font, max_glyphs=10, color=0xFFFFFF)
humidity_label.x = 72
humidity_label.y = 24
group.append(humidity_label)

noise_label = label.Label(babel.font, max_glyphs=30, color=0xFFFFFF)
noise_label.x = 144
noise_label.y = 24
group.append(noise_label)

main_label = label.Label(babel.font, max_glyphs=7 * 30, color=0xFFFFFF)
main_label.y = 102
group.append(main_label)

directions_label = label.Label(babel.font, max_glyphs=4 * 22, color=0x000000)
directions_label.x = 68
directions_label.y = 208
group.append(directions_label)

display.show(group)

current_notifications = {}
all_ids = []
last_update = None
active_connection, notification_service = find_connection()

# this method hilariously doesn't return anything remotely resembling a standard timestamp,
# i kind of gave up halfway through, but it mostly seems to order things in order.
def iso_to_timestamp(isodate):
    years = int(isodate[0:4])
    months = int(isodate[4:6])
    days = int(isodate[6:8])
    hours = int(isodate[9:11])
    minutes = int(isodate[11:13])
    seconds = int(isodate[13:15])
    
    centuries = years // 100
    leaps = centuries // 4
    leapDays = 2 - centuries + leaps
    yearDays = int(365.25 * (years + 4716))
    monthDays = int(30.6001 * (months + 1))
    julian_date = int(leapDays + days + monthDays + yearDays -1524.5)

    julian_date -= 2458800

    return julian_date + (seconds + minutes * 60 + hours * 3600) / 26400

def wrap(string, length):
    words = string.split(' ')
    wrapped = ""
    line_length = 0
    for word in words:
        if line_length + len(word) <= length:
            wrapped += word + ' '
            line_length += len(word) + 1
        else:
            wrapped += '\n' + word + ' '
            line_length = len(word) + 1
            
    return wrapped

while True:
    if not active_connection:
        radio.start_advertising(advertisement)

    while not active_connection:
        active_connection, notification_service = find_connection()

    # Connected
    while active_connection.connected:
        start_time = None
        all_ids.clear()
        current_notifications = notification_service.active_notifications
        for notif_id in current_notifications:
            notification = current_notifications[notif_id]
            t = iso_to_timestamp(notification._raw_date)
            if start_time is None or t > start_time:
                start_time = t
            if notification.app_id in WHITELIST:
                all_ids.append(notif_id)
            else:
                del current_notifications[notif_id] # i have way too many notifications to keep in memory
        all_ids.sort(key=lambda x: current_notifications[x]._raw_date)
        if len(all_ids) == 0:
            continue
        latest_update = current_notifications[all_ids[len(all_ids) - 1]]
        if latest_update != last_update:
            # print('LATEST UPDATE:', latest_update._raw_date)
            reminders = list()
            inthehouse = set()
            weather = None
            directions = None
            text = ""
            for notif_id in reversed(all_ids):
                current_notification = current_notifications[notif_id]
                t = iso_to_timestamp(current_notification._raw_date)
                # print(current_notification._raw_date, current_notification.id, start_time - t, current_notification.app_id, current_notification)
                if current_notification.app_id == "com.apple.reminders":
                    reminders.append('- ' + current_notification.title)
                elif current_notification.app_id == "com.grailr.CARROTweather" and weather is None:
                    weather = current_notification.message.replace('↑', 'H').replace('↓', 'L').split('.')[0] + '.'
                elif current_notification.app_id == "com.google.Maps" and directions is None:
                    directions = current_notification.message
                elif current_notification.app_id == "com.tinyspeck.chatlyio" and (start_time - t) < 0.5 and current_notification.message.endswith("in the house!"):
                    # my workshop's slack says something like "Joey is in the house!"; adapt to whatever you want from Slack.
                    elements = current_notification.message.split(' ')
                    inthehouse.add(elements[3])
            
            inthehouse_text = ', '.join(inthehouse) if inthehouse else 'No one'
            weather_wrapped = wrap(weather, 30) if weather else "No weather forecast."
            main_label.text = weather_wrapped + '\nReminders:\n' + ('\n'.join(reminders) if reminders else '  None') + '\nAt the Workshop:\n  ' + inthehouse_text
            if directions is not None:
                directions_label.text = wrap(directions, 21)
                directions = directions.lower()
                slight = "slight" in directions
                if "left" in directions:
                    arrow[0] = 5 if slight else 1
                elif "right" in directions:
                    arrow[0] = 6 if slight else 2
                elif "straight" in directions:
                    arrow[0] = 3
                elif "u-turn" in directions:
                    arrow[0] = 7
                elif "arrive" in directions:
                    arrow[0] = 4
                else:
                    arrow[0] = 0
            else:
                arrow[0] = 0
            last_update = latest_update

        temp = sht31.temperature * 9 / 5 + 32
        if temp >= 100:
            temp_label.color = 0xFF0000
        elif temp >= 90:
            temp_label.color = 0xFF9300
        elif temp >= 80:
            temp_label.color = 0xFFD479
        elif temp >= 70:
            temp_label.color = 0xD4FB79
        elif temp >= 60:
            temp_label.color = 0x73FCD6
        elif temp >= 50:
            temp_label.color = 0x73FDFF
        elif temp >= 40:
            temp_label.color = 0x76D6FF
        elif temp >= 30:
            temp_label.color = 0x0096FF
        elif temp >= 20:
            temp_label.color = 0x0433FF
        else:
            temp_label.color = 0x0000FF
        temp_label.text = str(int(temp)) + '° F'
        
        
        humidity = sht31.relative_humidity
        if humidity <= 20:
            humidity_label.color = 0xFFFC79
        elif humidity <= 40:
            humidity_label.color = 0xD4FB79
        elif humidity <= 60:
            humidity_label.color = 0x73FA79
        elif humidity <= 80:
            humidity_label.color = 0x73FCD6
        else:
            humidity_label.color = 0x73FDFF
        humidity_label.text = str(int(humidity)) + '% RH'
        
        mic.record(samples, len(samples))
        rms = normalized_rms(samples)
        db = 24 + 20 * math.log(rms, 10)
        if db < 80:
            noise_label.color = 0x00F900
        elif db < 100:
            noise_label.color = 0xFFCC00
        else:
            noise_label.color = 0xCC0000
        noise_label.text = str(int(db)) + ' dB'
        
        mag_x, mag_y, mag_z = lis3mdl.magnetic
        heading = 180 * (math.atan2(mag_y, mag_x) / math.pi)
        # print('X:{0:10.2f}, Y:{1:10.2f}, Z:{2:10.2f} uT'.format(mag_x, mag_y, mag_z))
        # print(heading)
        heading_label.text = "Heading:" + str(int(heading))
        compass[0] = int((heading + 22.5 ) / 45) % 8

    # Bluetooth Disconnected
    active_connection = None
    notification_service = None