import os
import numpy as np
from math import log

class AcousticCollapseTheoryV6:
    def __init__(self, num_frequencies=4096, absorption_threshold=0.60, max_ode_steps=8):
        self.NUM_FREQS = num_frequencies  
        self.ABSORPTION_THRESHOLD = absorption_threshold # % of input energy that must hit the target
        self.MAX_ODE_STEPS = max_ode_steps
        self.kb = []
        
        self.idf_weights = np.ones(self.NUM_FREQS) 
        self.total_docs = 0
        
        print(f"[ACT v6] Initialized Laser-Filter Energy Cortex ({self.NUM_FREQS} bins).")

    def _text_to_freq(self, bigram):
        hash_val = 5381
        for char in bigram.lower():
            hash_val = ((hash_val << 5) + hash_val) + ord(char)
        return hash_val % self.NUM_FREQS

    def encode_to_power_spectrum(self, text, apply_idf=False):
        """
        Encodes text into a 1D Power Spectrum (Real numbers only).
        No phase, no complex numbers. Pure acoustic energy at specific frequencies.
        """
        spectrum = np.zeros(self.NUM_FREQS, dtype=np.float64)
        words = text.lower().split()
        if len(words) < 2: return spectrum
        
        bigrams = [f"{words[i]} {words[i+1]}" for i in range(len(words) - 1)]
        
        for bigram in bigrams:
            freq = self._text_to_freq(bigram)
            # Energy is additive. Rare words (IDF) add more energy (louder).
            amplitude = self.idf_weights[freq] if apply_idf else 1.0
            spectrum[freq] += amplitude
            
        return spectrum

    def apply_ode_reverberation(self, spectrum, time_step):
        """
        ODE Simulation: Physical Acoustic Reverberation.
        Energy leaks to neighboring bins, then decays. 
        True semantic matches form a stable resonant peak. 
        Random hash collisions dissipate into heat.
        """
        new_spectrum = np.zeros_like(spectrum)
        decay_rate = 0.8 # Energy loss per time step
        
        for i in range(self.NUM_FREQS):
            if spectrum[i] > 0:
                energy = spectrum[i] * decay_rate
                # Leak energy to adjacent bins (simulating physical resonance)
                new_spectrum[i] += energy * 0.6       # Keep 60% in center
                if i > 0: new_spectrum[i-1] += energy * 0.2  # Leak left
                if i < self.NUM_FREQS - 1: new_spectrum[i+1] += energy * 0.2  # Leak right
                
        return new_spectrum

    def calculate_energy_stability(self, spec1, spec2):
        """Checks if the energy distribution stopped changing."""
        norm1 = np.linalg.norm(spec1)
        norm2 = np.linalg.norm(spec2)
        if norm1 == 0: return 1.0
        return np.dot(spec1, spec2) / (norm1 * norm2)

    def energy_absorption_score(self, input_spectrum, kb_spectrum):
        """
        LASER-FILTER METRIC:
        Returns (Energy Transferred) / (Total Input Energy).
        1.0 = 100% of the laser passed through. 
        0.1 = Only noise collided.
        """
        # Energy that successfully found a home in the KB
        transferred_energy = np.sum(input_spectrum * kb_spectrum)
        
        # Total energy fired by the laser
        total_input_energy = np.sum(input_spectrum)
        
        if total_input_energy == 0: return 0.0
        return transferred_energy / total_input_energy

    def train(self, books_dir="books"):
        if not os.path.exists(books_dir):
            print(f"[ERROR] '{books_dir}' not found."); return

        print(f"[TRAIN] Scanning '{books_dir}'...")
        raw_sentences = []
        
        # Pass 1: Calculate IDF (Q-Factor weighting)
        for filename in os.listdir(books_dir):
            if filename.endswith(".txt") or filename.endswith(".md"):
                with open(os.path.join(books_dir, filename), 'r', encoding='utf-8', errors='ignore') as f:
                    lines = [line.strip() for line in f if len(line.strip()) > 15]
                    raw_sentences.extend(lines)
                    
                    for line in lines:
                        words = line.lower().split()
                        bigrams = set(f"{words[i]} {words[i+1]}" for i in range(len(words) - 1))
                        for bigram in bigrams:
                            freq = self._text_to_freq(bigram)
                            self.idf_weights[freq] += 1

        self.total_docs = len(raw_sentences)
        
        # Inverse Document Frequency: Rare = High Energy, Common = Low Energy
        for i in range(self.NUM_FREQS):
            if self.idf_weights[i] > 0:
                self.idf_weights[i] = log(self.total_docs / self.idf_weights[i]) + 1.0
            else:
                self.idf_weights[i] = 0.0
                
        # Pass 2: Build KB as pure 1D Power Spectrums
        for i in range(len(raw_sentences) - 1):
            trigger_spec = self.encode_to_power_spectrum(raw_sentences[i], apply_idf=True)
            response_spec = self.encode_to_power_spectrum(raw_sentences[i+1], apply_idf=True)
            self.kb.append((trigger_spec, response_spec, raw_sentences[i+1]))
            
        print(f"[TRAIN] Complete. {len(self.kb)} energy profiles built.\n")

    def query(self, prompt_text):
        if not self.kb: return "Knowledge base empty."

        print(f"Input:  {prompt_text}")
        current_spectrum = self.encode_to_power_spectrum(prompt_text, apply_idf=True)
        prev_spectrum = np.zeros_like(current_spectrum)
        
        # ODE Loop: Let the physical reverberation stabilize
        for t in range(1, self.MAX_ODE_STEPS + 1):
            stability = self.calculate_energy_stability(current_spectrum, prev_spectrum)
            change = 1.0 - stability
            
            if change < 0.02 and t > 1:
                print(f"[ODE-CCT] REVERBERATION SETTLED at t={t}.")
                break
                
            prev_spectrum = current_spectrum.copy()
            current_spectrum = self.apply_ode_reverberation(current_spectrum, t)
            print(f"[ODE-CCT] t={t}: Resonating... (Energy shift: {change:.4f})")

        best_score = 0.0
        best_response = "... (Energy Dissipated - No Match)"

        for trigger_spec, response_spec, response_text in self.kb:
            score = self.energy_absorption_score(current_spectrum, trigger_spec)
            if score > best_score:
                best_score = score
                best_response = response_text

        # Gate based on pure energy transfer percentage
        if best_score < self.ABSORPTION_THRESHOLD:
            print(f"[RESULT] Energy Absorption: {best_score*100:.1f}% (Below Gate {self.ABSORPTION_THRESHOLD*100:.0f}%)")
            print(f"Output:  [REJECTED - Energy Dissipated]\n" + "-"*60)
            return "[REJECTED]"
        else:
            print(f"[RESULT] Energy Absorption: {best_score*100:.1f}% (Resonance Achieved)")
            print(f"Output:  {best_response}\n" + "-"*60)
            return best_response

if __name__ == "__main__":
    # 60% of the input's energy MUST hit the target to be considered a match
    ai = AcousticCollapseTheoryV6(num_frequencies=4096, absorption_threshold=0.60, max_ode_steps=8)
    ai.train("books")
    
    print("=== STARTING EVALUATION ===")
    test_prompts = [
        "What is the main concept?",
        "Explain the theory of relativity",
        "The quantum state collapses",
        "Poirot looked at the murder scene", 
        "Random words apple bicycle gravity"
    ]
    
    for prompt in test_prompts:
        ai.query(prompt)
