#!/usr/bin/env python3
"""
Real-Time OpenGL 8D Chromatic Visualization (fixed)

This file replaces a corrupted script. It reads the log in a background thread
so the main/render thread never blocks on I/O. It also logs unexpected
exceptions to a file so we can diagnose crashes.
"""

import os
import re
import time
import ctypes
import threading
import traceback
from collections import deque

import numpy as np
import glfw
from OpenGL.GL import *
from OpenGL.GLU import gluPerspective

LOG_FILE = os.path.join(os.path.dirname(__file__), '..', 'logs', 'peer1.log')
ERROR_LOG = os.path.join(os.path.dirname(__file__), '..', 'logs', 'opengl_error.log')


class Monitor:
    """Background tailer for the log file.

    Keeps a deque of recent 8-D positions and lightweight summary fields.
    """

    def __init__(self, maxlen=2000):
        self.positions = deque(maxlen=maxlen)
        self._lock = threading.Lock()
        self._stop = threading.Event()
        self._thread = None
        self.last_pos = 0
        self.omega = 0.0
        self.evol = 0

    def _tail_loop(self):
        try:
            # If file doesn't exist yet, keep trying
            while not os.path.exists(LOG_FILE) and not self._stop.is_set():
                time.sleep(0.1)

            with open(LOG_FILE, 'r', encoding='utf-8', errors='ignore') as f:
                # start at EOF so we only read new entries
                f.seek(0, 2)
                partial_dims = {}
                while not self._stop.is_set():
                    line = f.readline()
                    if not line:
                        time.sleep(0.05)
                        continue

                    # parse incoming line(s) and accumulate partial_dims across lines
                    try:
                        if 'Ω:' in line and 'Evolution:' in line:
                            try:
                                self.omega = float(line.split('Ω:')[1].split()[0])
                            except Exception:
                                pass
                            try:
                                self.evol = int(line.split('Evolution:')[1].split('│')[0].strip())
                            except Exception:
                                pass

                        # Extract any D# values present on this line and merge into partial_dims
                        for i in range(1, 9):
                            token = f'D{i}:'
                            if token in line:
                                try:
                                    # find the token and take the following value
                                    after = line.split(token, 1)[1]
                                    val = after.split('[')[0].split()[0].strip().rstrip(',')
                                    # sanitize
                                    val = re.sub(r"[^0-9eE+\-\.]+", '', val)
                                    partial_dims[i - 1] = float(val)
                                except Exception:
                                    pass

                        # If we've collected all 8 dims across recent lines, append and reset
                        if len(partial_dims) == 8:
                            try:
                                vec = [partial_dims[j] for j in range(8)]
                                with self._lock:
                                    self.positions.append(vec)
                            except Exception:
                                with open(ERROR_LOG, 'a', encoding='utf-8') as ef:
                                    ef.write('Monitor append error:\n')
                                    ef.write(traceback.format_exc())
                            partial_dims = {}
                    except Exception:
                        # Keep tail running even on parse errors
                        with open(ERROR_LOG, 'a', encoding='utf-8') as ef:
                            ef.write('Monitor parse error:\n')
                            ef.write(traceback.format_exc())
        except Exception:
            with open(ERROR_LOG, 'a', encoding='utf-8') as ef:
                ef.write('Monitor thread fatal error:\n')
                ef.write(traceback.format_exc())

    def start(self):
        if self._thread and self._thread.is_alive():
            return
        self._stop.clear()
        self._thread = threading.Thread(target=self._tail_loop, daemon=True)
        self._thread.start()

    def stop(self):
        self._stop.set()
        if self._thread:
            self._thread.join(timeout=1.0)

    def get_positions(self):
        with self._lock:
            return list(self.positions)


def process_data(positions):
    """Convert deque/list of 8-D positions to 3D arrays for rendering.

    Returns (x, y, z) numpy arrays or (None, None, None) if not enough data.
    """
    if not positions:
        return None, None, None

    arr = np.array(positions, dtype=np.float64)
    # if the log produced occasional invalid rows, guard
    if arr.size == 0 or arr.shape[1] < 8:
        return None, None, None

    # log10 scaling to compress dynamic range
    arr = np.log10(np.maximum(np.abs(arr), 1e-100))

    # normalize each column
    for i in range(8):
        col = arr[:, i]
        vmin = np.nanmin(col)
        vmax = np.nanmax(col)
        if np.isfinite(vmin) and np.isfinite(vmax) and vmax > vmin:
            arr[:, i] = (col - vmin) / (vmax - vmin + 1e-12)
        else:
            arr[:, i] = 0.5

    # musical projection
    x = (arr[:, 0] * 0.6 + arr[:, 7] * 0.4) * 2 - 1
    y = (arr[:, 2] * 0.5 + arr[:, 5] * 0.5) * 2 - 1
    z = (arr[:, 4] * 0.5 + arr[:, 3] * 0.5) * 2 - 1
    return x, y, z


