"""
\********************************************************************************
* Copyright (c) 2024 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
"""

from qrisp import z
from qrisp.alg_primitives.qae import amplitude_amplification 
import numpy as np


def IQAE(qargs, state_function, eps, alpha, mes_kwargs={}):
    r"""
    Accelerated Quantum Amplitude Estimation (IQAE). This function performs :ref:`QAE <QAE>` with a fraction of the quantum resources of the well-known `QAE algorithm <https://arxiv.org/abs/quant-ph/0005055>`_.
    See `Accelerated Quantum Amplitude Estimation without QFT <https://arxiv.org/abs/2407.16795>`_.

    The problem of iterative quantum amplitude estimation is described as follows:

    * Given a unitary operator :math:`\mathcal{A}`, let :math:`\ket{\Psi}=\mathcal{A}\ket{0}\ket{\text{False}}`.
    * Write :math:`\ket{\Psi}=\sqrt{a}\ket{\Psi_1}\ket{\text{True}}+\sqrt{1-a}\ket{\Psi_0}\ket{\text{False}}` as a superposition of the orthogonal good and bad components of :math:`\ket{\Psi}`.
    * Find an estimate for $a$, the probability that a measurement of $\ket{\Psi}$ yields a good state.

    Parameters
    ----------
    qargs : list[QuantumVariable]
        The list of QuantumVariables which represent the state,
        the quantum amplitude estimation is performed on. The last variable in the list must be of type :ref:`QuantumBool`.
    state_function : function
        A Python function preparing the state :math:`\ket{\Psi}`.
        This function will receive the variables in the list ``qargs`` as arguments in the
        course of this algorithm.
    eps : float
        Accuracy $\epsilon>0$ of the algorithm.
    alpha : float
        Confidence level $\alpha\in (0,1)$ of the algorithm.
    mes_kwargs : dict, optional
        The keyword arguments for the measurement function. Default is an empty dictionary.

    Returns
    -------
    a : float
        An estimate $\hat{a}$ of $a$ such that 
        
    .. math::
        
        \mathbb P\{|\hat{a}-a|<\epsilon\}\geq 1-\alpha

    Examples
    --------

    We show the same **Numerical integration** example which can also be found in the :ref:`QAE documentation <QAE>`.

    We wish to evaluate

    .. math::

        A=\int_0^1f(x)\mathrm dx.

    For this, we set up the corresponding ``state_function`` acting on the variables in ``input_list``:

    ::

        from qrisp import QuantumFloat, QuantumBool, control, z, h, ry, IQAE
        import numpy as np

        n = 6 
        inp = QuantumFloat(n,-n)
        tar = QuantumBool()
        input_list = [inp, tar]

    For example, if $f(x)=\sin^2(x)$, the ``state_function`` can be implemented as follows:

    ::

        def state_function(inp, tar):
            h(inp)

            N = 2**inp.size
            for k in range(inp.size):
                with control(inp[k]):
                    ry(2**(k+1)/N,tar)

    Finally, we apply IQAE and obtain an estimate $a$ for the value of the integral $A=0.27268$.

    ::

        a = IQAE(input_list, state_function, eps=0.01, alpha=0.01)

    >>> a 
    0.26782038552705856

    """

    # The oracle tagging the good states
    def oracle_function(*args):  
        tar = args[-1]
        z(tar)

    E = 1/2 * pow(np.sin(np.pi * 3/14), 2) -  1/2 * pow(np.sin(np.pi * 1/6), 2) 
    F = 1/2 * np.arcsin(np.sqrt(2 * E))
    
    C = 4/ (6*F + np.pi)
    break_cond =  2 * eps + 1
    #theta_b = 0
    #theta_sh = 1
    K_i = 1
    m_i = 0
    index_tot = 0
    while break_cond > 2 * eps: 
        index_tot +=1
        
        alp_i = C*alpha * eps * K_i 
        N_i = int(np.ceil(1/(2 * pow(E, 2) ) * np.log(2/alp_i) ) )

        # Perform quantum step
        qargs_dupl = [qarg.duplicate() for qarg in qargs]
        A_i  = quant_step( int((K_i -1 )/2) , N_i, qargs_dupl, state_function, 
                        oracle_function, mes_kwargs ) 
        
        for qarg in qargs_dupl:
            qarg.delete()
        
        # Compute new thetas
        theta_b, theta_sh = compute_thetas(m_i,  K_i, A_i, E)
        # Compute new Li
        L_new, m_new = compute_Li(m_i , K_i, theta_b, theta_sh)
        
        m_i = m_new
        K_i = L_new * K_i

        break_cond = abs( theta_b - theta_sh )
    
    final_res = np.sin((theta_b+theta_sh)/2)**2
    return final_res


