
from . import numerics as bd

from .poly import ChebyshevTExpansion, Polynomial
from .nlft import NonLinearFourierSequence
from .numerics.backend import generic_real, generic_complex
from .approximate import chebyshev_approximate

from .solvers import nlfft, weiss

def is_definite_parity(P: Polynomial, n: int = -1) -> bool:
    """Returns whether the polynomial has the parity of n. If n is not defined, then n = index of last coefficient of P."""
    if n < 0:
        n = P.support().stop - 1

    for k in P.support():
        if (k - n) % 2 != 0 and abs(P[k]) > bd.machine_threshold():
            return False

    return True


def analytic_to_laurent(P: Polynomial, n: int = -1) -> Polynomial:
    """Converts the given polynomial `p[0] + p[1] z + ... + p[n] z^n` into a definite-parity Laurent polynomial `p[0] z^(-n) + p[1] z^(-n+2) + ... + p[n] z^n`. If n is not given, then the degree of P will be considered.
    
    Note:
        No check is done on whether P is actually analytic, any negative-degree coefficients are not considered."""
    if n < 0:
        n = P.support().stop - 1

    Pl = Polynomial([0], support_start=-n)
    for k in range(n+1):
        Pl[2*k - n] = P[k]
    
    return Pl

def laurent_to_analytic(P: Polynomial, n: int = -1) -> Polynomial:
    """Converts the given definite-parity Laurent polynomial `p[0] z^(-n) + p[1] z^(-n+2) + ... + p[n] z^n` into an analytic polynomial `p[0] + p[1] z + ... + p[n] z^n`. If n is not given, then n = the Laurent degree of P.
    
    Note:
        No check is done on whether P is of definite-parity, and the other coefficients are not considered.
    """
    if n < 0:
        n = max(abs(P.support().start), abs(P.support().stop - 1))
    
    return Polynomial([P[2*k - n] for k in range(n+1)])

def chebyshev_to_laurent(c: list[generic_complex]) -> Polynomial:
    """Returns the Laurent polynomial equivalent to the Chebyshev expansion."""
    P = Polynomial(c)
    return (P + P.conjugate())/2


def phase_prefactor(F: generic_complex) -> generic_real:
    """Computes the phase prefactor for the Fourier sequence coefficient F."""

    if bd.abs(F) < bd.machine_threshold():
        return 0
    
    if bd.abs(bd.im(F)) < bd.machine_threshold():
        return -(bd.pi()/4)

    return -bd.arctan(bd.re(F)/bd.im(F))/2