def get_color_gradient(index, total):
    t = index / max(total - 1, 1)
    r = 0.15 + 0.85 * (1.0 - t) ** 2
    g = 0.2 + 0.7 * np.sin(np.pi * t)
    b = 0.9 * t
    return r, g, b


def main():
    try:
        print('\n' + '=' * 70)
        print('  🚀 REAL-TIME OPENGL 8D CHROMATIC VISUALIZATION (fixed)')
        print('=' * 70)

        if not glfw.init():
            print('❌ Failed to initialize GLFW')
            return

        window = glfw.create_window(1200, 800, '8D Chromatic Attractor - Real-Time', None, None)
        if not window:
            glfw.terminate()
            print('❌ Failed to create window')
            return

        glfw.make_context_current(window)
        glfw.swap_interval(1)

        # Position and show
        try:
            user32 = ctypes.windll.user32
            screen_w = user32.GetSystemMetrics(0)
            screen_h = user32.GetSystemMetrics(1)
            wx = max(0, (screen_w - 1200) // 2)
            wy = max(0, (screen_h - 800) // 2)
            glfw.set_window_pos(window, wx, wy)
            glfw.show_window(window)
            try:
                hwnd = glfw.get_win32_window(window)
                user32.SetForegroundWindow(hwnd)
                user32.BringWindowToTop(hwnd)
            except Exception:
                pass
        except Exception:
            pass

        # Quick pipeline test
        glClearColor(0.02, 0.02, 0.05, 1.0)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glLoadIdentity()
        glBegin(GL_TRIANGLES)
        glColor3f(1, 0, 0); glVertex3f(-0.7, -0.7, -1)
        glColor3f(0, 1, 0); glVertex3f(0.7, -0.7, -1)
        glColor3f(0, 0, 1); glVertex3f(0.0, 0.7, -1)
        glEnd()
        glfw.swap_buffers(window)
        glfw.poll_events()
        time.sleep(0.3)

        # OpenGL state for main render
        glClearColor(0.02, 0.02, 0.05, 1.0)
        glEnable(GL_DEPTH_TEST)
        glEnable(GL_BLEND)
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
        glPointSize(3.0)

        glMatrixMode(GL_PROJECTION)
        glLoadIdentity()
        gluPerspective(45, 1200 / 800, 0.1, 50.0)
        glMatrixMode(GL_MODELVIEW)

        monitor = Monitor()
        monitor.start()

        angle = 0.0
        frame = 0
        last_fps_time = time.perf_counter()
        frames_since = 0

        # Main loop
        while not glfw.window_should_close(window):
            start = time.perf_counter()

            # Always process OS events ASAP
            glfw.poll_events()

            positions = monitor.get_positions()
            x, y, z = process_data(positions)

            if x is not None:
                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
                glLoadIdentity()
                glTranslatef(0.0, 0.0, -5.0)
                glRotatef(angle, 0, 1, 0)
                glRotatef(20, 1, 0, 0)

                n = len(x)
                glBegin(GL_POINTS)
                for i in range(n):
                    r, g, b = get_color_gradient(i, n)
                    glColor4f(r, g, b, 0.8)
                    glVertex3f(float(x[i]), float(y[i]), float(z[i]))
                glEnd()

                if n > 100:
                    glLineWidth(0.5)
                    glBegin(GL_LINE_STRIP)
                    step = max(1, n // 200)
                    for i in range(0, n, step):
                        r, g, b = get_color_gradient(i, n)
                        glColor4f(r, g, b, 0.15)
                        glVertex3f(float(x[i]), float(y[i]), float(z[i]))
                    glEnd()

                glfw.swap_buffers(window)

            # minimal logging: print FPS once per second
            frames_since += 1
            now = time.perf_counter()
            if now - last_fps_time >= 1.0:
                fps = frames_since / (now - last_fps_time)
                frames_since = 0
                last_fps_time = now
                print(f"Frame {frame}: pts={len(positions)} ω={monitor.omega:.0f} evol={monitor.evol} fps={fps:.1f}")

            angle = (angle + 0.5) % 360.0
            frame += 1

            # let OS do other things; small sleep keeps loop from pegging CPU but preserves responsiveness
            elapsed = time.perf_counter() - start
            to_sleep = max(0.0, 0.012 - elapsed)
            if to_sleep:
                # instead of time.sleep, let glfw wait for a tiny timeout to process events
                glfw.wait_events_timeout(to_sleep)

    except Exception:
        # Write a detailed traceback for post-mortem
        with open(ERROR_LOG, 'a', encoding='utf-8') as ef:
            ef.write('\n===== Exception in realtime_opengl_fixed =====\n')
            ef.write(traceback.format_exc())
        raise
    finally:
        try:
            monitor.stop()
        except Exception:
            pass
        glfw.terminate()


if __name__ == '__main__':
    main()
