import numpy as np

class SeerOracle:
    def __init__(self, accuracy=0.95, overhead_factor=0.0):
        """
        Simula um Oráculo Neural (Speculative Execution).
        accuracy: Porcentagem de precisão em identificar blocos inativos (False Positive Rate control).
                  Assumimos 100% de Recall (nunca perde dados ativos) para garantir corretude.
                  accuracy=0.9 -> 10% dos blocos frios são incorretamente marcados como quentes (overhead).
        overhead_factor: Custo computacional simulado do preditor (s).
        """
        self.accuracy = accuracy
        self.overhead_factor = overhead_factor

    def predict(self, Matrix, block_size):
        """
        Retorna a máscara de blocos previstos como ativos.
        """
        n_blocks = Matrix.shape[0] // block_size

        # 1. Ground Truth (Oracle Perfeito - lento na prática, aqui usado como base)
        # Na vida real, o Seer usaria apenas inputs parciais ou históricos.
        view = Matrix.reshape(n_blocks, block_size, n_blocks, block_size)
        block_sums = view.sum(axis=(1, 3))
        ground_truth = (block_sums > 0)

        # 2. Simulação de Predição
        # O Seer nunca deve gerar False Negatives (perder dados), então mantemos todos os True.
        # Mas ele gera False Positives (processa lixo).
        prediction = ground_truth.copy()

        # Adicionar Ruído (False Positives)
        # Onde é Zero (Frio), temos (1-accuracy) chance de marcar como Um (Quente).
        zeros_mask = ~ground_truth
        noise = np.random.rand(n_blocks, n_blocks) > self.accuracy

        # Falso Positivo = Era Zero E Ruído marcou.
        false_positives = zeros_mask & noise

        prediction = prediction | false_positives

        return prediction

from numba import njit, prange

@njit(parallel=True, fastmath=True)
def _sampling_check(Matrix, n_blocks, block_size, samples_per_block):
    """
    Kernel JIT para amostragem rápida.
    Verifica 'samples_per_block' pixels aleatórios em cada bloco.
    Se encontrar != 0, marca como ativo.
    """
    mask = np.zeros((n_blocks, n_blocks), dtype=np.bool_)

    # Pre-compute random offsets?
    # Generating random numbers inside parallel loop can be tricky/slow.
    # Instead, we use a deterministic stride or pseudo-random check.
    # Simple strategy: Check fixed positions or stride.
    # Better: Use a Linear Congruential Generator (LCG) simple implementation or just fixed strides.

    # Let's check a few fixed points and some strides.
    # To be "random" but fast:
    step = (block_size * block_size) // samples_per_block
    if step < 1: step = 1

    for bi in prange(n_blocks):
        i_start = bi * block_size
        for bk in range(n_blocks):
            k_start = bk * block_size

            # Check sampling points
            is_active = False
            # Linear probe through the block
            count = 0
            for r in range(0, block_size):
                if is_active: break
                for c in range(0, block_size):
                    # We want to visit only 'samples_per_block' pixels.
                    # We can pick pixels where (r * block_size + c) % step == 0
                    linear_idx = r * block_size + c
                    if linear_idx % step == 0:
                        val = Matrix[i_start + r, k_start + c]
                        if val != 0:
                            is_active = True
                            break
                        count += 1
                        if count >= samples_per_block:
                            break
                if is_active: break

            mask[bi, bk] = is_active

    return mask

class SamplingOracle:
    def __init__(self, sample_rate=0.05):
        """
        Oráculo Estatístico (Amostragem).
        sample_rate: Fração dos pixels do bloco a serem verificados (0.05 = 5%).
        """
        self.sample_rate = sample_rate

    def predict(self, Matrix, block_size):
        n_blocks = Matrix.shape[0] // block_size
        total_pixels = block_size ** 2
        samples = int(total_pixels * self.sample_rate)
        if samples < 1: samples = 1

        return _sampling_check(Matrix, n_blocks, block_size, samples)