class PhaseFactors:
    """Set of phase factors for a general Quantum Signal Processing protocol.
    It also provides methods to construct polynomials generated by QSP protocols.
    Each subclass of this class represents a different QSP ansatz."""
    def duplicate(self):
        raise NotImplementedError()

    def processing_operator(self, k: int):
        """Returns the k-th signal processing operator according to the given QSP variant."""
        raise NotImplementedError()
    
    def signal_operator(self, P1: Polynomial, Q1: Polynomial, P2: Polynomial, Q2: Polynomial) -> tuple[Polynomial, Polynomial]:
        """Returns the pair of polynomials given by multiplying `(P1, Q1) W(z) (P2, Q2)`, where `W(z)` is the signal operator.
        
        Note:
            This might not reflect the signal operator as expressed in the original papers. Some basis transformations are implicitly made for computational efficiency."""
        # W(z) = diag(z, z^(-1))
        zP1 = P1.shift(1)  # z*P1
        zQ1 = Q1.shift(-1) # z^(-1)*Q1
        return zP1 * P2 - zQ1 * Q2.conjugate(), zP1 * Q2 + zQ1 * P2.conjugate()
    
    def protocol_conjugation(self, P, Q):
        """Given (P, Q), this applies a final conjugation to the whole protocol, e.g., with the Hadamard gate."""
        return P, Q
    
    def processing_operator_conjugation(self, a, b):
        """This applies a conjugation to each signal processing operator returned by `processing_operator()`, e.g., with the Hadamard gate.
        This is done mainly to make `processing_operator()` return the operators as expressed by the original ansatze in the papers, while keeping computational efficiency and numerical stability."""
        return a, b
    
    def degree(self):
        """Returns the degree of the polynomials generated by the QSP protocol."""
        raise NotImplementedError()
    
    def iX(self):
        """Returns a new QSP protocol, obtained by multiplying the given QSP protocol by iX on the right, where X is the Pauli matrix.
        This will make the generated polynomials undergo the transformation (P, Q) -> (iQ, iP).
        This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.
        
        Raises:
            ValueError: if multiplying by iX does not preserve the subalgebra of the phase factors."""
        raise ValueError("Multiplying by iX is not possible.")
    
    def iY(self):
        """Returns a new QSP protocol, obtained by multiplying the given QSP protocol by iY on the right, where Y is the Pauli matrix.
        This will make the generated polynomials undergo the transformation (P, Q) -> (-Q, P).
        This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.
        
        Raises:
            ValueError: if multiplying by iY does not preserve the subalgebra of the phase factors."""
        raise ValueError("Multiplying by iY is not possible.")
    
    def iZ(self):
        """Returns a new QSP protocol, obtained by multiplying the given QSP protocol by iZ on the right, where Z is the Pauli matrix.
        This will make the generated polynomials undergo the transformation (P, Q) -> (iP, -iQ).
        This method is useful to bring to swap the places of the two polynomials, to switch between NLFT and QSP conventions.
        
        Raises:
            ValueError: if multiplying by iZ does not preserve the subalgebra of the phase factors."""
        raise ValueError("Multiplying by iZ is not possible.")
    
    def polynomials_bounds(self, inf: int, sup: int) -> tuple[Polynomial, Polynomial]:
        """Returns the pair of polynomials `(P, Q) = A[inf] w A[inf+1] w ... w A[sup-1] w A[sup]`,
        where w is the signal operator.
        
        Note:
            We use the Laurent picture to compute, so that we can use `Polynomial.conjugate()`."""
        if sup - inf < 0:
            return Polynomial([bd.make_complex(1)]), Polynomial([bd.make_complex(0)])
        if sup - inf <= 0:
            p, q = self.processing_operator(inf)
            p, q = self.processing_operator_conjugation(p, q)
            return Polynomial([p]), Polynomial([q])
        
        mid = (sup + inf) // 2
        P1, Q1 = self.polynomials_bounds(inf, mid)
        P2, Q2 = self.polynomials_bounds(mid+1, sup)

        return self.signal_operator(P1, Q1, P2, Q2) # (P1, Q1) W(z) (P2, Q2)

    def polynomials(self, inf: int = 0, sup: int = -1, mode: str = 'analytic') -> tuple[Polynomial, Polynomial]:
        """Returns the pair of polynomials (P, Q) generated by the given set of phase factors.
        The polynomials are computed with a divide and conquer strategy (see arXiv:2410.06409).
        
        Args:
            mode (str): Either 'analytic' or 'laurent', indicating whether an analytic or a Laurent QSP protocol should be composed.
        """
        if sup < 0:
            sup = self.degree()

        Pl, Ql = self.polynomials_bounds(inf, sup)
        Pl, Ql = self.protocol_conjugation(Pl, Ql)
        match mode:
            case 'analytic': # convert from Laurent to analytic picture
                return laurent_to_analytic(Pl), laurent_to_analytic(Ql)
            case 'laurent':
                return Pl, Ql
            case _:
                raise ValueError("mode can only be 'analytic' or 'laurent'.")
    
    def to_nlfs(self) -> NonLinearFourierSequence:
        """Returns the Non-Linear Fourier Sequence generating (z^{-n} P, Q),
        where (P, Q) is the pair of polynomial generated by the given set of GQSP phase factors.
        
        Note: if the phase factors are not canonical, then the phase of the leading coefficient of P is adjusted
        so that it becomes real and positive, and (z^{-n} P, Q) is in the image of the NLFT."""
        raise NotImplementedError()
    
    