def quant_step(k, N, qargs, state_function, oracle_function, mes_kwargs):
    """
    Performs the quantum step, i.e., Quantum Amplitude Amplification, 
    in accordance to `Accelerated Quantum Amplitude Estimation without QFT <https://arxiv.org/abs/2407.16795>`_

    Parameters
    ----------
    k : int
        The amount of amplification steps, i.e., the power of :math:`\mathcal{Q}` in amplitude amplification.
    N : int
        The amount of shots, i.e., the amount of times the last qubit is measured after the amplitude amplification steps.
    state_function : function
        A Python function preparing the state :math:`\ket{\Psi}`.
        This function will receive the variables in the list ``args`` as arguments in the
        course of this algorithm.
    oracle_function : function
        A Python function tagging the good state :math:`\ket{\Psi_1}`.
        This function will receive the variables in the list ``args`` as arguments in the
        course of this algorithm.
    mes_kwargs : dict, optional
        The keyword arguments for the measurement function. Default is an empty dictionary.
    """

    if k==0:
        state_function(*qargs)
    else:
        state_function(*qargs)
        amplitude_amplification(qargs, state_function, oracle_function, iter = k)

    # store result of last qubit 
    # shot-based measurement 
    mes_kwargs["shots"] = N
    res_dict = qargs[-1].get_measurement(**mes_kwargs)
    
    # case of single dict return, this should not occur but to be safe
    if True not in list(res_dict.keys()):
        return 0 
    
    a_i = res_dict[True]

    return a_i 


def compute_thetas(m_i, K_i, A_i, E): 
    """
    Helper function to perform statistical evaluation and compute the angles for the next iteration. 
    See `the original paper <https://arxiv.org/abs/2407.16795>`_ , Algorithm 1.
    
    Parameters
    ----------
    m_i : Int
        Used for the computation of the interval of allowed angles.
    K_i : Int
        Maximal amount of amplitude amplification steps for the next iteration.
    A_i : Float
        Share of ``1``-measurements in amplitude amplification steps
    E : 
        :math:`\epsilon` limit
    """
    
    b_max = max(A_i - E, 0)
    sh_min = min(A_i + E, 1)

    theta_b_intermed = update_angle(b_max, m_i)
    theta_b = theta_b_intermed/K_i

    sh_theta_intermed = update_angle(sh_min, m_i)
    sh_theta = sh_theta_intermed/K_i

    assert round( pow( np.sin(K_i * theta_b),2) , 8 )  == round(b_max, 8)
    assert round( pow( np.sin(K_i * sh_theta),2), 8 )  == round(sh_min, 8)

    return theta_b, sh_theta


def update_angle(old_angle, m_in):
    """
    Subroutine to compute new angles.

    Parameters
    ----------
    old_angle : float
        Old angle from last iteration.    
    m_in : int
        Used for the computation of the interval of allowed angles.
    """

    val_intermed1 = np.arcsin( np.sqrt(old_angle) ) - np.pi
    val_intermed2 = np.arcsin( - np.sqrt(old_angle) )
    cond_break = True
    while cond_break :
        if not (m_in*np.pi/2 <= val_intermed1 <= (m_in+1)*np.pi/2):
            val_intermed1 += np.pi
        else: 
            final_intermed = val_intermed1
            cond_break = False
            break

        if not (m_in*np.pi/2 <= val_intermed2 <= (m_in+1)*np.pi/2):
            val_intermed2 += np.pi
        else: 
            final_intermed = val_intermed2
            cond_break = False
            break

    return final_intermed
    

def compute_Li(m_i , K_i, theta_b, theta_sh):
    """
    Helper function to perform statistical evaluation and compute further values for the next iteration. 
    See `the original paper <https://arxiv.org/abs/2407.16795>`_ , Algorithm 1.

    Parameters
    ----------
    m_i : int
        Used for the computation of the interval of allowed angles.
    K_i : int
        Maximal amount of amplitude amplification steps for the next iteration.
    theta_b : float
        Lower bound for angle from last iteration.
    theta_b : float
        Upper bound for angle from last iteration.
    """

    Li_list = (3,5,7)

    # create the Li-values
    for Li in Li_list:
        # create the possible M_new vals
        m_new_list = range(Li*m_i, Li*m_i +Li)
        first_val = Li *K_i * theta_b
        sec_val = Li *K_i * theta_sh

        for m_new in m_new_list:
            # check the conditions
            if m_new*np.pi/2 <= first_val <= (m_new+1)*np.pi/2:
                if m_new*np.pi/2 <= sec_val <= (m_new+1)*np.pi/2:
                    
                    #assert  m_new*np.pi/2 <= Li *K_i * theta_b <= (m_new+1)*np.pi/2
                    #assert  m_new*np.pi/2 <= Li *K_i * theta_sh <= (m_new+1)*np.pi/2
                    elem1 = Li
                    elem2 = m_new

                    return  elem1, elem2
