// visualizer.js
const userVideo = document.getElementById('userVideo');
const terminal = document.getElementById('terminal');
const analysisList = document.getElementById('analysisList');
const analysisStatus = document.getElementById('analysisStatus');
const runStatus = document.getElementById('runStatus');
const topicValue = document.getElementById('topicValue');
const subjectValue = document.getElementById('subjectValue');
const footerStatus = document.getElementById('footerStatus');
const elapsedTimer = document.getElementById('elapsedTimer');

const ctx = document.getElementById('entityCanvas').getContext('2d');
const DPR = window.devicePixelRatio || 1;

let isAiSpeaking = false;

// Load Ack Sound
const ackSound = new Audio('/app/content/js/ack.mp3');
ackSound.load(); 

ackSound.addEventListener('ended', () => {
    isAiSpeaking = false;
	inputOn();
});

function ack() {
    ackSound.currentTime = 0; 
    ackSound.play();
}

// Load End Sound
const endSound = new Audio('/app/content/js/synthesis.mp3');
endSound.load(); 

function synthesis() {

	return new Promise((resolve) => {
	        endSound.currentTime = 0;
	        endSound.play();
	        
	        // This event fires exactly when the audio reaches the end
	        endSound.onended = () => {
	       		if(resolve) {
					resolve();
				}
	        };
	    });
}

// --- GLITCH CONFIGURATION ---
const glitchState = {
  isActive: false,
  nextGlitchTime: 0,
  glitchDuration: 0,
  totalAiTalkingTime: 0,
  lastCheckTime: 0
};

const state = {
  mode: 'initialising',
  speaking: false,
  analysis: false,
  level: 0.25,
  targetLevel: 0.25,
  subject: '<Awaiting Input>',
  topic: '<Awaiting Input>',
  startedAt: Date.now(),
  systemLines: [
    '[BOOT] SYSTEM QUERY INITIALISING...',
    '[LOAD] SERIES: THE ALGORITHM ASKS',
    '[WAIT] INTERVIEW CONTEXT PENDING'
  ]
};

function setMode(mode, statusText) {
  state.mode = mode;
  const display = statusText || mode.toUpperCase();
  runStatus.textContent = display;
  footerStatus.textContent = display;
}

function writeTerminal(lines) {
  terminal.textContent = Array.isArray(lines) ? lines.join('\n') : String(lines);
  terminal.scrollTop = terminal.scrollHeight;
}

function pulseSpeak(ms = 1600) {
  state.speaking = true;
  state.targetLevel = 0.92;
  window.setTimeout(() => {
    state.speaking = false;
    state.targetLevel = 0.24;
  }, ms);
}

function online() {
	setMode('ready', 'QRY ONLINE');
	document.querySelectorAll('.dot-offline').forEach(el => {
	    el.classList.remove('dot-offline');
	    el.classList.add('dot-online');
	  });
}

function inputOn() {
	el = document.getElementById('input');
	el.classList.remove('led-off');
	el.classList.add('led-on');
}

function offline() {
	setMode('ready', 'QRY OFFLINE');
	document.querySelectorAll('.dot-online').forEach(el => {
	    el.classList.remove('dot-online');
	    el.classList.add('dot-offline');
	  });
}

function inputOff() {
	el = document.getElementById('input');
	el.classList.remove('led-on');
	el.classList.add('led-off');
}

function bootSequence() {
  document.querySelectorAll('.audioDisabled').forEach(el => {
    el.classList.remove('audioDisabled');
  });
  
  document.querySelectorAll('.uninitialised').forEach(el => {
    el.classList.remove('uninitialised');
    el.classList.add('audioDisabled');
  });
  
  online();
  
  state.systemLines = [
    '[BOOT] SYSTEM QUERY INITIALISING...',
    '[LOAD] SERIES: THE ALGORITHM ASKS',
    '[OK] SESSION PROTOCOL LOADED',
    '[OK] QRY ONLINE'
  ];
  state.startedAt = Date.now();

  writeTerminal(state.systemLines);
  setMode('ready', 'QRY ONLINE');
  pulseSpeak(2400);
}

