import numpy as np

# ============================================================
# CRYSTAL FILTER CLASS
# ============================================================

class CrystalFilter:
    """One of 10 crystal filters for processing observations."""
    
    def __init__(self, crystal_id, crystal_type):
        self.crystal_id = crystal_id
        self.crystal_type = crystal_type
        self.weights = np.random.rand(10)  # Internal weights
        self.output_history = []
        self.checksum_value = 0.5
    
    def process(self, system_input, theta, intelligence):
        """Process system input through this crystal filter."""
        # Scale input by alignment and intelligence
        scaling = (1 - intelligence) * (theta / np.pi)
        
        # Apply crystal-specific transformation
        if self.crystal_type == "Cubic":
            output = self._cubic_transform(system_input, scaling)
        elif self.crystal_type == "Hexagonal":
            output = self._hexagonal_transform(system_input, scaling)
        elif self.crystal_type == "Tetrahedral":
            output = self._tetrahedral_transform(system_input, scaling)
        elif self.crystal_type == "Quasicrystal":
            output = self._quasicrystal_transform(system_input, scaling)
        elif self.crystal_type == "Graphene":
            output = self._graphene_transform(system_input, scaling)
        elif self.crystal_type == "BCC":
            output = self._bcc_transform(system_input, scaling)
        elif self.crystal_type == "FCC":
            output = self._fcc_transform(system_input, scaling)
        elif self.crystal_type == "Perovskite":
            output = self._perovskite_transform(system_input, scaling)
        elif self.crystal_type == "Cayley":
            output = self._cayley_transform(system_input, scaling)
        elif self.crystal_type == "Fractal":
            output = self._fractal_transform(system_input, scaling)
        else:
            output = system_input * scaling
        
        self.output_history.append(output)
        return output
    
    def _cubic_transform(self, x, s):
        """Grid-based hashing - systematic analysis."""
        return np.floor(x * 10) / 10 * (1 - s)
    
    def _hexagonal_transform(self, x, s):
        """Voronoi clustering - local grouping."""
        return np.sin(x * 6 + s) * np.cos(x * 6 - s)
    
    def _tetrahedral_transform(self, x, s):
        """Symmetry detection - rotational checks."""
        return (x**3 - x) * (1 - s)
    
    def _quasicrystal_transform(self, x, s):
        """Pattern recognition - aperiodic structures."""
        return np.sin(x * 8.5) * np.cos(x * 5.2) * np.exp(-s)
    
    def _graphene_transform(self, x, s):
        """Sequential logic - edge traversal."""
        return np.cumsum(np.sin(x + s))
    
    def _bcc_transform(self, x, s):
        """Hierarchical reasoning - tree structures."""
        return np.sort(np.abs(x)) * (1 + s)
    
    def _fcc_transform(self, x, s):
        """Mirror validation - inverse paths."""
        return (x + np.flip(x)) / 2 * np.exp(-s)
    
    def _perovskite_transform(self, x, s):
        """Domain constraints - specialized filtering."""
        return x * (1 + s) * np.tanh(x)
    
    def _cayley_transform(self, x, s):
        """Symbolic manipulation - algebraic."""
        return x**2 - s * x + s**2
    
    def _fractal_transform(self, x, s):
        """Multi-scale analysis - recursive."""
        output = x
        for _ in range(3):
            output = output * np.sin(output + s)
        return output
    
    def get_checksum(self):
        """Return current checksum value for this crystal."""
        if len(self.output_history) > 0:
            self.checksum_value = np.mean(self.output_history)
        return self.checksum_value
    
    def reset(self):
        """Reset the crystal filter."""
        self.output_history = []
        self.checksum_value = 0.5


# ============================================================
# OBSERVER VARIABLE CLASS
# ============================================================

