"""Part 12, step 4: the controller decides. The reader reports badges and keys,
and lights, beeps and shows text only on the controller's command.
Needs osdp.py, mfrc522.py and ssd1306.py. Run controller.py on the Mac.
"""

from machine import Pin, PWM, UART, I2C, unique_id
import time
from mfrc522 import MFRC522
from ssd1306 import SSD1306_I2C
import osdp

ADDRESS = 1
READER_NUMBER = 0
RAW_BITS = 0
IDENTITY = bytes([0x00, 0x00, 0x00, 1, 1]) + unique_id()[-4:] + bytes([0, 1, 0])
CAPABILITIES = bytes([
    3, 1, 0,
    4, 4, 1,
    5, 2, 1,
    6, 1, 1,
    8, 1, 0,
    10, 128, 0,
])
BAD_LENGTH = bytes([2])
UNKNOWN_COMMAND = bytes([3])

KEYS = [
    ["1", "2", "3", "A"],
    ["4", "5", "6", "B"],
    ["7", "8", "9", "C"],
    ["*", "0", "#", "D"],
]
KEY_SCAN_MS = 20
BADGE_POLL_MS = 100
BADGE_GONE_AFTER_MISSES = 5

uart = UART(1, baudrate=9600, tx=Pin(4), rx=Pin(5))
transmit_enable = Pin(2, Pin.OUT, value=0)
rows = [Pin(n, Pin.OUT, value=1) for n in (9, 8, 7, 6)]
columns = [Pin(n, Pin.IN, Pin.PULL_UP) for n in (13, 12, 11, 10)]
red = Pin(14, Pin.OUT, value=0)
green = Pin(15, Pin.OUT, value=0)
buzzer = PWM(Pin(0))
buzzer.freq(4000)
buzzer.duty_u16(0)
reader = MFRC522(spi_id=0, sck=18, mosi=19, miso=16, cs=17, rst=20)
screen = SSD1306_I2C(128, 64, I2C(1, sda=Pin(26), scl=Pin(27)))
screen.write_cmd(0xA0)
screen.write_cmd(0xC0)


def send(frame):
    transmit_enable.value(1)
    uart.write(frame)
    uart.flush()
    transmit_enable.value(0)


def pressed_key():
    found = None
    for row, labels in zip(rows, KEYS):
        row.value(0)
        for column, label in zip(columns, labels):
            if column.value() == 0:
                found = label
        row.value(1)
    return found


def read_badge():
    reader.init()
    status, _ = reader.request(reader.REQIDL)
    if status == reader.OK:
        status, uid = reader.SelectTagSN()
    return uid if status == reader.OK else None


def show_line(message, row):
    screen.fill(0)
    screen.text(message[:16], 0, max(row - 1, 0) * 16)
    screen.show()


class Led:
    """A permanent colour, and a temporary one that wins until it times out."""

    def __init__(self):
        self.permanent = osdp.OFF
        self.temporary = osdp.OFF
        self.temporary_until = time.ticks_ms()

    def command(self, data):
        temporary_control, temporary_color = data[2], data[5]
        timer_ms = (data[7] | (data[8] << 8)) * 100
        permanent_control, permanent_color = data[9], data[12]
        if temporary_control == 2:
            self.temporary = temporary_color
            self.temporary_until = time.ticks_add(time.ticks_ms(), timer_ms)
        if temporary_control == 1:
            self.temporary_until = time.ticks_ms()
        if permanent_control == 1:
            self.permanent = permanent_color

    def color(self):
        temporary_active = time.ticks_diff(self.temporary_until, time.ticks_ms()) > 0
        return self.temporary if temporary_active else self.permanent

    def refresh(self):
        color = self.color()
        red.value(color in (osdp.RED, osdp.AMBER))
        green.value(color in (osdp.GREEN, osdp.AMBER))