function resizeCanvas() {
  const canvas = ctx.canvas;
  const rect = canvas.getBoundingClientRect();
  canvas.width = Math.round(rect.width * DPR);
  canvas.height = Math.round(rect.height * DPR);
  ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
}

function setSubject(subject) {
  state.subject = subject;
  subjectValue.textContent = subject;
}

function setTopic(topic) {
  state.topic = topic;
  topicValue.textContent = topic;
}

function setAnalysisStatus(text) {
  analysisStatus.textContent = text;
}

function appendSystemLine(line) {
  state.systemLines.push(line);
  writeTerminal(state.systemLines);
}

function setSpeaking(active, intensity = null) {
  state.speaking = active;
  if (typeof intensity === 'number') {
    state.targetLevel = Math.max(0, Math.min(1, intensity));
  }
}

function setAnalysisActive(active) {
  state.analysis = active;
}

function updateTimer() {
  const elapsedMs = Date.now() - state.startedAt;
  const totalSeconds = Math.floor(elapsedMs / 1000);
  const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0');
  const seconds = String(totalSeconds % 60).padStart(2, '0');
  elapsedTimer.textContent = `${minutes}:${seconds}`;
  requestAnimationFrame(updateTimer);
}

function drawEntity(t) {
  const canvas = ctx.canvas;
  const w = canvas.width / DPR;
  const h = canvas.height / DPR;
  ctx.clearRect(0, 0, w, h);

  state.level += (state.targetLevel - state.level) * 0.08;
  const cx = w / 2;
  const cy = h / 2;
  const time = t * 0.001;

  const grd = ctx.createRadialGradient(cx, cy, 10, cx, cy, w * 0.21);
  grd.addColorStop(0, 'rgba(156,242,255,0.24)');
  grd.addColorStop(0.48, 'rgba(102,230,255,0.09)');
  grd.addColorStop(1, 'rgba(102,230,255,0)');
  ctx.fillStyle = grd;
  ctx.beginPath();
  ctx.arc(cx, cy, w * 0.19 + state.level * 15, 0, Math.PI * 2);
  ctx.fill();

  for (let r = 1; r <= 4; r++) {
    ctx.strokeStyle = `rgba(102,230,255,${0.12 - r * 0.015})`;
    ctx.lineWidth = 1.2;
    ctx.beginPath();
    ctx.arc(cx, cy, w * (0.08 + r * 0.045) + Math.sin(time * (0.8 + r * 0.18)) * 2, 0, Math.PI * 2);
    ctx.stroke();
  }

  ctx.save();
  ctx.translate(cx, cy);

  ctx.strokeStyle = 'rgba(156,242,255,0.98)';
  ctx.lineWidth = 2.2;
  ctx.beginPath();
  const points = 180;
  for (let i = 0; i <= points; i++) {
    const a = (i / points) * Math.PI * 2;
    const mod = state.analysis ? 1.5 : 1;
    const radius = w * 0.11 + Math.sin(a * 4 + time * 2.2) * 9 * state.level * mod + Math.cos(a * 9 - time * 1.7) * 3;
    const x = Math.cos(a) * radius;
    const y = Math.sin(a) * radius;
    if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
  }
  ctx.closePath();
  ctx.stroke();

  ctx.strokeStyle = 'rgba(102,230,255,0.92)';
  ctx.lineWidth = 2.8;
  ctx.beginPath();
  const waveW = w * 0.5;
  for (let i = 0; i <= 160; i++) {
    const x = -waveW / 2 + (i / 160) * waveW;
    const y = Math.sin(i * 0.22 + time * 8.2) * 13 * state.level * (state.speaking ? 1.5 : 0.65)
            + Math.cos(i * 0.08 - time * 2.6) * 4;
    if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
  }
  ctx.stroke();

  if (state.analysis) {
    ctx.strokeStyle = 'rgba(255,100,135,0.82)';
    ctx.lineWidth = 1.6;
    ctx.beginPath();
    ctx.arc(0, 0, w * 0.16 + Math.sin(time * 6) * 4, 0, Math.PI * 2);
    ctx.stroke();
  }

  ctx.restore();
  requestAnimationFrame(drawEntity);
}

