import gc
import utime
import ujson
import network
import urequests
from machine import Pin

THRESHOLD_DISTANCE = 15  # cm
SOUND_SPEED = 340 * 100  # m/s * centi = cm/s
MAX_CONNECTION_RETRIES = 50


class App:
    ssid: str
    password: str
    server: str
    token: str

    opened: bool = False
    previously_opened: bool = False
    wlan: network.WLAN

    led = Pin(15, Pin.OUT)
    trigger = Pin(2, Pin.OUT)
    echo = Pin(3, Pin.IN)

    def __init__(self):
        print("Loading .env.json...")
        self.load_env()

    def load_env(self):
        try:
            with open(".env.json") as stream:
                data = ujson.load(stream)

                network = data.get("network")
                if network is None:
                    print("'network' missing in env")
                    exit(1)

                server = data.get("server")
                if server is None:
                    print("'server' missing in env")
                    exit(1)

                self.ssid = network.get("ssid")
                self.password = network.get("password")
                self.server = server.get("address")
                self.token = server.get("token")

                if None in (self.ssid, self.password, self.server, self.token):
                    print("Missing properties in env")
                    exit(1)
        except Exception as e:
            print(f"Error loading .env.json: {e}")
            exit(1)

    def connect(self):
        print("Connecting to Wi-Fi...")
        self.wlan = network.WLAN(network.STA_IF)

        self.wlan.active(False)
        utime.sleep_ms(250)
        self.wlan.active(True)
        utime.sleep_ms(250)

        self.wlan.connect(self.ssid, self.password)
        utime.sleep_ms(100)

        retry_count = 0
        while not self.wlan.isconnected():
            if retry_count >= MAX_CONNECTION_RETRIES:
                print("Max connection retries reached")
                exit(1)

            print(f"Waiting for connection{(retry_count % 3 + 1) * '.'}{3 * ' '}", end="\r")
            retry_count += 1

            for _ in range(4):
                self.led.toggle()
                utime.sleep_ms(250)

            if retry_count % 10 == 0:
                print("Attempting to restart connection...")
                self.wlan.connect(self.ssid, self.password)
                for _ in range(10):
                    self.led.toggle()
                    utime.sleep_ms(50)

        print(f"Connected with IP {self.wlan.ifconfig()[0]}")
        self.update_server()

    def update_server(self):
        print("Updating state...", end="\r")
        data = {"opened": self.opened}
        raw = ujson.dumps(data)

        try:
            r = urequests.post(
                f"{self.server}/opened",
                headers={"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"},
                data=raw
            )
            print(f"State updated [{r.status_code}] {r.content.decode()}")
        except Exception as e:
            print(f"Error occurred: {e}")

    def measure_distance(self) -> float:
        self.trigger.low()
        utime.sleep_us(2)
        self.trigger.high()
        utime.sleep_us(10)
        self.trigger.low()

        start_time = utime.ticks_us()
        sent_time = utime.ticks_us()
        while self.echo.value() == 0:
            print("hey", end="\r")
            sent_time = utime.ticks_us()
            if sent_time - start_time >= 100_000:  # if it takes more than 100ms, stop
                return -1

        start_time = utime.ticks_us()
        received_time = utime.ticks_us()
        while self.echo.value() == 1:
            print("ho", end="\r")
            received_time = utime.ticks_us()
            if received_time - start_time >= 100_000:  # same
                return -1

        delta_time = received_time - sent_time
        distance = delta_time / 1_000_000 * SOUND_SPEED / 2
        print(f"Distance: {distance:0<5} cm; mem_free = {gc.mem_free()}")

        return distance

    def run(self):
        print("Starting door alarm...")
        self.connect()

        i = 0
        while True:
            try:
                if not self.wlan.isconnected():
                    self.connect()

                distance = self.measure_distance()
                self.opened = distance >= THRESHOLD_DISTANCE

                if not self.opened:
                    self.led.low()

                    # was it just closed?
                    if self.previously_opened:
                        self.update_server()
                        self.previously_opened = False
                else:
                    # was it just opened? +wait delay
                    if not self.previously_opened:
                        self.led.high()
                        self.update_server()
                        self.previously_opened = True

                if i >= 20:
                    print("Blinking...")
                    self.led.toggle()
                    utime.sleep_ms(100)
                    self.led.toggle()
                    utime.sleep_ms(400)

                    i = 0
                    gc.collect()
                else:
                    utime.sleep_ms(500)

            except Exception as e:
                print(f"Fatal error occurred: {e}")

            i += 1


if __name__ == "__main__":
    App().run()