class XQSPPhaseFactors(PhaseFactors):
    """Phase factors for a X-constrained QSP protocol.
    
    Signal operator: `W(z) = diag(z, z^(-1))`,
    
    Processing operators: `A[k] = exp(I*phi[k]*X)`."""
    def __init__(self, phi: list[generic_real]):
        self.phi = list(phi)

    def duplicate(self):
        return XQSPPhaseFactors(self.phi)
    
    def processing_operator(self, k: int): # exp(i phi[k] X)
        return bd.cos(self.phi[k]), 1j*bd.sin(self.phi[k])
    
    def degree(self):
        return len(self.phi) - 1

    def iX(self):
        pf = self.duplicate()
        pf.phi[-1] += bd.pi()/2
        return pf
    
    def to_nlfs(self) -> NonLinearFourierSequence:
        return NonLinearFourierSequence([1j*bd.tan(phik) for phik in self.phi])
    
    @classmethod
    def from_nlfs(cls, F: NonLinearFourierSequence) -> PhaseFactors:
        """Computes the XQSP phase factors for a given imaginary NLFT sequence.
        If `NLFT(F) = (a, b)`, then the returned phase factors will implement `(z^n a, b)` in the analytic picture.
    
        Args:
            F (NonLinearFourierSequence): The imaginary sequence to be converted to phase factors.

        Raises:
            ValueError if F is not imaginary.
    
        Note:
            The support start of F is ignored, so the support of b is assumed to start at 0."""
        if not F.is_imaginary():
            raise ValueError("The Non-Linear Fourier sequence must be imaginary in order to be turned into a XQSP protocol.")

        return XQSPPhaseFactors([bd.arctan(bd.im(Fk)) for Fk in F.coeffs])
    
class YQSPPhaseFactors(PhaseFactors):
    """Phase factors for a Y-constrained QSP protocol.

    Signal operator: `W(z) = diag(z, z^(-1))`,
    
    Processing operators: `A[k] = exp(I*phi[k]*Y)`."""
    def __init__(self, phi: list[generic_real]):
        self.phi = list(phi)

    def duplicate(self):
        return YQSPPhaseFactors(self.phi)
    
    def processing_operator(self, k: int): # exp(i phi[k] Y)
        return bd.cos(self.phi[k]), bd.sin(self.phi[k])
    
    def degree(self):
        return len(self.phi) - 1

    def iY(self):
        pf = self.duplicate()
        pf.phi[-1] += bd.pi()/2
        return pf
    
    def to_nlfs(self) -> NonLinearFourierSequence:
        return NonLinearFourierSequence([bd.tan(phik) for phik in self.phi])
    
    @classmethod
    def from_nlfs(cls, F: NonLinearFourierSequence) -> PhaseFactors:
        """Computes the YQSP phase factors for a given real NLFT sequence.
        If `NLFT(F) = (a, b)`, then the returned phase factors will implement `(z^n a, b)` in the analytic picture.
    
        Args:
            F (NonLinearFourierSequence): The real sequence to be converted to phase factors.

        Raises:
            ValueError if F is not real.
    
        Note:
            The support start of F is ignored, so the support of b is assumed to start at 0."""
        if not F.is_real():
            raise ValueError("The Non-Linear Fourier sequence must be real in order to be turned into a YQSP protocol.")

        return YQSPPhaseFactors([bd.arctan(bd.re(Fk)) for Fk in F.coeffs])

