"""Plays the controller for the reader (address 1) and the door (address 2), on one bus.

A known badge or PIN 1234 opens the door for 5 seconds. PIN 7777 holds it
open, PIN 0000 locks it back. Everything the door reports is printed.

python3 door_controller.py [port]

Needs osdp.py and controller.py from the reader, in the same folder or in
../../diy-reader/code as in the wardn repository.
"""

import os
import sys
import time

sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "diy-reader", "code"))

import osdp
from controller import IDLE_TEXT, KNOWN_BADGES, POLL_INTERVAL_S, PORT, decide, open_port, sequence_numbers, talk

READER = 1
DOOR = 2
OPEN_DS = 50
PIN_OPEN = "1234"
PIN_HOLD_OPEN = "7777"
PIN_LOCK = "0000"
LABELS = {READER: "reader ", DOOR: "door   "}


def at_reader(commands):
    return [(READER, code, data) for code, data in commands]


def at_door(control, message):
    return [
        (DOOR, osdp.OUT, osdp.output(control, OPEN_DS if control == osdp.OUTPUT_PULSE else 0)),
        (DOOR, osdp.TEXT, osdp.text(message)),
    ]


def on_pin(typed):
    """The commands a complete PIN calls for, at the reader and at the door."""
    actions = {
        PIN_OPEN: (osdp.OUTPUT_PULSE, "PIN: open 5 s"),
        PIN_HOLD_OPEN: (osdp.OUTPUT_ON, "PIN: held open"),
        PIN_LOCK: (osdp.OUTPUT_OFF, "PIN: locked"),
    }
    granted = typed in actions
    door = at_door(*actions[typed]) if granted else []
    return at_reader(decide(granted)) + door


def on_keys(keys, typed):
    """Returns the PIN typed so far and the commands these keys call for."""
    commands = []
    for key in keys:
        if key == "#":
            commands, typed = on_pin(typed), ""
        elif key == "*":
            commands, typed = at_reader([(osdp.TEXT, osdp.text(IDLE_TEXT))]), ""
        else:
            typed += key
            commands = at_reader([(osdp.TEXT, osdp.text("PIN: " + "*" * len(typed)))])
    return typed, commands


def on_badge(uid):
    granted = uid in KNOWN_BADGES
    door = at_door(osdp.OUTPUT_PULSE, "badge: open 5 s") if granted else []
    return at_reader(decide(granted)) + door


def on_door_report(answer):
    if answer.code == osdp.ISTATR:
        print("   door input: wired pulse", "on" if answer.data[0] else "off")
    if answer.code == osdp.OSTATR:
        print("   door is now", "open" if answer.data[0] else "closed")


def on_answer(address, answer, typed):
    if answer is not None and address == READER and answer.code == osdp.KEYPAD:
        return on_keys(answer.data[2 : 2 + answer.data[1]].decode(), typed)
    if answer is not None and address == READER and answer.code == osdp.RAW:
        return "", on_badge(answer.data[4:].hex(" ").upper())
    if answer is not None and address == DOOR:
        on_door_report(answer)
    return typed, []


def main():
    port = open_port(PORT)
    sequences = {READER: sequence_numbers(), DOOR: sequence_numbers()}

    def ask(address, code, data=b""):
        return talk(port, next(sequences[address]), code, data, address, LABELS[address])

    for address in (READER, DOOR):
        ask(address, osdp.ID, b"\x00")
        ask(address, osdp.CAP, b"\x00")

    typed = ""
    outbox = at_reader([(osdp.TEXT, osdp.text(IDLE_TEXT))]) + [(DOOR, osdp.TEXT, osdp.text("Waiting"))]
    idle_at = None
    polled = DOOR
    while True:
        if outbox:
            address, code, data = outbox.pop(0)
        else:
            polled = READER if polled == DOOR else DOOR
            address, code, data = polled, osdp.POLL, b""
        typed, commands = on_answer(address, ask(address, code, data), typed)
        outbox += commands
        if any(code == osdp.LED for _, code, _ in commands):
            idle_at = time.monotonic() + 2
        if idle_at is not None and time.monotonic() >= idle_at:
            outbox += at_reader([(osdp.TEXT, osdp.text(IDLE_TEXT))])
            idle_at = None
        time.sleep(0 if outbox else POLL_INTERVAL_S / 2)


if __name__ == "__main__":
    main()
