from flask import Flask, render_template
from flask_socketio import SocketIO
import numpy as np
import sounddevice as sd
import subprocess
import queue
from threading import Thread, Event
import RPi.GPIO as GPIO
import time, random, wave, os

app = Flask(__name__)
socketio = SocketIO(app, async_mode="threading")

# ==== AUDIO CONFIG ====
SAMPLERATE = 44100
BLOCKSIZE = 4096

audio_queue = queue.Queue(maxsize=5)
playback_buffer = np.zeros(SAMPLERATE, dtype=np.int16)  # ring buffer
write_pos = 0

# ==== IMAGE PATHS ====
CLOSED_IMG = "/home/pi/Webserver/images/closed.png"
OPEN_IMG   = "/home/pi/Webserver/images/open.png"

last_talking = False

# ==== AUTOPILOT FLAGS ====
mic_enabled = False
autopilot_running = True
stop_event = Event()

# ==== GPIO SETUP ====
PIR_PIN = 21
DT_PIN = 4
SCK_PIN = 26

GPIO.setmode(GPIO.BCM)
GPIO.setup(PIR_PIN, GPIO.IN)

# HX711 setup
def read_hx711():
    GPIO.setup(SCK_PIN, GPIO.OUT)
    GPIO.setup(DT_PIN, GPIO.IN)

    count = 0
    while GPIO.input(DT_PIN) == 1:
        pass
    for i in range(24):
        GPIO.output(SCK_PIN, 1)
        count = count << 1
        GPIO.output(SCK_PIN, 0)
        if GPIO.input(DT_PIN) == 1:
            count += 1
    GPIO.output(SCK_PIN, 1)
    GPIO.output(SCK_PIN, 0)
    if(count & 0x800000):
        count -= 16777216
    return count

baseline_weight = None
last_pir_time = 0
last_weight_time = 0

# ==== SOUND FOLDERS ====
PIR_SOUNDS = "/home/pi/Webserver/sounds/pir"
WEIGHT_SOUNDS = "/home/pi/Webserver/sounds/weight"

def play_wav_file(path):
    try:
        wf = wave.open(path, 'rb')
        frames = wf.getnframes()
        data = wf.readframes(frames)
        audio = np.frombuffer(data, dtype=np.int16)
        wf.close()

        # queue data into main stream
        chunk_size = 1024
        for i in range(0, len(audio), chunk_size):
            while audio_queue.full():
                time.sleep(0.005)
            audio_queue.put(audio[i:i+chunk_size])

    except Exception as e:
        print("Error playing sound:", e)
        
def play_wav_blocked(path):
    global audio_queue
    
    with wave.open(path, 'rb') as wf:
        assert wf.getframerate() == SAMPLERATE
        assert wf.getnchannels() == 1
        assert wf.getsampwidth() == 2
        
        data = wf.readframes(BLOCKSIZE)
        
        while data and not mic_active.is_set():
            chunk = np.frombuffer(data, dtype=np.int16)
            
            try:
                audio_queue.put(chunk, timeout=0.1)
            except queue.Full:
                print("full")
                pass
            time.sleep(BLOCKSIZE/SAMPLERATE)
            data = wf.readframes(BLOCKSIZE)

# ==== FBI IMAGE HANDLER ====
img_queue = queue.Queue(maxsize=1)

def show_image_worker():
    while not stop_event.is_set():
        try:
            img = img_queue.get(timeout=0.1)
            subprocess.run(
                ["sudo", "fbi", "-T", "1", "-d", "/dev/fb0", "-a", "-y", "--noverbose", img],
                stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
            )
        except queue.Empty:
            pass

Thread(target=show_image_worker, daemon=True).start()

def set_image(img):
    img_queue.queue.clear()
    img_queue.put_nowait(img)

# ==== AUDIO CALLBACK ====
def audio_callback(outdata, frames, time_info, status):
    global write_pos, playback_buffer
    try:
        chunk = audio_queue.get_nowait()
    except queue.Empty:
        chunk = np.zeros(frames, dtype=np.int16)

    end_pos = write_pos + len(chunk)
    if end_pos <= len(playback_buffer):
        playback_buffer[write_pos:end_pos] = chunk
    else:
        first = len(playback_buffer) - write_pos
        playback_buffer[write_pos:] = chunk[:first]
        playback_buffer[:end_pos % len(playback_buffer)] = chunk[first:]
    write_pos = end_pos % len(playback_buffer)

    out_chunk = np.zeros(frames, dtype=np.int16)
    buf_pos = (write_pos - frames) % len(playback_buffer)
    if buf_pos + frames <= len(playback_buffer):
        out_chunk[:] = playback_buffer[buf_pos:buf_pos+frames]
    else:
        first = len(playback_buffer) - buf_pos
        out_chunk[:first] = playback_buffer[buf_pos:]
        out_chunk[first:] = playback_buffer[:frames-first]

    outdata[:, 0] = out_chunk

stream = sd.OutputStream(
    samplerate=SAMPLERATE,
    channels=1,
    dtype='int16',
    blocksize=BLOCKSIZE,
    callback=audio_callback
)

# ==== SOCKET HANDLERS ====
@socketio.on("mic_enabled")
def mic_on():
    global mic_enabled
    mic_enabled = True
    print("MIC ENABLED")

@socketio.on("mic_disabled")
def mic_off():
    global mic_enabled
    mic_enabled = False
    print("MIC DISABLED")

@socketio.on('audio_chunk')
def handle_audio_chunk(data):
    global last_talking
    if not mic_enabled:
        return

    audio = np.frombuffer(data, dtype=np.int16)

    try:
        audio_queue.put_nowait(audio)
    except queue.Full:
        pass

    rms = float(np.sqrt(np.mean(audio.astype(np.float32)**2)))
    talking = rms > 1000

    if talking != last_talking:
        set_image(OPEN_IMG if talking else CLOSED_IMG)
        last_talking = talking

    if not stream.active:
        stream.start()

# ==== AUTOPILOT THREAD ====
def autopilot_loop():
    global baseline_weight, last_pir_time, last_weight_time

    # establish baseline
    baseline_weight = read_hx711()

    while not stop_event.is_set():
        if mic_enabled:
            time.sleep(0.2)
            continue

        # PIR check
        if GPIO.input(PIR_PIN) == 1 and time.time() - last_pir_time > 15:
            socketio.emit("presence_update", {"status": "DETECTED"})
            sound = random.choice(os.listdir(PIR_SOUNDS))
            play_wav_file(os.path.join(PIR_SOUNDS, sound))
            last_pir_time = time.time()
            time.sleep(15)
            socketio.emit("presence_update", {"status": "Vacant"})

        # HX711 check
        weight = read_hx711()
        grams = (weight - baseline_weight) / 100  # crude scale factor
        if abs(grams) > 15 and time.time() - last_weight_time > 10:
            sound = random.choice(os.listdir(WEIGHT_SOUNDS))
            play_wav_file(os.path.join(WEIGHT_SOUNDS, sound))
            last_weight_time = time.time()
            time.sleep(10)

        time.sleep(0.05)

Thread(target=autopilot_loop, daemon=True).start()

# ==== ROUTES ====
@app.route('/')
def index():
    return render_template("index.html")

# ==== RUN ====
if __name__ == "__main__":
    set_image(CLOSED_IMG)
    socketio.run(app, host="0.0.0.0", port=5000, use_reloader=False)
    stop_event.set()
    GPIO.cleanup()