class GQSPPhaseFactors(PhaseFactors):
    """Phase factors for a GQSP protocol.
    
    Signal operator: `W(z) = diag(z, z^(-1))`,
    
    Processing operators: `A[0] = exp(I*lbd*Z) exp(I*phi[0]*X) exp(I*theta[0]*Z), A[k] = exp(I*phi[k]*X) exp(I*theta[k]*Z)`.
    
    Note:
        This class follows the convention of arXiv:2503.03026, Theorem 2, which is different from the original GQSP convention. If the convention of arXiv:2308.01501 Theorem 3 is desired, then one should use the `to_mw_gqsp()` method."""
    def __init__(self, phi: list[generic_real], lbd: generic_real=0, theta: list[generic_real]=None):
        self.lbd = lbd
        self.phi = list(phi)

        if theta is None:
            theta = [0] * len(phi)

        self.theta = list(theta)
        
        if len(theta) < len(phi):
            theta += [0] * (len(phi) - len(theta))

        if len(theta) > len(phi):
            phi += [0] * (len(theta) - len(phi))

    def duplicate(self):
        return GQSPPhaseFactors(self.phi, self.lbd, self.theta)
    
    def processing_operator(self, k: int):
        if k == 0: # exp(i lbd Z) exp(i phi X) exp(i theta Z)
            return bd.exp(1j*(self.lbd + self.theta[k]))*bd.cos(self.phi[k]), \
                1j*bd.exp(1j*(self.lbd - self.theta[k]))*bd.sin(self.phi[k])
        else: # exp(i phi X) exp(i theta Z)
            return bd.exp(1j*self.theta[k])*bd.cos(self.phi[k]), 1j*bd.exp(-1j*self.theta[k])*bd.sin(self.phi[k])
    
    def degree(self):
        return len(self.phi) - 1

    def iX(self):
        pf = self.duplicate()
        pf.theta[-1] = -pf.theta[-1]
        pf.phi[-1] += bd.pi()/2
        return pf
    
    def iY(self):
        return self.iZ().iX()
    
    def iZ(self):
        pf = self.duplicate()
        pf.theta[-1] += bd.pi()/2
        return pf
    
    def phase_offset(self) -> generic_real:
        """Returns the phase of the leading coefficient of P, where (P, Q) is the pair of polynomials generated by this set."""
        return self.lbd + sum(self.theta)
    
    def to_xqsp(self):
        """Converts the QSP phase factors into X-constrained QSP phase factors.
        
        Raises:
            ValueError: If the phase factors do not lie in the X-constrained subalgebra."""
        F = self.to_nlfs()
        if not F.is_imaginary():
            raise ValueError("The phase factors are not reducible to X-constrained QSP.")
        
        return XQSPPhaseFactors.from_nlfs(F)
    
    def to_yqsp(self):
        """Converts the QSP phase factors into Y-constrained QSP phase factors.
        
        Raises:
            ValueError: If the phase factors do not lie in the Y-constrained subalgebra."""
        F = self.to_nlfs()
        if not F.is_real():
            raise ValueError("The phase factors are not reducible to Y-constrained QSP.")
        
        return YQSPPhaseFactors.from_nlfs(F)
    
    def to_nlfs(self) -> NonLinearFourierSequence:
        n = self.degree()
        alpha = self.phase_offset() # phase of the leading coefficient of P

        psi = [bd.make_float(0)] * (n+1) # prefactors

        psi[0] = self.lbd - alpha/2
        for k in range(n):
            psi[k+1] = self.theta[k] + psi[k]

        phi = self.phi
        return NonLinearFourierSequence([1j*bd.tan(phik)*bd.exp(2j*psik) for phik, psik in zip(phi, psi)])
    
    @classmethod
    def from_nlfs(cls, F: NonLinearFourierSequence, alpha: generic_real = 0) -> PhaseFactors:
        """Computes the GQSP phase factors for a given NLFT sequence.
        If `NLFT(F) = (a, b)`, then the returned phase factors will implement `(exp(i alpha) z^n a, b)` in the analytic picture.
    
        Args:
            F (NonLinearFourierSequence): The sequence to be converted to phase factors.
            alpha (float): In the pair of polynomials (P, Q) generated by the returned phase factors,
            P will be multiplied by `exp(i alpha)`.
    
        Note:
            The support start of F is ignored, so the support of b is assumed to start at 0."""
        psi = [phase_prefactor(Fk) for Fk in F.coeffs]
        lbd = psi[0]

        phi = [bd.arctan(bd.im(Fk * bd.exp(-2j * psik))) for Fk, psik in zip(F, psi)]

        psi += [bd.make_float(0)] # we add psi[n+1] to compute theta
        theta = [psi[k+1] - psi[k] for k in range(len(phi))]

        lbd += alpha/2
        theta[-1] += alpha/2
        return GQSPPhaseFactors(phi, lbd, theta)
    
    def to_mw_gqsp(self) -> tuple[list[generic_real], list[generic_real], generic_real]:
        """Converts the GQSP phase factors to the convention of arXiv:2308.01501 Theorem 3.

        In particular, a protocol should be constructed so that `A[k] = R(theta[k], phi[k], 0)` for k < n and `A[n] = R(theta[n], phi[n], lbd)` where `R(theta, phi, lbd)` is as given in arXiv:2308.01501.

        Returns:
            The phase factors theta[k], phi[k] (as lists) and lbd, in this order."""
        n = self.degree()

        alpha = - self.lbd - sum(self.theta) - (n+1) * bd.pi() # needed to counterbalance the global phase

        mw_theta = [self.phi[k] for k in range(n+1)]
        mw_phi = [2 * self.lbd - bd.pi()/2 + alpha] + [2 * self.theta[k-1] - bd.pi() for k in range(1, n+1)]
        mw_lbd = 2 * self.theta[n] - bd.pi()/2 + alpha

        return mw_theta, mw_phi, mw_lbd

    