function driveVisualiserFromAnalyser() {
  if (!analyser) {
    requestAnimationFrame(driveVisualiserFromAnalyser);
    return;
  }

  if (!dataArray || dataArray.length !== analyser.frequencyBinCount) {
    dataArray = new Uint8Array(analyser.frequencyBinCount);
  }

  analyser.getByteFrequencyData(dataArray);

  let sum = 0;
  for (let i = 0; i < dataArray.length; i++) {
    sum += dataArray[i];
  }

  const avg = sum / dataArray.length / 255;

  state.speaking = avg > 0.02;
  state.targetLevel = Math.max(0.18, Math.min(1, avg * 1.8));

  // --- GLITCH MONITORING LOGIC ---
  // We monitor the talking time to trigger glitches
  if (state.speaking) {
    const now = performance.now();
    if (glitchState.lastCheckTime > 0) {
      const delta = (now - glitchState.lastCheckTime) / 1000;
      glitchState.totalAiTalkingTime += delta;
      
      // Check if it's time for a glitch
      if (!glitchState.isActive && glitchState.totalAiTalkingTime >= glitchState.nextGlitchTime) {
        triggerGlitch();
      }
      
      // Check if glitch duration is over
      if (glitchState.isActive && (now / 1000) > glitchState.glitchDuration) {
        stopGlitch();
      }
    }
    glitchState.lastCheckTime = now;
  } else {
    glitchState.lastCheckTime = 0;
  }

  requestAnimationFrame(driveVisualiserFromAnalyser);
}

/**
 * Trigger a random glitch event with variable intensity/duration
 */
function triggerGlitch() {
  glitchState.isActive = true;
  
  // RANDOM DURATION: Between a quick 100ms pop and a long 700ms failure
  const durationMs = 100 + Math.random() * 300;
  glitchState.glitchDuration = (performance.now() / 1000) + (durationMs / 1000);
  
  // Visual Feedback: Flash the analysis ring red if active
  const originalAnalysis = state.analysis;
  state.analysis = true;

  // AUDIO CROSSFADE: Swap clean for crushed immediately
  if (cleanGain && crusherGain) {
    const now = audioContext.currentTime;
    cleanGain.gain.setTargetAtTime(0, now, 0.01);
    crusherGain.gain.setTargetAtTime(0.8, now, 0.01);
  }

  appendSystemLine(`[ERR] SIGNAL INSTABILITY: ${Math.round(durationMs)}ms`);
  
  // Optional: Reset analysis ring visual after the glitch
  setTimeout(() => { state.analysis = originalAnalysis; }, durationMs);
}

function stopGlitch() {
  glitchState.isActive = false;
  
  // RESET: Start the timer over and pick a new random gap (1-10 seconds)
  glitchState.totalAiTalkingTime = 0;
  glitchState.nextGlitchTime = 1 + Math.random() * 9;
  
  if (cleanGain && crusherGain) {
    const now = audioContext.currentTime;
    cleanGain.gain.setTargetAtTime(1, now, 0.01);
    crusherGain.gain.setTargetAtTime(0, now, 0.01);
  }
}

window.addEventListener('resize', resizeCanvas);
resizeCanvas();


let audioContext;
let analyser;
let dataArray;
let bitCrusher;
let cleanGain;
let crusherGain;

let lastAudio = Date.now();
let nextStartTime = 0; 
const BUFFER_LOOKAHEAD = 0.1;

