# Importing standard Qiskit libraries
from qiskit import QuantumCircuit, transpile, Aer, execute
from qiskit.tools.jupyter import *
from qiskit.visualization import *
from ibm_quantum_widgets import *

# qiskit-ibmq-provider has been deprecated.
# Please see the Migration Guides in https://ibm.biz/provider_migration_guide for more detail.
from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Estimator, Session, Options

# Loading your IBM Quantum account(s)
service = QiskitRuntimeService(channel="ibm_quantum")
backend = Aer.get_backend('qasm_simulator') 

# Invoke a primitive. For more details see https://qiskit.org/documentation/partners/qiskit_ibm_runtime/tutorials.html
# result = Sampler("ibmq_qasm_simulator").run(circuits).result()

from qiskit.providers.fake_provider import FakeGuadalupe, FakeKolkata
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel

not_gate=QuantumCircuit(1,1) # Create a quantum circuit with 1 qubit and 1 classical bit
not_gate.x(0)
not_gate.measure(0,0)
not_gate.draw(output='mpl')

and_gate=QuantumCircuit(3,1) # Create a quantum circuit with 3 qubits and 1 classical bit
and_gate.ccx(0,1,2)
and_gate.measure(2,0)
and_gate.draw(output='mpl')

or_gate=QuantumCircuit(3,1) # Create a quantum circuit with 3 qubits and 1 classical bit
or_gate.cx(1,2)
or_gate.cx(0,2)
or_gate.ccx(0,1,2)
or_gate.measure(2,0)
or_gate.draw(output='mpl')

xor_gate=QuantumCircuit(3,1) # Create a quantum circuit with 3 qubits and 1 classical bit
xor_gate.cx(0,2)
xor_gate.cx(1,2)
xor_gate.measure(2,0)
xor_gate.draw(output='mpl')

# Write your code here
myxor_gate=QuantumCircuit(3) # Create a quantum circuit with 3 qubits and 1 classical bit
myxor_gate.cx(0,2)
myxor_gate.cx(1,2)
sudokucheck = QuantumCircuit(9)

sudokucheck = sudokucheck.compose(myxor_gate,[0,1,5]) # Check if v_0 = v_1

sudokucheck = sudokucheck.compose(myxor_gate,[2,3,6]) # Check if v_2 = v_3

sudokucheck = sudokucheck.compose(myxor_gate,[0,2,7]) # Check if v_0 = v_2

sudokucheck = sudokucheck.compose(myxor_gate,[1,3,8]) # Check if v_1 = v_3

sudokucheck.mct([5,6,7,8],4) # Take the AND of all the upper results


sudokucheck.draw('mpl')


# Write your code here
oracle = QuantumCircuit(9)
oracle.x(4)
oracle.h(4)
oracle = oracle.compose(sudokucheck, [0,1,2,3,4,5,6,7,8])
oracle.draw('mpl')

# Write your code here
uncompute = QuantumCircuit(9)
uncompute = uncompute.compose(oracle, [0,1,2,3,4,5,6,7,8])

uncompute = uncompute.compose(myxor_gate,[1,3,8])
uncompute = uncompute.compose(myxor_gate,[0,2,7])
uncompute = uncompute.compose(myxor_gate,[2,3,6])
uncompute = uncompute.compose(myxor_gate,[0,1,5])
uncompute.h(4)
uncompute.x(4)
uncompute.draw('mpl')

# Write a code to construct the diffuser operator for the problem
def diffuser_operator(n):
    diffuse = QuantumCircuit(n)
    controlbit = []      # This will help to list all the control bits for 
    for i in range(n):
        diffuse.h(i)     # First layer of hadamard gates
        diffuse.x(i)     # Layer of X gates
        if i>0:
            controlbit.append(i) 
    diffuse.h(0)
    diffuse.mcx(control_qubits=controlbit, target_qubit=[0])   # applied C^nX
    diffuse.h(0)
    for i in range(n):
        diffuse.x(i)     # The second last layer of X gates    
        diffuse.h(i)     # The last layer of hadamard gates
    return diffuse
    
n = 4
diffuser_circuit = diffuser_operator(4)
diffuser_circuit.draw('mpl')
    
    


# Write your code here to put all the pieces together as a single circuit

# pi/4 x sqrt{2^4}=pi/4 which is approximately 3. So we will apply the operator = uncompute + diffuser_operator 3 times
grover_search = QuantumCircuit(9,4)
for i in range(4):
    grover_search.h(i)
for i in range(3):
    grover_search = grover_search.compose(uncompute, [0,1,2,3,4,5,6,7,8]) # apply the uncompute operator
    grover_search = grover_search.compose(diffuser_circuit, [0,1,2,3])    # then apply the diffuser operator
grover_search.measure([0,1,2,3],[0,1,2,3])
grover_search.draw('mpl')

# Write a code to run the final circuit and plot the results to see if there are any satisfying assignments
result = execute(grover_search, backend).result() 
counts = result.get_counts(grover_search)
plot_histogram(counts)

# List down the satisfying assignments and verify for yourself if they are correct

# Write your code here
fake_kolkata = FakeKolkata()           
fake_guadalupe = FakeGuadalupe()

fake_kolkata_noisemodel = NoiseModel.from_backend(fake_kolkata)  # Created a noisemodel from FakeKolkata backend
fake_guadalupe_noisemodel = NoiseModel.from_backend(fake_guadalupe)  # Created a noisemodel from FakeGuadalupe backend

# simulate with fakekoklkata backend
simulate_fake_kolkata = AerSimulator(noise_model = fake_kolkata_noisemodel) 
fake_kolkata_circ= transpile(grover_search,simulate_fake_kolkata)
fake_kolkata_counts = simulate_fake_kolkata.run(fake_kolkata_circ,shots=1000).result().get_counts()
plot_histogram(fake_kolkata_counts)

# Simulate with fakeguadalupe bakcend
simulate_fake_guadalupe = AerSimulator(noise_model = fake_guadalupe_noisemodel)
fake_guadalupe_circ= transpile(grover_search,simulate_fake_guadalupe)
fake_guadalupe_counts = simulate_fake_guadalupe.run(fake_guadalupe_circ,shots=1000).result().get_counts()
plot_histogram(fake_guadalupe_counts)

# Write your code here