class ChebyshevQSPPhaseFactors(XQSPPhaseFactors):
    """Phase factors for a Chebyshev QSP protocol.

    Signal operator: `W(z) = (x, I*sqrt(1 - x^2))` with x = (z + z^(-1))/2.
    
    Processing operators: `A[k] = exp(I*phi[k]*Z)`.

    Note:
        This is the ansatz of Theorem 9 arXiv:2105.02859, but the polynomial
        construction is implemented by conjugating XQSP with a Hadamard gate."""
    def processing_operator(self, k: int): # exp(i phi[k] Z)
        return bd.exp(1j*self.phi[k]), bd.make_complex(0)

    def protocol_conjugation(self, P, Q): # Return the polynomials (P', Q') = H (P, Q) H
        return ((P + P.conjugate()) + (Q - Q.conjugate()))/2, ((P - P.conjugate()) - (Q + Q.conjugate()))/2
    
    def processing_operator_conjugation(self, a, b):
        return bd.re(a) + 1j*bd.im(b), 1j*bd.im(a) - bd.re(b)

    def duplicate(self):
        return ChebyshevQSPPhaseFactors(self.phi)
    
    def iX(self):
        raise ValueError("Multiplying by iX is not possible.")
    
    def iZ(self):
        return super.iX() # applying iZ is equivalent to applying iX, by the Hadamard conjugation
    
class QSVTPhaseFactors(ChebyshevQSPPhaseFactors):
    """Phase factors for a QSVT/Reflection QSP protocol.

    Signal operator: `R(x) = (x, sqrt(1 - x^2))` where `R(x)` is Hermitian with x = (z + z^(-1))/2.
    
    Processing operators: `A[k] = exp(I*phi[k]*Z)`.

    Note:
        This is the ansatz of Corollary 8 arXiv:1806.01838, but the polynomial
        construction is implemented by implicitly adjusting the phase factors from Chebyshev QSP, see arXiv:2105.02859, (A5)."""
    def protocol_conjugation(self, P, Q):
        d = self.degree()
        return ((P + P.conjugate()) + (Q - Q.conjugate()))*((-1j)**(d+1)/2), \
               ((P - P.conjugate()) - (Q + Q.conjugate()))*((-1j)**d/2)
    
    def processing_operator_conjugation(self, a, b):
        a *= 1j # R(x) = -I*exp(I*pi*Z/4)*W(x)*exp(I*pi*Z/4)
        return bd.re(a) + 1j*bd.im(b), 1j*bd.im(a) - bd.re(b)

    def duplicate(self):
        return QSVTPhaseFactors(self.phi)

    @classmethod
    def from_chebqsp(cls, pf: ChebyshevQSPPhaseFactors):
        """Returns a set of QSVT phase factors constructing the same left chebyshev expansion as the given QSP protocol."""
        d = pf.degree()
        phi = [0] * (d+1)

        phi[0] = pf.phi[0] + (2*d - 1)*bd.pi()/4
        for k in range(1, d):
            phi[k] = pf.phi[k] - bd.pi()/2
        phi[d] = pf.phi[d] - bd.pi()/4

        return QSVTPhaseFactors(phi)

    def to_chebqsp(self) -> ChebyshevQSPPhaseFactors:
        """Returns a set of Chebyshev QSP phase factors constructing the same left chebyshev expansion as the given QSVT protocol."""
        d = self.degree()
        phi = [0] * (d+1)

        phi[0] = self.phi[0] - (2*d - 1)*bd.pi()/4
        for k in range(1, d):
            phi[k] = self.phi[k] + bd.pi()/2
        phi[d] = self.phi[d] + bd.pi()/4

        return ChebyshevQSPPhaseFactors(phi)


#### QSP SOLVERS

def __riemann_hilbert_weiss(P: Polynomial) -> NonLinearFourierSequence:
    Q = weiss.complete(P)
    return nlfft.inlft(Q, P) # NLFT(F) = (Q, P)

