"""Deterministic signal transformations; no state or probability register."""

import cmath
from dataclasses import dataclass
import math


def _algebra_simbolica():
    try:
        import sympy
    except ModuleNotFoundError as error:
        raise ModuleNotFoundError(
            'Las relaciones simbolicas necesitan SymPy. Instala con: '
            'python -m pip install -r requirements_relaciones.txt') from error
    return sympy


@dataclass(frozen=True)
class Bloque:
    nombre: str
    puertos: tuple
    angulo: float | None = None

    def transformar_par(self, s0, s1, *, simbolico=False):
        """Apply the formula to signals or formal variables, never a readout."""
        algebra = _algebra_simbolica() if simbolico else None
        imaginary = algebra.I if simbolico else 1j
        exponential = algebra.exp if simbolico else cmath.exp
        if self.nombre in ('fase', 'rx', 'rz') and self.angulo is None:
            raise ValueError('Falta el angulo de la operacion')
        if self.nombre == 'h':
            scale = algebra.sqrt(2) / 2 if simbolico else math.sqrt(.5)
            return (s0 + s1) * scale, (s0 - s1) * scale
        if self.nombre == 'fase':
            return s0, exponential(imaginary * self.angulo) * s1
        if self.nombre == 'rx':
            cosine, sine = (algebra.cos, algebra.sin) if simbolico else (math.cos, math.sin)
            c, s = cosine(self.angulo / 2), sine(self.angulo / 2)
            return c*s0 - imaginary*s*s1, -imaginary*s*s0 + c*s1
        if self.nombre == 'rz':
            return exponential(-imaginary*self.angulo/2)*s0, exponential(imaginary*self.angulo/2)*s1
        raise ValueError('Este bloque no es una transformacion de dos caminos')

    def transformar_conexion_doble(self, s00, s01, s10, s11, *, simbolico=False):
        """Local two-unit relations in order 00,01,10,11, first unit control.

        Formal joint-channel variables, not products inferred from independent
        single-unit pairs. This does not allocate a whole-machine register.
        """
        if self.nombre == 'cnot':
            return s00, s01, s11, s10
        if self.nombre == 'swap':
            return s00, s10, s01, s11
        if self.nombre == 'cz':
            return s00, s01, s10, -s11
        if self.nombre == 'cp':
            if self.angulo is None:
                raise ValueError('Falta el angulo de fase controlada')
            algebra = _algebra_simbolica() if simbolico else None
            factor = (algebra.exp(algebra.I * self.angulo) if simbolico
                      else cmath.exp(1j * self.angulo))
            return s00, s01, s10, factor*s11
        raise ValueError('No hay relacion de dos unidades para este bloque')


def fourier_inversa(puertos):
    """Inverse Fourier wiring as H, controlled phase and SWAP: quadratic size."""
    ports = tuple(puertos)
    if not ports or len(set(ports)) != len(ports):
        raise ValueError('Los puertos de Fourier deben ser distintos y no vacios')
    forward = []
    for j in range(len(ports)-1, -1, -1):
        forward.append(Bloque('h', (ports[j],)))
        for k in range(j-1, -1, -1):
            forward.append(Bloque('cp', (ports[j], ports[k]), math.ldexp(math.pi, k-j)))
    for j in range(len(ports)//2):
        forward.append(Bloque('swap', (ports[j], ports[-j-1])))
    return tuple(Bloque(block.nombre, block.puertos,
                        None if block.angulo is None else -block.angulo)
                 for block in reversed(forward))


def compilar_bloque(operation, unidades):
    name, *arguments = operation
    if name == 'fourier_inversa':
        if any(type(i) is not int or not 0 <= i < unidades for i in arguments):
            raise ValueError('Puertos de Fourier invalidos')
        return fourier_inversa(arguments)
    angle = None
    if name in ('rx', 'rz', 'fase', 'cp'):
        if not arguments:
            raise ValueError('Falta el angulo definido por el protocolo')
        angle = arguments.pop()
        if type(angle) not in (int, float) or not math.isfinite(angle):
            raise ValueError('El angulo debe ser finito')
    arity = 2 if name in ('cz', 'cp') else 1
    if name not in ('h', 'fase', 'rx', 'rz', 'cz', 'cp'):
        raise ValueError('Bloque de senales no definido')
    if len(arguments) != arity or any(type(i) is not int or not 0 <= i < unidades for i in arguments):
        raise ValueError('Puertos invalidos')
    if len(set(arguments)) != len(arguments):
        raise ValueError('Los puertos deben ser distintos')
    return (Bloque(name, tuple(arguments), angle),)