class Buzzer:
    """Beeps in the background, so the reader keeps answering the bus."""

    def __init__(self):
        self.on_ms = self.off_ms = 0
        self.edges_left = 0
        self.next_edge = time.ticks_ms()

    def command(self, data):
        tone, on, off, count = data[1], data[2], data[3], data[4]
        self.on_ms, self.off_ms = on * 100, off * 100
        self.edges_left = 2 * count if tone == osdp.TONE_DEFAULT else 0
        self.next_edge = time.ticks_ms()
        buzzer.duty_u16(0)

    def refresh(self):
        if self.edges_left > 0 and time.ticks_diff(time.ticks_ms(), self.next_edge) >= 0:
            sounding = self.edges_left % 2 == 0
            buzzer.duty_u16(32768 if sounding else 0)
            self.next_edge = time.ticks_add(time.ticks_ms(), self.on_ms if sounding else self.off_ms)
            self.edges_left -= 1

    def click(self):
        buzzer.duty_u16(32768)
        time.sleep_ms(40)
        buzzer.duty_u16(0)


led = Led()
beeper = Buzzer()
pending_badges = []
pending_keys = ""
last_sequence = None
last_reply = b""


def badge_reply(uid):
    bits = len(uid) * 8
    return osdp.RAW, bytes([READER_NUMBER, RAW_BITS, bits & 0xFF, bits >> 8]) + bytes(uid)


def keys_reply(keys):
    return osdp.KEYPAD, bytes([READER_NUMBER, len(keys)]) + keys.encode()


def obey(action, data, smallest):
    if len(data) < smallest:
        return osdp.NAK, BAD_LENGTH
    action(data)
    return osdp.ACK, b""


def show_text(data):
    show_line(data[6 : 6 + data[5]].decode(), data[3])


def answer(command):
    global pending_keys
    code, data = command.code, command.data
    if code == osdp.POLL and pending_badges:
        return badge_reply(pending_badges.pop(0))
    if code == osdp.POLL and pending_keys:
        keys, pending_keys = pending_keys, ""
        return keys_reply(keys)
    if code == osdp.POLL:
        return osdp.ACK, b""
    if code == osdp.ID:
        return osdp.PDID, IDENTITY
    if code == osdp.CAP:
        return osdp.PDCAP, CAPABILITIES
    if code == osdp.LED:
        return obey(led.command, data, 14)
    if code == osdp.BUZ:
        return obey(beeper.command, data, 5)
    if code == osdp.TEXT:
        return obey(show_text, data, 6)
    return osdp.NAK, UNKNOWN_COMMAND


def handle(command):
    global last_sequence, last_reply
    repeated = command.sequence == last_sequence and command.sequence != 0
    if not repeated:
        code, data = answer(command)
        last_reply = osdp.reply(ADDRESS, command.sequence, code, data)
    last_sequence = command.sequence
    send(last_reply)


buffer = b""
previous_key = None
previous_badge = None
misses = 0
last_scan = time.ticks_ms()
last_poll = time.ticks_ms()
show_line("Waiting...", 1)

while True:
    if uart.any():
        buffer += uart.read()
    command, buffer = osdp.parse(buffer)
    if command is not None and command.address == ADDRESS:
        handle(command)

    led.refresh()
    beeper.refresh()

    if time.ticks_diff(time.ticks_ms(), last_scan) >= KEY_SCAN_MS:
        last_scan = time.ticks_ms()
        key = pressed_key()
        if key is not None and key != previous_key:
            beeper.click()
            pending_keys += key
        previous_key = key

    if time.ticks_diff(time.ticks_ms(), last_poll) >= BADGE_POLL_MS:
        last_poll = time.ticks_ms()
        uid = read_badge()
        misses = 0 if uid is not None else misses + 1
        if uid is not None and uid != previous_badge:
            pending_badges.append(uid)
            print("Badge queued:", reader.tohexstring(uid))
        if uid is not None or misses > BADGE_GONE_AFTER_MISSES:
            previous_badge = uid

    time.sleep_ms(2)
