%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

# Importing standard Qiskit libraries and configuring account
from qiskit import QuantumRegister, ClassicalRegister, execute
from qiskit import QuantumCircuit, execute, Aer, IBMQ
from qiskit.compiler import transpile, assemble
from qiskit.tools.jupyter import *
from qiskit.visualization import plot_state_city

def real_map(value, leftMin, leftMax, rightMin, rightMax):
    # Maps one range to another
    # Figure out how 'wide' each range is
    leftSpan = leftMax - leftMin
    rightSpan = rightMax - rightMin

    # Convert the left range into a 0-1 range (float)
    valueScaled = float(value - leftMin) / float(leftSpan)

    # Convert the 0-1 range into a value in the right range.
    return rightMin + (valueScaled * rightSpan)


def QRandNum(lb, ub):
    # Quantum Random Number generator
    # Select the number of qubits 
    qubits = int(np.log2(ub-lb))
    q = QuantumRegister(qubits, 'q')
    circ = QuantumCircuit(q)
    c0 = ClassicalRegister(2, 'c0')
    circ.add_register(c0)

    for i in range(qubits):
        circ.h(q[i])

    for i in range(qubits):
        circ.measure(q[i], c0)

    backend = Aer.get_backend('statevector_simulator')
    job = execute(circ, backend)
    #print(job.status())
    result = job.result()
    output = np.asarray(result.get_statevector(circ))

    num = 0
    for i in range(len(output)):
        if output[i] != 0:
            num = i
    
    y = real_map(num + 1 , 1, 2**qubits, lb, ub)
    return y

x = []
for i in range(100):
    x.append(QRandNum(1, 100))

plt.scatter(range(0, 100), x)

from qiskit.extensions import UnitaryGate
from qiskit.circuit.library import GroverOperator
from qiskit.visualization import plot_histogram

def grover_search(marked_elt: str):
    index = int(marked_elt, 2)

    arr = np.identity(2**(len(marked_elt)))
    arr[index, index] = -1

    unitary_gate = UnitaryGate(arr)

    oracle = QuantumCircuit(5)
    oracle.compose(unitary_gate, qubits=range(5), inplace=True)
    #oracle.draw(output='mpl')

    grover_op = GroverOperator(oracle)

    init = QuantumCircuit(5, 5)
    init.h(range(5))

    qc = init.compose(grover_op)
    qc.compose(grover_op, inplace=True)
    qc.compose(grover_op, inplace=True)
    qc.compose(grover_op, inplace=True)

    qc.measure(range(5),range(5))
    #qc.draw(output='mpl')

    sim = Aer.get_backend('aer_simulator')
    t_qc = transpile(qc, sim)
    counts = sim.run(t_qc).result().get_counts()

    return counts

counts = grover_search('01101')
plot_histogram(counts)

def grover_circuit(marked_elts: list):

    n = len(marked_elts[0])
    indices = [int(marked_elt, 2) for marked_elt in marked_elts]
    
    arr = np.identity(2**n)
    for index in indices:
        arr[index, index] = -1
    
    unitary_gate = UnitaryGate(arr)

    oracle = QuantumCircuit(n)
    oracle.compose(unitary_gate, qubits=range(n), inplace=True)
    
    # grover operator
    grover_op = GroverOperator(oracle)

    init = QuantumCircuit(n, n)
    init.h(range(n))

    r = int(np.pi/4*np.sqrt(2**n/len(indices)))
    print(f'{n} qubits, basis states {indices} marked, {r} rounds')

    qc = init.compose(grover_op)
    for _ in range(1, r):
        qc.compose(grover_op, inplace=True)

    qc.measure(range(n),range(n))
    return qc

q_circuit = grover_circuit(['001101', '101010'])
q_circuit.draw('mpl')

sim = Aer.get_backend('aer_simulator')
t_qc = transpile(q_circuit, sim)
counts = sim.run(t_qc).result().get_counts()
plot_histogram(counts)

# Implementation both with constant and balanced oracles

def build_oracle(size, type='constant'):
    ''' size (int): n for an n-bit the function
        type (str): "constant" or "balanced". default value = constant 
        output: an oracle
    '''
    oracle = QuantumCircuit(size+1)
    if type == 'constant':
        r = np.random.randint(2)
        if r == 1:
            oracle.x(size)
    elif type == 'balanced':
        r = np.random.randint(2**size)
        b_str = (bin(r)[2:]).zfill(size)
        # place X gates determined by b_str
        for qubit in range(size):
            if b_str[qubit] == '1':
                oracle.x(qubit)
        oracle.barrier()
        # controlled not gate
        for qubit in range(size):
            oracle.cx(qubit, size)
        oracle.barrier()
        # place X gates again
        for qubit in range(size):
            if b_str[qubit] == '1':
                oracle.x(qubit)
    else:
        print("Give valid inputs")
        return None
    
    return oracle

balanced_oracle = build_oracle(5, 'balanced')
balanced_oracle.draw('mpl')

constant_oracle = build_oracle(5)
constant_oracle.draw('mpl')

# Let's built the dj circuit now

def build_dj_circuit(size, oracle):
    ''' size: size of the oracle
        oracle: oracle representing the given function 
    '''
    qc = QuantumCircuit(size+1, size)

    # Apply H gates to the input qubits
    for qubit in range(size):
        qc.h(qubit)
    
    # Initialize the output qubit in the |-> state
    qc.x(size)
    qc.h(size)

    # Add oracle
    qc = qc.compose(oracle)

    # Add the H gate on the input qubits again
    for qubit in range(size):
        qc.h(qubit)
    
    # Measure
    for qubit in range(size):
        qc.measure(qubit, qubit)
    
    return qc

dj_circuit = build_dj_circuit(5, balanced_oracle)
dj_circuit.draw('mpl')

# Output using the Aer simulator
sim = Aer.get_backend('aer_simulator')
t_qc = transpile(dj_circuit, sim)
counts = sim.run(dj_circuit).result().get_counts()

#the output should be anything other than '00000'

plot_histogram(counts)

# with the constant oracle the output should always be '00000'
dj_circuit_const = build_dj_circuit(5, constant_oracle)
#dj_circuit_const.draw('mpl')
sim = Aer.get_backend('aer_simulator')
t_qc = transpile(dj_circuit_const, sim)
counts = sim.run(dj_circuit_const).result().get_counts()
plot_histogram(counts)