async function initAI() {
    audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
    
    try {
		
		document.querySelectorAll('.initialTitle > div:nth-child(1)').forEach(el => {
           el.innerHTML = "Initialising audio-visual pipeline...";	
       });
            
        await audioContext.audioWorklet.addModule('/app/content/js/pcm-processor.js');
        
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
        userVideo.srcObject = stream;

        // --- USER MICROPHONE PIPELINE ---
        const source = audioContext.createMediaStreamSource(stream);
        const pcmWorker = new AudioWorkletNode(audioContext, 'pcm-processor');
        pcmWorker.port.onmessage = (event) => {
            if(window.socket && window.socket.readyState === WebSocket.OPEN && !isAiSpeaking) {
                window.socket.send(event.data); 
            }
        };
        source.connect(pcmWorker);

        // --- AI OUTPUT GLITCH PIPELINE ---
        // Create BitCrusher Node
        bitCrusher = audioContext.createScriptProcessor(4096, 1, 1);
        const bits = 4; // Lower = crunchier. 4 is a nice "slight" distortion.
        const norm = Math.pow(2, bits);
        bitCrusher.onaudioprocess = (e) => {
            let input = e.inputBuffer.getChannelData(0);
            let output = e.outputBuffer.getChannelData(0);
            for (let i = 0; i < input.length; i++) {
                output[i] = Math.round(input[i] * norm) / norm;
            }
        };

        // Create Mix Nodes (Clean vs Glitchy)
        cleanGain = audioContext.createGain();
        crusherGain = audioContext.createGain();
        cleanGain.gain.value = 1; // Default to clean
        crusherGain.gain.value = 0; // Start with no glitch

        // Initialize the random glitch gate
        glitchState.nextGlitchTime = 1 + Math.random() * 9;

        // Routing for AI Voice:
        // Input -> [cleanGain] -> Analyser -> Destination
        // Input -> [bitCrusher] -> [crusherGain] -> Analyser -> Destination
        bitCrusher.connect(crusherGain);

        analyser = audioContext.createAnalyser();
        analyser.fftSize = 256;
        analyser.smoothingTimeConstant = 0.82;
        dataArray = new Uint8Array(analyser.frequencyBinCount);

        cleanGain.connect(analyser);
        crusherGain.connect(analyser);
        analyser.connect(audioContext.destination);
        
        driveVisualiserFromAnalyser();

		synthesis();
    } catch (err) {
        console.error("Initialization failed:", err);
    }
}

function handleJsonControl(msg) {
    if (msg.type === "TOPIC") {
        setTopic(msg.text);
        bootSequence();
        requestAnimationFrame(drawEntity);
        requestAnimationFrame(updateTimer);
    } else if(msg.type === "SUBJECT") {
        setSubject(msg.text);
    } else if(msg.type === "SOUND") {
		if(msg.text === "ACKNOWLEDGMENT") ack();
		if(msg.text === "SYTHNESIS") synthesis();
    } 	else if(msg.type === "LOG") {
		appendSystemLine(`[LOG] ${msg.text}`);
    } else if(msg.type === "DONE") {
		appendSystemLine(`[LOG] Interview complete.`);
		appendSystemLine(``);
		offline();
		inputOff();
    } 
}

window.onclick = () => { 
    if(!audioContext) {
        initAI(); 
        
        window.socket = new WebSocket('wss://' 
            + window.location.hostname 
            + ':' + window.location.port 
            + document.body.getAttribute('data-url')
        );

        window.socket.binaryType = 'arraybuffer'; 
        
        window.socket.onmessage = async (event) => {
            if (event.data instanceof ArrayBuffer) {
                if (audioContext.state === 'suspended') await audioContext.resume();

                const data = new Uint8Array(event.data);
                const type = data[0];
                const payload = data.slice(1);

                if (type === 0) { // AUDIO DATA
					
					isAiSpeaking = true;
					inputOff();
					
                    lastAudio = Date.now();
                    
                    const int16Data = new Int16Array(payload.buffer, payload.byteOffset, payload.byteLength / 2);
                    const audioBuffer = audioContext.createBuffer(1, int16Data.length, 24000);
                    const float32Data = audioBuffer.getChannelData(0);
                    for (let i = 0; i < int16Data.length; i++) float32Data[i] = int16Data[i] / 32768.0;

                    const source = audioContext.createBufferSource();
                    source.buffer = audioBuffer;

                    // --- THE GLITCH ROUTING ---
                    // Source connects to BOTH paths. Gain nodes control which path is audible.
                    source.connect(cleanGain);
                    source.connect(bitCrusher);

                    const currentTime = audioContext.currentTime;
                    if (nextStartTime < currentTime) nextStartTime = currentTime + 0.1;

                    source.start(nextStartTime);
                    nextStartTime += audioBuffer.duration;
                } 
                else if (type === 1) { // TURN COMPLETE
                    const delayUntilDone = (nextStartTime - audioContext.currentTime) * 1000;
                    setTimeout(() => {
                        ack();
                    }, Math.max(0, delayUntilDone));
                }
            } else {
                handleJsonControl(JSON.parse(event.data));
            }
        };
    }
};