class ObserverVariable:
    """
    Complete Observer Variable for Measurement Mathematics.
    
    Combines:
    - 10 crystal filters (Cubic, Hexagonal, Tetrahedral, Quasicrystal, Graphene,
      BCC, FCC, Perovskite, Cayley, Fractal)
    - π/e checksum anchoring
    - Will strength dynamics
    - Intelligence tracking
    - Collapse rate computation
    """
    
    def __init__(self, n_crystals=10):
        # Crystal types in order
        crystal_types = [
            "Cubic", "Hexagonal", "Tetrahedral", "Quasicrystal", "Graphene",
            "BCC", "FCC", "Perovskite", "Cayley", "Fractal"
        ]
        
        # === Stationary components (fixed at init) ===
        self.Omega_0 = 1.0           # Base will strength
        self.I_0 = 0.5               # Base intelligence (0-1)
        self.theta_life = 0.1        # Life zone threshold
        self.n_crystals = min(n_crystals, len(crystal_types))
        
        # === Probabilistic components (variable state) ===
        self.Omega = self.Omega_0    # Current will strength
        self.I = self.I_0            # Current intelligence
        self.H = 1.0                 # Current semantic entropy
        self.theta = np.pi / 2       # Alignment angle (π/2 = neutral)
        
        # === Crystal filter bank ===
        self.crystals = [
            CrystalFilter(i, crystal_types[i % len(crystal_types)])
            for i in range(self.n_crystals)
        ]
        self.weights = np.ones(self.n_crystals) / self.n_crystals
        
        # === π/e checksum baselines ===
        self.C_pi_baseline = 0.5
        self.C_e_baseline = 0.5
        self.observation_history = []
        
        # === Exhaustion parameters ===
        self.epsilon = 0.01          # Exhaustion rate
        self.energy_spent = 0.0      # Accumulated energy expenditure
        
        # === Collapse parameters ===
        self.Delta_max = 1.0
        self.alpha_base = 1.0
        
        # === Performance tracking ===
        self.success_count = 0
        self.total_measurements = 0
    
    # --------------------------------------------------------
    # Observer Function (Crystal Filter Bank)
    # --------------------------------------------------------
    def compute_observer_function(self, system_input):
        """Compute observer function via crystal filter bank."""
        outputs = []
        for i, crystal in enumerate(self.crystals):
            phi = crystal.process(system_input, self.theta, self.I)
            outputs.append(phi * self.weights[i])
        
        result = np.sum(outputs, axis=0)
        self.observation_history.append(result)
        return result
    
    # --------------------------------------------------------
    # π/e Checksum Computation
    # --------------------------------------------------------
    def compute_pi_checksum(self, F_o, T=10.0):
        """C_π(O) = ∫ F_o(t) · cos(πt) dt"""
        t = np.linspace(0, T, 1000)
        F_o_array = np.atleast_1d(F_o)
        
        # Create function from scalar or array
        if len(F_o_array) == 1:
            integrand = F_o_array[0] * np.cos(np.pi * t / T)
        else:
            # Interpolate to match time points
            x_pts = np.linspace(0, 1, len(F_o_array))
            f_interp = np.interp(np.linspace(0, 1, 1000), x_pts, F_o_array)
            integrand = f_interp * np.cos(np.pi * t / T)
        
        return np.trapezoid(integrand, t)
    
    def compute_e_checksum(self, F_o, T=10.0):
        """C_e(O) = ∫ F_o(t) · exp(-e·t) dt"""
        t = np.linspace(0, T, 1000)
        F_o_array = np.atleast_1d(F_o)
        
        if len(F_o_array) == 1:
            integrand = F_o_array[0] * np.exp(-np.e * t / T)
        else:
            x_pts = np.linspace(0, 1, len(F_o_array))
            f_interp = np.interp(np.linspace(0, 1, 1000), x_pts, F_o_array)
            integrand = f_interp * np.exp(-np.e * t / T)
        
        return np.trapezoid(integrand, t)
    
    # --------------------------------------------------------
    # Collapse Potential and Rate
    # --------------------------------------------------------
    def compute_collapse_potential(self):
        """
        Δ_c(θ, I) = Δ_max · sin²(θ/2) · (1 - I)
        
        Measures how much collapse the observer triggers.
        High intelligence + aligned → low collapse potential
        Low intelligence + misaligned → high collapse potential
        """
        return self.Delta_max * np.sin(self.theta / 2)**2 * (1 - self.I)
    
    def compute_crystal_coherence(self):
        """Product of coherence factors from all crystal filters."""
        coherence_prod = 1.0
        for crystal in self.crystals:
            C_pi_i = crystal.get_checksum()
            coherence_factor = np.exp(-0.1 * abs(C_pi_i - self.C_pi_baseline))
            coherence_prod *= coherence_factor
        return coherence_prod
    
    def compute_collapse_rate(self):
        """
        α_measure = α_base · Δ_c · (Ω/(Ω+ε)) · Πφ_i
        
        Rate at which observer causes collapse.
        """
        Delta_c = self.compute_collapse_potential()
        will_util = self.Omega / (self.Omega + self.epsilon)
        coherence = self.compute_crystal_coherence()
        
        return self.alpha_base * Delta_c * will_util * coherence
    
    # --------------------------------------------------------
    # Truth Extraction
    # --------------------------------------------------------
    def compute_understanding_level(self):
        """
        u_i(O) = exp(-β · d_crystal) / Z
        
        How well the observer understands each truth domain.
        """
        beta = 1.0
        distances = []
        
        for i, crystal in enumerate(self.crystals):
            d = abs(crystal.get_checksum() - self.C_pi_baseline)
            distances.append(np.exp(-beta * d))
        
        Z = np.sum(distances) + 1e-10
        return distances / Z
    
    def extract_truth(self, system_input):
        """Extract truth from system using sensor array."""
        understanding = self.compute_understanding_level()
        
        # High understanding (>0.5) → known answer, no collapse
        # Low understanding (<0.5) → collapse required
        
        results = []
        for i, (crystal, u) in enumerate(zip(self.crystals, understanding)):
            if u > 0.5:
                # Known - use original value
                results.append(("known", system_input))
            else:
                # Unknown - collapsed value
                collapsed = self.force_collapse(system_input)
                results.append(("collapsed", collapsed))
        
        return results
    
    def force_collapse(self, state):
        """Force collapse to single deterministic outcome."""
        if isinstance(state, (int, float)):
            return state
        elif hasattr(state, '__len__'):
            return np.mean(np.atleast_1d(state))
        else:
            return state

    def summarize_outcome(self, outcome):
        """Convert an outcome to a scalar summary for evaluation and display."""
        if isinstance(outcome, (int, float, np.integer, np.floating)):
            return float(outcome)
        if hasattr(outcome, '__len__'):
            return float(np.mean(np.atleast_1d(outcome)))
        return float(outcome)
    
    # --------------------------------------------------------
    # Observer State Update
    # --------------------------------------------------------
    def update_will(self, success):
        """dΩ/dt = η·S - ε·E_spent"""
        eta = 0.1
        self.Omega += eta * success - self.epsilon * self.energy_spent
        self.Omega = max(0.1, self.Omega)  # Floor at 0.1
        self.energy_spent += 0.01
    
    def update_crystal_weights(self, divergence_per_crystal):
        """Adapt crystal weights based on divergence."""
        kappa = 0.1
        delta_good = 0.01
        delta_bad = 0.01
        
        for i, div in enumerate(divergence_per_crystal):
            if div < 0.1:  # Good alignment
                self.weights[i] += kappa * delta_good
            else:  # Bad alignment
                self.weights[i] -= kappa * delta_bad
        
        # Normalize
        self.weights = np.maximum(self.weights, 0.01)
        self.weights = self.weights / np.sum(self.weights)
    
    def update_intelligence(self, success):
        """Gradually improve intelligence based on success."""
        if success > 0.8:
            self.I = min(1.0, self.I + 0.01)
        elif success < 0.3:
            self.I = max(0.0, self.I - 0.01)
    
    # --------------------------------------------------------
    # Main Measurement Function
    # --------------------------------------------------------
    def measure(self, system_input, ground_truth=None):
        """
        Perform complete measurement and return results.
        
        Args:
            system_input: The system state to observe
            ground_truth: Optional ground truth for evaluation
        
        Returns:
            Dictionary with measurement results
        """
        self.total_measurements += 1
        
        # Step 1: Compute observer function
        F_o = self.compute_observer_function(system_input)
        
        # Step 2: Compute checksums
        C_pi = self.compute_pi_checksum(F_o)
        C_e = self.compute_e_checksum(F_o)
        
        # Step 3: Compute collapse
        alpha = self.compute_collapse_rate()
        measurement_outcome = self.collapse(system_input, alpha)
        measurement_value = self.summarize_outcome(measurement_outcome)
        
        # Step 4: Compute divergence per crystal
        divergence_per_crystal = []
        for crystal in self.crystals:
            divergence = abs(crystal.get_checksum() - self.C_pi_baseline)
            divergence_per_crystal.append(divergence)
        
        # Step 5: Compute success signal
        if ground_truth is not None:
            if isinstance(ground_truth, (int, float)):
                success = 1.0 if abs(measurement_value - ground_truth) < 0.1 else 0.0
            else:
                ground_truth_value = self.summarize_outcome(ground_truth)
                success = 1.0 if abs(measurement_value - ground_truth_value) < 0.1 else 0.0
        else:
            # Use checksum alignment as proxy
            total_divergence = sum(divergence_per_crystal)
            success = np.exp(-total_divergence / self.n_crystals)
        
        # Step 6: Update observer state
        self.update_will(success)
        self.update_crystal_weights(divergence_per_crystal)
        self.update_intelligence(success)
        
        # Track success
        self.success_count += success
        
        # Step 7: Extract truth
        understanding = self.compute_understanding_level()
        truth_results = self.extract_truth(system_input)
        
        return {
            'outcome': measurement_value,
            'raw_outcome': measurement_outcome,
            'checksums': {
                'C_pi': C_pi,
                'C_e': C_e
            },
            'divergence': sum(divergence_per_crystal) / self.n_crystals,
            'collapse_rate': alpha,
            'collapse_potential': self.compute_collapse_potential(),
            'will_strength': self.Omega,
            'intelligence': self.I,
            'entropy': self.H,
            'alignment_angle': self.theta,
            'crystal_weights': self.weights.copy(),
            'success': success,
            'understanding': understanding,
            'truth_results': truth_results
        }
    
    def collapse(self, system_input, alpha):
        """Collapse system based on computed alpha."""
        if alpha < 0.1:
            # Low alpha: wave pattern preserved (no collapse)
            return system_input
        else:
            # High alpha: particle pattern (forced collapse)
            return self.force_collapse(system_input)
    
    # --------------------------------------------------------
    # Status and Reset
    # --------------------------------------------------------
    def get_status(self):
        """Get current observer status."""
        return {
            'will_strength': self.Omega,
            'intelligence': self.I,
            'entropy': self.H,
            'alignment': self.theta,
            'success_rate': self.success_count / max(1, self.total_measurements),
            'energy_spent': self.energy_spent
        }
    
    def reset(self):
        """Reset observer to initial state."""
        self.Omega = self.Omega_0
        self.I = self.I_0
        self.H = 1.0
        self.theta = np.pi / 2
        self.energy_spent = 0.0
        self.success_count = 0
        self.total_measurements = 0
        self.observation_history = []
        self.weights = np.ones(self.n_crystals) / self.n_crystals
        
        for crystal in self.crystals:
            crystal.reset()
    
    def __repr__(self):
        return f"ObserverVariable(Ω={self.Omega:.3f}, I={self.I:.3f}, crystals={self.n_crystals})"


