#from qiskit_textbook.widgets import dj_widget

#dj_widget(size="small", case="balanced")

# initialization

#######################################################################################################################   qiskit_edit_1
from qiskit import QuantumCircuit, execute
from qiskit import QuantumRegister, ClassicalRegister
from qiskit.tools.monitor import job_monitor
from qiskit import *
import time
import matplotlib.pyplot as plt
import argparse
import sys
from concurrent.futures import ThreadPoolExecutor
import resource
from time import sleep
#########################################################################################################################

import numpy as np

options = {

'plot': True,
"rotation_error": {'rx':[0.9, 0.9], 'ry':[0.9, 0.9], 'rz':[0.9, 0.9]},
"tsp_model_error": [0.9, 0.9],
"thermal_factor": 0.9,
"decoherence_factor": 0.9,
"depolarization_factor": 0.9,
"bell_depolarization_factor": 0.9,
"decay_factor": 0.89,
}

# importing Qiskit
#from qiskit import IBMQ, Aer
#from qiskit.providers.ibmq import least_busy
#from qiskit import QuantumCircuit, assemble, transpile

# import basic plot tools

from qiskit.visualization import plot_histogram

# set the length of the n-bit input string. 
n = 3

const_oracle = QuantumCircuit(n+1)

output = np.random.randint(2)
if output == 1:
    const_oracle.x(n)

const_oracle.draw()

balanced_oracle = QuantumCircuit(n+1)

b_str = "101"

balanced_oracle = QuantumCircuit(n+1)
b_str = "101"

# Place X-gates
for qubit in range(len(b_str)):
    if b_str[qubit] == '1':
        balanced_oracle.x(qubit)
balanced_oracle.draw()

balanced_oracle = QuantumCircuit(n+1)
b_str = "101"

# Place X-gates
for qubit in range(len(b_str)):
    if b_str[qubit] == '1':
        balanced_oracle.x(qubit)

# Use barrier as divider
balanced_oracle.barrier()

# Controlled-NOT gates
for qubit in range(n):
    balanced_oracle.cx(qubit, n)

balanced_oracle.barrier()
balanced_oracle.draw()

balanced_oracle = QuantumCircuit(n+1)
b_str = "101"

# Place X-gates
for qubit in range(len(b_str)):
    if b_str[qubit] == '1':
        balanced_oracle.x(qubit)

# Use barrier as divider
balanced_oracle.barrier()

# Controlled-NOT gates
for qubit in range(n):
    balanced_oracle.cx(qubit, n)

balanced_oracle.barrier()

# Place X-gates
for qubit in range(len(b_str)):
    if b_str[qubit] == '1':
        balanced_oracle.x(qubit)

# Show oracle
balanced_oracle.draw()

dj_circuit = QuantumCircuit(n+1, n+1)

# Apply H-gates
for qubit in range(n):
    dj_circuit.h(qubit)

# Put qubit in state |->
dj_circuit.x(n)
dj_circuit.h(n)
dj_circuit.draw()

dj_circuit = QuantumCircuit(n+1, n+1)

# Apply H-gates
for qubit in range(n):
    dj_circuit.h(qubit)

# Put qubit in state |->
dj_circuit.x(n)
dj_circuit.h(n)

# Add oracle
dj_circuit += balanced_oracle
dj_circuit.draw()

dj_circuit = QuantumCircuit(n+1, n+1)

# Apply H-gates
for qubit in range(n):
    dj_circuit.h(qubit)

# Put qubit in state |->
dj_circuit.x(n)
dj_circuit.h(n)

# Add oracle
# dj_circuit += balanced_oracle

# Repeat H-gates
for qubit in range(n):
    dj_circuit.h(qubit)
dj_circuit.barrier()

# Measure
for i in range(n):
    dj_circuit.measure(i, i, basis='Ensemble', add_param='Z')

# Display circuit
dj_circuit.draw()

# use local simulator
backend = qiskit.BasicAer.get_backend('dm_simulator')
job = execute(dj_circuit, backend=backend, shots=1)
#job1 = execute(dj_circuit, backend=backend, backend_options=options, shots=1)
job_result = job.result()
#job_result1 = job1.result()
#print(job_result['results'][0]['data']['densitymatrix'])
#print(job_result1['results'][0]['data']['densitymatrix'])
job_result['results'][0]['data']['ensemble_probability']

backend = qiskit.BasicAer.get_backend('dm_simulator')
job1 = execute(dj_circuit, backend=backend, backend_options=options, shots=1)
job_result1 = job1.result()
job_result1['results'][0]['data']['ensemble_probability']

no_noise=job_result['results'][0]['data']['ensemble_probability']
x= no_noise.keys()
y = no_noise.values()



plt.bar(x, y, width=0.6, label='No Noise')

plt.xlabel('combination of bits')
plt.ylabel('Probablities')
plt.title('Ensemble Probabilities')
plt.figure(figsize=(3, 3))
plt.show()

noisy=job_result1['results'][0]['data']['ensemble_probability']
x1 = noisy.keys()
y1 = noisy.values()
plt.bar(x1, y1, width=0.6, label='Noisy', color='coral')
plt.xlabel('combination of bits')
plt.ylabel('Probablities')
plt.title('Ensemble Probabilities Over Noisy data')
plt.show()

