import numpy as np

from braket.circuits import Circuit


def phase_estimation_circuit(n_qubits: int, phase: float) -> Circuit:
    """Phase estimation circuit.

    Args:
        n_qubits (int): Number of total qubits in the circuit.
        phase (float): Phase in radians.

    Returns:
        Circuit: Circuit for phase estimation.

    """
    n_precision_qubits = n_qubits - 1

    q_precision = range(n_precision_qubits)
    q_query = [n_precision_qubits]

    circ = Circuit()
    circ.h(q_precision)
    circ.x(q_query)

    for i, qubit in enumerate(reversed(q_precision)):
        circ.add(_custom_control_phase(qubit, q_query, phase * 2**i))

    iqft_circuit = _inverse_quantum_fourier_transform_circuit(n_precision_qubits)
    circ.add(iqft_circuit)

    return circ


def _custom_control_phase(control: int, target: int, angle: float) -> Circuit:
    """Custom control phase shift using CNots and Rz rotations.

    Args:
        control (int): Control qubit
        target (int): Target qubit
        angle (float): Angle of controlled-phaseshift.

    Returns:
        Circuit: Circuit for control phase shift.

    """
    circuit = Circuit()
    circuit.rz(control, angle / 2).cnot(control, target)
    circuit.rz(target, -angle / 2).cnot(control, target).rz(target, angle / 2)
    return circuit


def _inverse_quantum_fourier_transform_circuit(n_qubits: int) -> Circuit:
    """Inverse quanutm Fourier transform (iQFT).

    Args:
        n_qubits (int): Number of qubits.

    Returns:
        Circuit: inverse QFT circuit.

    """
    qft_circ = Circuit()
    qubits = list(range(n_qubits))

    for i in range(n_qubits // 2):
        qft_circ.swap(qubits[i], qubits[-i - 1])

    for k in reversed(range(n_qubits)):
        for j in reversed(range(1, n_qubits - k)):
            angle = -2 * np.pi / (2 ** (j + 1))
            qft_circ.add(_custom_control_phase(qubits[k + j], qubits[k], angle))

        qft_circ.h(qubits[k])

    return qft_circ