def gqsp_solve(P: Polynomial, mode='qsp') -> GQSPPhaseFactors:
    r"""Returns the set of phase factors for a Generalized QSP protocol producing the given polynomial.
    A complementary Q will be computed with Weiss' algorithm.

    Args:
        mode (str): Whether the phase factors should produce (P, Q) (`'qsp'`), or (Q, P) (`'nlft'`).
    
    Note:
        The sup norm of P should be bounded by :math:`1 - \eta < 1`.
        The time required by the algorithm to compute the phase factors will scale with :math:`1/\eta`.
        
        The support_start of P will be ignored."""
    if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
        raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")
    
    match mode:
        case "qsp":
            P = -1j * Polynomial(P.coeffs, 0) # (Q, -iP) -> (P, iQ)
        case "nlft":
            P = Polynomial(P.coeffs, 0)
        case _:
            raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")
    
    F = __riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

    if mode != "qsp":
        return GQSPPhaseFactors.from_nlfs(F)
    return GQSPPhaseFactors.from_nlfs(F).iX()

def xqsp_solve(P: Polynomial, mode='qsp') -> XQSPPhaseFactors:
    r"""Returns the set of phase factors for a X-constrained QSP protocol producing the given polynomial.
    A complementary Q will be computed with Weiss' algorithm.

    Args:
        mode (str): Whether the phase factors should produce (P, Q) (`'qsp'`), or (Q, P) (`'nlft'`).

    Raises:
        ValueError: If P does not lie in the X-constrained subalgebra.
    
    Note:
        The sup norm of P should be bounded by :math:`1 - \eta < 1`.
        The time required by the algorithm to compute the phase factors will scale with :math:`1/\eta`.
        
        The support_start of P will be ignored. This is a solver for analytic QSP. In order to obtain phase factors for Laurent XQSP, use `xqsp_solve_laurent`."""
    if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
        raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")
    
    match mode:
        case "qsp":
            P = -1j * Polynomial(P.coeffs, 0) # (Q, -iP) -> (P, iQ)
        case "nlft":
            P = Polynomial(P.coeffs, 0)
        case _:
            raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")
    
    F = __riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

    if mode != "qsp":
        return XQSPPhaseFactors.from_nlfs(F)
    return XQSPPhaseFactors.from_nlfs(F).iX()

def xqsp_solve_laurent(P: Polynomial, mode='qsp') -> XQSPPhaseFactors:
    r"""Returns the set of phase factors for a X-constrained QSP protocol producing the given definite-parity polynomial. A complementary Q will be computed with Weiss' algorithm.

    Args:
        mode (str): Whether the phase factors should produce (P, Q) (`'qsp'`), or (Q, P) (`'nlft'`).

    Raises:
        ValueError: If P does not lie in the X-constrained subalgebra or P has not definite-parity.
    
    Note:
        The sup norm of P should be bounded by :math:`1 - \eta < 1`.
        The time required by the algorithm to compute the phase factors will scale with :math:`1/\eta`.
        
        The support_start of P will be ignored. In order to obtain phase factors for Laurent XQSP, first convert the polynomial into analytic form."""
    if not is_definite_parity(P):
        raise ValueError("Laurent polynomial is not of definite parity.")
    
    return xqsp_solve(laurent_to_analytic(P), mode=mode)

def yqsp_solve(P: Polynomial, mode='qsp') -> YQSPPhaseFactors:
    r"""Returns the set of phase factors for a Y-constrained QSP protocol producing the given polynomial.
    A complementary Q will be computed with Weiss' algorithm.

    Args:
        mode (str): Whether the phase factors should produce (P, Q) (`'qsp'`), or (Q, P) (`'nlft'`).

    Raises:
        ValueError: If P does not lie in the Y-constrained subalgebra.
    
    Note:
        The sup norm of P should be bounded by :math:`1 - \eta < 1`.
        The time required by the algorithm to compute the phase factors will scale with :math:`1/\eta`.
        
        The support_start of P will be ignored. This is a solver for analytic QSP. In order to obtain phase factors for Laurent YQSP, use `yqsp_solve_laurent`."""
    if 1 - P.sup_norm(4*P.effective_degree()) < bd.machine_threshold():
        raise ValueError("The given polynomial cannot be too close to or larger than one on the unit circle.")
    
    match mode:
        case "qsp":
            P = -Polynomial(P.coeffs, 0) # (Q, -P) -> (P, Q)
        case "nlft":
            P = Polynomial(P.coeffs, 0)
        case _:
            raise ValueError("The given mode does not exist. Only modes available are 'qsp', 'nlft'.")
    
    F = __riemann_hilbert_weiss(P) # NLFT(F) = (Q, P)

    if mode != "qsp":
        return YQSPPhaseFactors.from_nlfs(F)
    return YQSPPhaseFactors.from_nlfs(F).iY()