def dj_oracle(case, n):
    # We need to make a QuantumCircuit object to return
    # This circuit has n+1 qubits: the size of the input,
    # plus one output qubit
    oracle_qc = QuantumCircuit(n+1)
    
    # First, let's deal with the case in which oracle is balanced
    if case == "balanced":
        # First generate a random number that tells us which CNOTs to
        # wrap in X-gates:
        b = np.random.randint(1,2**n)
        # Next, format 'b' as a binary string of length 'n', padded with zeros:
        b_str = format(b, '0'+str(n)+'b')
        # Next, we place the first X-gates. Each digit in our binary string 
        # corresponds to a qubit, if the digit is 0, we do nothing, if it's 1
        # we apply an X-gate to that qubit:
        for qubit in range(len(b_str)):
            if b_str[qubit] == '1':
                oracle_qc.x(qubit)
        # Do the controlled-NOT gates for each qubit, using the output qubit 
        # as the target:
        for qubit in range(n):
            oracle_qc.cx(qubit, n)
        # Next, place the final X-gates
        for qubit in range(len(b_str)):
            if b_str[qubit] == '1':
                oracle_qc.x(qubit)

    # Case in which oracle is constant
    if case == "constant":
        # First decide what the fixed output of the oracle will be
        # (either always 0 or always 1)
        output = np.random.randint(2)
        if output == 1:
            oracle_qc.x(n)
    
    oracle_gate = oracle_qc.to_gate()
    oracle_gate.name = "Oracle" # To show when we display the circuit
    return oracle_gate

def dj_algorithm(oracle, n):
    dj_circuit = QuantumCircuit(n+1, n)
    # Set up the output qubit:
    dj_circuit.x(n)
    dj_circuit.h(n)
    # And set up the input register:
    for qubit in range(n):
        dj_circuit.h(qubit)
    # Let's append the oracle gate to our circuit:
    dj_circuit.append(oracle, range(n+1))
    # Finally, perform the H-gates again and measure:
    for qubit in range(n):
        dj_circuit.h(qubit)
    
    for i in range(n):
        dj_circuit.measure(i, i, basis='Ensemble', add_param='Z')
    
    return dj_circuit


n = 4
oracle_gate = dj_oracle('balanced', n)
dj_circuit = dj_algorithm(oracle_gate, n)
dj_circuit.draw()

##NO_NOISE

backend = qiskit.BasicAer.get_backend('dm_simulator')
job = execute(dj_circuit, backend=backend, shots=1)
job_result = job.result()
print(job_result['results'][0]['data']['densitymatrix'])


##plot_histogram(answer)   ###no histrogram because of single shot in DM simulator 

## Noisy

job1 = execute(dj_circuit, backend=backend, backend_options=options, shots=1)
job_result1 = job1.result()
print(job_result1['results'][0]['data']['densitymatrix'])
from qiskit.visualization import plot_state_city
get_ipython().run_line_magic('matplotlib', 'inline')

plot_state_city(job_result1['results'][0]['data']['densitymatrix'], color=['midnightblue', 'midnightblue'],title="Density Matrix_no_noise")

##no_noise_ensemble_probabs
job_result['results'][0]['data']['ensemble_probability']

##noisy_ensemble_probabs
job_result1['results'][0]['data']['ensemble_probability']

no_noise=job_result['results'][0]['data']['ensemble_probability']

noisy=job_result1['results'][0]['data']['ensemble_probability']
labels = noisy.keys()
without_noise = no_noise.values()
with_noise = noisy.values()

x = np.arange(len(labels))  # the label locations
width = 0.35  # the width of the bars

fig, ax = plt.subplots()
rects1 = ax.bar(x - width/2, without_noise, width, label='Without Noise')
rects2 = ax.bar(x + width/2, with_noise, width, label='With Noise')

# Add some text for labels, title and custom x-axis tick labels, etc.
ax.set_ylabel('Probability')
ax.set_title('Ensemble Probabilities with Noise')
ax.set_xticks(x)
ax.set_xticklabels(labels)
ax.legend()
plt.show()



# Load our saved IBMQ accounts and get the least busy backend device with greater than or equal to (n+1) qubits
IBMQ.load_account()
provider = IBMQ.get_provider(hub='ibm-q')
backend = least_busy(provider.backends(filters=lambda x: x.configuration().n_qubits >= (n+1) and
                                   not x.configuration().simulator and x.status().operational==True))
print("least busy backend: ", backend)

# Run our circuit on the least busy backend. Monitor the execution of the job in the queue
from qiskit.tools.monitor import job_monitor

transpiled_dj_circuit = transpile(dj_circuit, backend, optimization_level=3)
job = backend.run(transpiled_dj_circuit)
job_monitor(job, interval=2)

# Get the results of the computation
results = job.result()
answer = results.get_counts()

plot_histogram(answer)

from qiskit_textbook.problems import dj_problem_oracle
oracle = dj_problem_oracle(1)

import qiskit.tools.jupyter
%qiskit_version_table