# general imports
import math

# AWS imports: Import Braket SDK modules
from braket.circuits import Circuit, circuit


# QFT subroutine without swaps
def qft_no_swap(qubits):
    """Subroutine of the QFT excluding the final SWAP gates, applied to the qubits argument.
    Returns the a circuit object.

    Args:
        qubits (int): The list of qubits on which to apply the QFT

    """
    # On a single qubit, the QFT is just a Hadamard.
    if len(qubits) == 1:
        return Circuit().h(qubits)

    # For more than one qubit, we define the QFT recursively (as shown on the right half of the image above):
    qftcirc = Circuit()

    # First add a Hadamard gate
    qftcirc.h(qubits[0])

    # Then apply the controlled rotations, with weights (angles) defined by the distance to the control qubit.
    for k, qubit in enumerate(qubits[1:]):
        qftcirc.cphaseshift(qubit, qubits[0], 2 * math.pi / (2 ** (k + 2)))

    # Now apply the above gates recursively to the rest of the qubits
    qftcirc.add(qft_no_swap(qubits[1:]))

    return qftcirc


# To complete the full QFT, add swap gates to reverse the order of the qubits
@circuit.subroutine(register=True)
def qft_recursive(qubits):
    """Construct a circuit object corresponding to the Quantum Fourier Transform (QFT)
    algorithm, applied to the argument qubits.

    Args:
        qubits (int): The list of qubits on which to apply the QFT

    """
    qftcirc = Circuit()

    # First add the QFT subroutine above
    qftcirc.add(qft_no_swap(qubits))

    # Then add SWAP gates to reverse the order of the qubits:
    for i in range(math.floor(len(qubits) / 2)):
        qftcirc.swap(qubits[i], qubits[-i - 1])

    return qftcirc


# Non-recursive definition of the QFT
def qft(qubits):
    """Construct a circuit object corresponding to the Quantum Fourier Transform (QFT)
    algorithm, applied to the argument qubits.  Does not use recursion to generate the QFT.

    Args:
        qubits (int): The list of qubits on which to apply the QFT

    """
    qftcirc = Circuit()

    # get number of qubits
    num_qubits = len(qubits)

    for k in range(num_qubits):
        # First add a Hadamard gate
        qftcirc.h(qubits[k])

        # Then apply the controlled rotations, with weights (angles) defined by the distance to the control qubit.
        # Start on the qubit after qubit k, and iterate until the end.  When num_qubits==1, this loop does not run.
        for j in range(1, num_qubits - k):
            angle = 2 * math.pi / (2 ** (j + 1))
            qftcirc.cphaseshift(qubits[k + j], qubits[k], angle)

    # Then add SWAP gates to reverse the order of the qubits:
    for i in range(math.floor(num_qubits / 2)):
        qftcirc.swap(qubits[i], qubits[-i - 1])

    return qftcirc


# inverse QFT
@circuit.subroutine(register=True)
def inverse_qft(qubits):
    """Construct a circuit object corresponding to the inverse Quantum Fourier Transform (QFT)
    algorithm, applied to the argument qubits.  Does not use recursion to generate the circuit.

    Args:
        qubits (int): The list of qubits on which to apply the inverse QFT

    """
    # Instantiate circuit object
    qftcirc = Circuit()

    # Fet number of qubits
    num_qubits = len(qubits)

    # First add SWAP gates to reverse the order of the qubits:
    for i in range(math.floor(num_qubits / 2)):
        qftcirc.swap(qubits[i], qubits[-i - 1])

    # Start on the last qubit and work to the first.
    for k in reversed(range(num_qubits)):
        # Apply the controlled rotations, with weights (angles) defined by the distance to the control qubit.
        # These angles are the negative of the angle used in the QFT.
        # Start on the last qubit and iterate until the qubit after k.
        # When num_qubits==1, this loop does not run.
        for j in reversed(range(1, num_qubits - k)):
            angle = -2 * math.pi / (2 ** (j + 1))
            qftcirc.cphaseshift(qubits[k + j], qubits[k], angle)

        # Then add a Hadamard gate
        qftcirc.h(qubits[k])

    return qftcirc
