"""Motion Detection example
This example shows how to use the CSI framework to connect to a FeitCSI host and
detect changes in the environment
"""

import numpy as np
import numpy.typing as npt
import requests

from where_fi.application import CSIApplication
from where_fi.collection.ingest import RealtimeCSIProducer
from where_fi.config import config
from where_fi.visualise import server as visualise

# Connect to a FeitCSI host
app = CSIApplication(visualise_raw=True)

# Register visualisations
app.register_figure(
    "magn-diff",
    visualise.figures.PerAntennaFigure(
        [
            visualise.figures.SimpleLineChart(
                "Magnitude diff", "Subcarrier", "Magnitude"
            ),
        ],
        [lambda x: x],
    ),
)
app.register_figure(
    "phase-diff",
    visualise.figures.PerAntennaFigure(
        [
            visualise.figures.SimpleLineChart("Phase diff", "Subcarrier", "Phase"),
        ],
        [lambda x: x],
    ),
)

MAGN_THRESHOLD = 0.2
PHASE_THRESHOLD = 0.02
QUEUE_SIZE = 20

# Stores historical data for each receiving antenna, for each subcarrier
historical = np.zeros(
    (QUEUE_SIZE, config.subcarriers, config.antennas.count, 1), dtype=np.complex64
)
sample_position = 0


class HomeAssistantBinarySensor:
    """
    Represents a binary sensor in Home Assistant.

    Uses the HTTP API [1] to update the state of the sensor.

    [1] - https://www.home-assistant.io/integrations/http/#binary-sensor
    """

    def __init__(self, id: str, name: str) -> None:
        self.id = id
        self.name = name

        BASE_URL = os.getenv("HOME_ASSISTANT_URL")
        API_KEY = os.getenv("HOME_ASSISTANT_API_KEY")

        self.url = f"{BASE_URL}/api/states/binary_sensor.{self.id}"
        self.headers = {"Authorization": f"Bearer {API_KEY}"}

        self.state = False

    def update(self, state: bool) -> None:
        if self.state == state:
            return

        self.state = state
        data = {
            "state": "on" if state else "off",
            "attributes": {"friendly_name": self.name, "device_class": "motion"},
        }
        print(f"Updating sensor {self.name} to {data}")
        requests.post(self.url, json=data, headers=self.headers)


sensor = HomeAssistantBinarySensor("motion_detector", "Room Motion Detector")


@app.on_process
def _(sample: npt.NDArray[np.complex64]) -> None:
    """
    Process the CSI data and detect changes in the environment.

    This function is called by the framework at a fixed interval, with the latest CSI
    sample received.

    It is used to detect changes in the environment caused by motion, by comparing each
    entry in the matrix with a moving average
    """
    global sample_position

    historical[sample_position] = sample
    sample_position = (sample_position + 1) % QUEUE_SIZE

    mean = np.mean(historical, axis=0)
    magn_diff = np.abs(mean - sample)
    phase_diff = np.abs(np.angle(mean) - np.angle(sample))

    app.visualise_data(magn_diff, "magn-diff")
    app.visualise_data(phase_diff, "phase-diff")

    if (magn_diff > MAGN_THRESHOLD).any() or (phase_diff > PHASE_THRESHOLD).any():
        print("Motion detected!")
        sensor.update(True)
    else:
        print("No motion detected!")
        sensor.update(False)


if __name__ == "__main__":
    # Start the application
    print("Starting app")
    app.set_producer(RealtimeCSIProducer())
    app.start()