def yqsp_solve_laurent(P: Polynomial, mode='qsp') -> YQSPPhaseFactors:
    r"""Returns the set of phase factors for a Y-constrained QSP protocol producing the given definite-parity polynomial. A complementary Q will be computed with Weiss' algorithm.

    Args:
        mode (str): Whether the phase factors should produce (P, Q) (`'qsp'`), or (Q, P) (`'nlft'`).

    Raises:
        ValueError: If P does not lie in the X-constrained subalgebra or P has not definite-parity.
    
    Note:
        The sup norm of P should be bounded by :math:`1 - \eta < 1`.
        The time required by the algorithm to compute the phase factors will scale with :math:`1/\eta`.
        
        The support_start of P will be ignored. In order to obtain phase factors for Laurent YQSP, first convert the polynomial into analytic form."""
    if not is_definite_parity(P):
        raise ValueError("Laurent polynomial is not of definite parity.")
    
    return yqsp_solve(laurent_to_analytic(P), mode=mode)

def chebqsp_solve(T: list[generic_complex] | ChebyshevTExpansion) -> ChebyshevQSPPhaseFactors:
    """Returns the set of phase factors for a Chebyshev QSP protocol implementing the polynomial `P(x)` (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).

    The target polynomial will be `P(x) = c[0] + c[1] T_1(x) + c[2] T_2(x) + ... + c[n] T_n(x)`, where `T_k` are the Chebyshev polynomials of the first kind.
    
    Args:
        c: the list of coefficients of `P(x)` in the Chebyshev basis, or a ChebyshevTExpansion object.
        
    Raises:
        ValueError: If the target polynomial does not have definite parity or is not real."""
    if isinstance(T, list):
        T = ChebyshevTExpansion(T)

    if not T.is_real():
        raise ValueError("Only real polynomials are supported.")

    xqsp = xqsp_solve_laurent(T.to_laurent())
    return ChebyshevQSPPhaseFactors(xqsp.phi)

def chebqsp_approximate(f, deg: int) -> ChebyshevQSPPhaseFactors:
    r"""Approximate the given callable object `f` (which takes :math:`x \in [-1, 1]` and returns a complex number)
    and returns the Chebyshev QSP phase factors implementing an approximating polynomial of degree `deg`
    (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).
    
    Note: The parity of `deg` should coincide with the parity of f, otherwise the Chebyshev approximator might give numerical errors."""
    return chebqsp_solve(chebyshev_approximate(f, deg))

def qsvt_solve(T: list[generic_complex] | ChebyshevTExpansion) -> QSVTPhaseFactors:
    """Returns the set of phase factors for a reflection QSP/QSVT protocol implementing the polynomial `P(x)` (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).

    The target polynomial will be `P(x) = c[0] + c[1] T_1(x) + c[2] T_2(x) + ... + c[n] T_n(x)`, where `T_k` are the Chebyshev polynomials of the first kind.
    
    Args:
        c: the list of coefficients of `P(x)` in the Chebyshev basis, or a ChebyshevTExpansion object.
    
    Raises:
        ValueError: If the target polynomial does not have definite parity or is not real."""
    return QSVTPhaseFactors.from_chebqsp(chebqsp_solve(T))

def qsvt_approximate(f, deg: int) -> QSVTPhaseFactors:
    r"""Approximate the given callable object `f` (which takes :math:`x \in [-1, 1]` and returns a complex number)
    and returns the reflection QSP (a.k.a. QSVT) phase factors implementing an approximating polynomial of degree `deg`
    (as the real part of the top-left polynomial, see Theorem 9 of arXiv:2105.02859).
    
    Note: The parity of `deg` should coincide with the parity of f, otherwise the Chebyshev approximator might give numerical errors."""
    return qsvt_solve(chebyshev_approximate(f, deg))