import numpy as np
from utils_circuit import adjoint, get_unitary

from braket.circuits import Circuit, circuit

# monkey patch to Circuit class
Circuit.get_unitary = get_unitary
Circuit.adjoint = adjoint


# helper function to apply XZX to given qubit
@circuit.subroutine(register=True)
def minus_R_B(qubit):
    """Function to apply a minus sign to |B>|0>. This is achieved by applying XZX to the ancilla qubit.

    Args:
        qubit: the ancilla qubit on which we apply XZX.

    """
    # instantiate circuit object
    circ = Circuit()

    # Apply sequence XZX to given qubit
    circ.x(qubit).z(qubit).x(qubit)

    return circ


# Helper function to apply rotation -R0
@circuit.subroutine(register=True)
def minus_R_zero(qubits, use_explicit_unitary=False):
    """Function to implement transformation: |0,0,...0> -> -|0,0,...0>, all others unchanged.

    Args:
        qubits: list of qubits on which to apply the gates
        use_explicit_unitary (default False): Flag to specify that we could instead implement
        the desired gate using a custom gate defined by the unitary diag(-1,1,...,1).

    """
    circ = Circuit()

    # If the use_explicit_matrix flag is True, we just apply the unitary defined by |0,0,...0> -> -|0,0,...0>
    if use_explicit_unitary:
        # Create the matrix diag(-1,1,1,...,1)
        unitary = np.eye(2 ** len(qubits))
        unitary[0][0] = -1
        # Add a gate defined by this matrix
        circ.unitary(matrix=unitary, targets=qubits)

    # Otherwise implement the unitary using ancilla qubits:
    else:
        # Flip all qubits. We now need to check if all qubits are |1>, rather than |0>.
        circ.x(qubits)

        # If we have only 1 qubit, we only need to apply XZX to that qubit to pick up a minus sign on |0>
        if len(qubits) < 2:
            circ.z(qubits)

        # For more qubits, we use Toffoli (or CCNOT) gates to check that all the qubits are in |1> (since we applied X)
        else:
            # Dynamically add ancilla qubits, starting on the next unused qubit in the circuit
            # TODO: if this subroutine is being applied to a subset of qubits in a circuit, these ancilla
            # registers might already be used. We could pass in circ as an argument and add ancillas outside of
            # circ.targets
            ancilla_start = max(qubits) + 1

            # Check that the first two register qubits are both 1's using a CCNOT on a new ancilla qubit.
            circ.ccnot(qubits[0], qubits[1], ancilla_start)

            # Now add a CCNOT from each of the next register qubits, comparing with the ancilla we just added.
            # Target on a new ancilla. If len(qubits) is 2, this does not execute.
            for ii, qubit in enumerate(qubits[2:]):
                circ.ccnot(qubit, ancilla_start + ii, ancilla_start + ii + 1)

            # Apply a Z gate to the last ancilla qubit to pick up a minus sign if all of the register qubits are |1>
            ancilla_end = ancilla_start + len(qubits[2:])
            circ.z(ancilla_end)

            # Now uncompute to disentangle the ancilla qubits by applying CCNOTs in the reverse order to above.
            for jj, qubit in enumerate(reversed(qubits[2:])):
                circ.ccnot(qubit, ancilla_end - jj - 1, ancilla_end - jj)

            # Finally undo the last CCNOT on the first two register qubits.
            circ.ccnot(qubits[0], qubits[1], ancilla_start)

        # Flip all qubits back
        circ.x(qubits)

    return circ


@circuit.subroutine(register=True)
def grover_iterator(A, flag_qubit, qubits=None, use_explicit_unitary=False):
    """Function to implement the Grover iterator Q=A R_0 A* R_B.

    Args:
        A: Circuit defining the unitary A
        flag_qubit: Specifies which of the qubits A acts on labels the good/bad subspace.
                    Must be an element of qubits (if passed) or A.qubits.
        qubits: list of qubits on which to apply the gates (including the flag_qubit).
                If qubits is different from A.qubits, A is applied to qubits instead.
        use_explicit_unitary: Flag to specify that we should implement R_0 using using a custom
                              gate defined by the unitary diag(-1,1,...,1). Default is False.

    """
    # If no qubits are passed, apply the gates to the targets of A
    if qubits is None:
        qubits = A.qubits
    elif len(qubits) != len(A.qubits):
        raise ValueError(
            "Number of desired target qubits differs from number of targets in A".format(),
        )

    # Verify that flag_qubit is one of the qubits on which A acts, or one of the user defined qubits
    if flag_qubit not in qubits:
        raise ValueError(f"flag_qubit {flag_qubit!r} is not in targets of A")

    # Instantiate the circuit
    circ = Circuit()

    # Apply -R_B to the flag qubit
    circ.minus_R_B(flag_qubit)

    # Apply A^\dagger. Use target mapping if different qubits are specified
    circ.add_circuit(A.adjoint(), target=qubits)

    # Apply -R_0
    circ.minus_R_zero(qubits, use_explicit_unitary)

    # Apply A, mapping targets if desired.
    circ.add_circuit(A, target=qubits)

    return circ


@circuit.subroutine(register=True)
def qaa(A, flag_qubit, num_iterations, qubits=None, use_explicit_unitary=False):
    """Function to implement the Quantum Amplitude Amplification Q^m, where Q=A R_0 A* R_B, m=num_iterations.

    Args:
        A: Circuit defining the unitary A
        flag_qubit: Specifies which of the qubits A acts on labels the good/bad subspace.
                    Must be an element of qubits (if passed) or A.qubits.
        num_iterations: number of applications of the Grover iterator Q.
        qubits: list of qubits on which to apply the gates (including the flag_qubit).
                If qubits is different from A.qubits, A is applied to qubits instead.
        use_explicit_unitary: Flag to specify that we should implement R_0 using using a custom
                              gate defined by the unitary diag(-1,1,...,1). Default is False.

    """
    # Instantiate the circuit
    circ = Circuit()

    # Apply the Grover iterator num_iterations times:
    for _ in range(num_iterations):
        circ.grover_iterator(A, flag_qubit, qubits, use_explicit_unitary)

    return circ