# ============================================================
# DEMONSTRATION
# ============================================================

if __name__ == "__main__":
    # Create observer
    obs = ObserverVariable()
    print("Created:", obs)
    print("\n" + "="*60)
    print("OBSERVER STATUS")
    print("="*60)
    print(obs.get_status())
    
    # Run several measurements
    print("\n" + "="*60)
    print("RUNNING MEASUREMENTS")
    print("="*60)
    
    #np.random.seed(42)
    
    for i in range(5):
        # Simulate system input
        system_input = np.random.rand(10) * 10
        ground_truth = np.mean(system_input)
        
        result = obs.measure(system_input, ground_truth)
        
        print(f"\n--- Measurement {i+1} ---")
        print(f"  Outcome: {result['outcome']:.4f}")
        print(f"  Collapse Rate: {result['collapse_rate']:.4f}")
        print(f"  Will Strength: {result['will_strength']:.4f}")
        print(f"  Intelligence: {result['intelligence']:.4f}")
        print(f"  Success: {result['success']:.4f}")
    
    print("\n" + "="*60)
    print("FINAL STATUS")
    print("="*60)
    status = obs.get_status()
    for key, value in status.items():
        print(f"  {key}: {value:.4f}")
    
    print("\n" + "="*60)
    print("CRYSTAL WEIGHTS")
    print("="*60)
    crystal_names = ["Cubic", "Hexagonal", "Tetrahedral", "Quasicrystal", 
                     "Graphene", "BCC", "FCC", "Perovskite", "Cayley", "Fractal"]
    for i, (name, w) in enumerate(zip(crystal_names[:obs.n_crystals], obs.weights)):
        print(f"  {name:12s}: {w:.4f}")
