import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# main classes and functions
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, Aer, BasicAer, execute

# visualization stuff
from qiskit.visualization import (plot_histogram, plot_bloch_multivector,
                                  plot_state_city, plot_state_hinton,
                                  plot_state_qsphere,
                                  plot_state_paulivec,
                                  plot_bloch_multivector,
                                  plot_bloch_vector,
                                  plot_gate_map,
                                  plot_error_map)


# gates
from qiskit.circuit.library import CXGate, MCMT, XGate

# quantum info theory stuff
from qiskit.quantum_info import Operator, Statevector, state_fidelity, process_fidelity, average_gate_fidelity

# other tools
from qiskit.tools import job_monitor
from qiskit import IBMQ

def show_figure(fig):
    '''
    auxiliar function to display plot 
    even if it's not the last command of the cell
    from: https://github.com/Qiskit/qiskit-terra/issues/1682
    '''
    
    new_fig = plt.figure()
    new_mngr = new_fig.canvas.manager
    new_mngr.canvas.figure = fig
    fig.set_canvas(new_mngr.canvas)
    plt.show(fig)

def test_circuit_qasm(qc):
    ''' 
    auxiliar function, used to run a given quantum circuit with the qasm simulator.
    measurement is added in this function, so you must pass a circuit
    with no classical registers and measurements.
    '''
    
    qc.measure_all()

    #################################

    backend = Aer.get_backend("qasm_simulator")

    job = execute(qc, backend, shots=1e5, seed_simulator=42)
    results = job.result()
    counts = results.get_counts()

    return plot_histogram(counts, title="Results", figsize=(12, 4))

def test_circuit_sv(qc, print_stuff=True):
    ''' 
    auxiliar function, used to run a given quantum circuit with the statevector simulator.
    it optinally prints the state vector components and respective probabilities.
    '''

    backend = Aer.get_backend("statevector_simulator")

    job = execute(qc, backend, seed_simulator=42)
    results = job.result()
    counts = results.get_counts()
    
    if print_stuff:
        sv = results.data(0)['statevector']
        probs = sv**2

        print(f"Statevector:\t{sv}\n")
        print(f"Probabilities:\t{probs}")

    return plot_histogram(counts, title="Results", figsize=(12, 4))

# 1 - A)

qc = QuantumCircuit(4, 4)

qc.qregs, qc.cregs

# 2 - C)

qc = QuantumCircuit(1)

qc.ry(3*np.pi/4, 0)

test_circuit_sv(qc)

# 3 - A), B) & D) are possible

inp_reg = QuantumRegister(2, name="inp")
ancilla = QuantumRegister(1, name="anc")

qc = QuantumCircuit(inp_reg, ancilla)

qc.h(inp_reg)
qc.x(ancilla)

qc.barrier()

qc.h(inp_reg[0])
qc.h(inp_reg[1])
qc.x(ancilla)

qc.barrier()

qc.h(inp_reg[0:2])
qc.x(ancilla[0])

qc.draw("mpl")

# 4 - A)

inp = QuantumRegister(3, name="q")
meas = ClassicalRegister(3, name="c")

qc = QuantumCircuit(inp, meas)

# # this would also be possible, but it's not in options
# qc.measure(inp, meas)

qc.measure([0, 1, 2], [0, 1, 2])

qc.draw("mpl")

# 5 - A)

bell = QuantumCircuit(2)

# this makes |01> + |10>
bell.h(0)
bell.x(1)
bell.cx(0, 1)

show_figure(bell.draw("mpl"))
test_circuit_sv(bell)

# 6 - A) & C)

qc = QuantumCircuit(1, 1)

# # both below are equivalent

# qc.h(0)
qc.ry(np.pi/2, 0)

show_figure(qc.draw("mpl"))

simulator = Aer.get_backend("statevector_simulator")
job = execute(qc, simulator)
results = job.result()

outputstate = results.get_statevector(qc)

print(f"\nState:\t{outputstate}\n")

plot_bloch_multivector(outputstate)

# 7 - B)

# S gate induces a pi/2 phase! (is a P gate with \theta = pi/2, i.e., it's a Rz gate with \theta = pi/2)
# tip: S = T^2, hence the "S" stands for "T Squared" (not sure if that's the actual reason for the name, but it's a mnemonic)

qc = QuantumCircuit(1)

qc.h(0)
qc.s(0)

show_figure(qc.draw("mpl"))

simulator = Aer.get_backend("statevector_simulator")
job = execute(qc, simulator)
results = job.result()

outputstate = results.get_statevector(qc)
print(f"\nState:\t{outputstate}\n")

plot_bloch_multivector(outputstate)

# another way to get the statevector, using the Statevector class

qc = QuantumCircuit(1)

qc.h(0)
qc.s(0)

show_figure(qc.draw("mpl"))

outputstate = Statevector.from_label("0").evolve(qc)

print(f"\nState:\t{outputstate}\n")

plot_bloch_multivector(outputstate)

# 8 - A), B)

qc = QuantumCircuit(2)

# # both options below work

# v = [1/np.sqrt(2), 0, 0, 1/np.sqrt(2)]
# qc.initialize(v, [0, 1])

qc.h(0)
qc.cx(0, 1)

show_figure(qc.draw("mpl"))

simulator = Aer.get_backend("statevector_simulator")
result = execute(qc, simulator).result()
statevector = result.get_statevector(qc)

print(f"\nState:\t{statevector}\n")

# 9 - D)

# notice how the first tree are equivalent to cnot
# the last one is the only multi-qubit gate OTHER THAN a cnot ;)

qc = QuantumCircuit(2)

################################

qc.cx(0, 1)
qc.barrier()

################################

qc.cnot(0, 1)
qc.barrier()

################################

# that's nice! multi-controlled x
qc.mct([0], 1)
qc.barrier()

################################

qc.cz(0, 1)
qc.barrier()

################################

qc.draw("mpl")

# 10 - D)

# notice how the first tree are equivalent to the toffoli gate
# the last one is the only multi-qubit gate OTHER THAN a toffoli ;)

qc = QuantumCircuit(3)

################################

qc.ccx(0, 1, 2)
qc.barrier()

################################

qc.mct([0, 1], 2)
qc.barrier()

################################

# very nice construction!

ccx = CXGate().control()
qc.append(ccx, [0, 1, 2])

qc.barrier()

################################

qc.cry(0, 1, 2)
qc.barrier()

################################

qc.draw("mpl")

# another nice way to do toffoli

qc = QuantumCircuit(3)

mct = MCMT(gate='x', num_ctrl_qubits = 2, num_target_qubits = 1)

qc.append(mct, [0,1,2]) 

qc = qc.decompose()

qc.draw("mpl")

# 11 - B) & C)

qc = QuantumCircuit(3, 3)

qc.barrier()

qc.barrier([0, 1, 2])

qc.draw("mpl")

# 12 - A)

qc = QuantumCircuit(1, 1)

qc.h(0)
qc.t(0)

qc.t(0)
qc.h(0)

qc.measure(0, 0)

show_figure(qc.draw("mpl"))

print("\nSince S = T^2, the circuit above is equivalent to:")

qc = QuantumCircuit(1, 1)

qc.h(0)
qc.s(0)
qc.h(0)

qc.measure(0, 0)

show_figure(qc.draw("mpl"))

# 13 - A)

qc = QuantumCircuit(2, 2)

qc.h(0)
qc.barrier(0)

qc.cx(0, 1)
qc.barrier([0, 1])

# take a look in the docstring!
print(f"\nDepth of the circuit: {qc.depth()}")

qc.draw("mpl")

# 14 - A)

qc = QuantumCircuit(3)

qasm_sim = Aer.get_backend("qasm_simulator")

# coupling map connecting 3 qubits linearly
couple_map = [[0, 1], [1, 2]]

job = execute(qc, qasm_sim, shots=1024, coupling_map=couple_map)

result = job.result()

# 15 - A)

backend = BasicAer.get_backend("qasm_simulator")

qc = QuantumCircuit(3)

execute(qc, backend, shots=1024, coupling_map=[[0, 1], [1, 2]])

# 16 - A), C) & D)

# answer here: https://qiskit.org/documentation/apidoc/providers_basicaer.html#simulators

qasm = BasicAer.get_backend("qasm_simulator")

sv = BasicAer.get_backend("statevector_simulator")

unitary = BasicAer.get_backend("unitary_simulator")

# 17 - B)

backend = BasicAer.get_backend("statevector_simulator")

# 18 - C)

# info: https://qiskit.org/documentation/stubs/qiskit.quantum_info.Operator.html

# # i think the following is possible:
# op = Operator([[0, 1], [1, 0]])

qc = QuantumCircuit(1)

qc.x(0)

op = Operator(qc)

op

# 19 - C)

# info here: https://qiskit.org/documentation/tutorials/circuits_advanced/02_operators_overview.html#Process-Fidelity
# namely:
# "We may also compare operators using the process_fidelity function from the Quantum Information module.
# This is an information theoretic quantity for how close two quantum channels are to each other,
# and in the case of unitary operators it does not depend on global phase."

op_a = Operator(XGate())
op_b = np.exp(1j * 0.5) * Operator(XGate())

pF = process_fidelity(op_a, op_b)

print(f'\nProcess fidelity pF = {pF}')

agF = average_gate_fidelity(op_a, op_b)

print(f'\nAverage gate fidelity agF = {agF}')

# # code below returns an error, because, of course, op_a and op_b are not states,
# # but operators (processes, gates)

# sF = state_fidelity(op_a, op_b)

# print(f'\nState fidelity sF = {sF}')

# 20 - B)

# BE VERY CAREFUL WITH THE ENDIANNESS!!!

qc = QuantumCircuit(2, 2)

qc.x(0)

# better to visualize order of measurements
qc.barrier()

qc.measure([0, 1], [0, 1])

show_figure(qc.draw("mpl"))

simulator = Aer.get_backend("qasm_simulator")
result = execute(qc, simulator, shots=1000).result()
counts = result.get_counts(qc)

print(counts)

# openqasm string

qc = QuantumCircuit(2, 2)

qc.h(0)
qc.cnot(0, 1)

qc.barrier()

qc.measure([0, 1], [0, 1])

####################################

show_figure(qc.draw("mpl"))

####################################

print(qc.qasm())

print("\n")

print(qc.qasm(formatted=True))

qasm_str = qc.qasm(filename="qasm_test")

qc_test_str = QuantumCircuit.from_qasm_str(qasm_str)

qc_test_str.draw("mpl")

qc_test_file = QuantumCircuit.from_qasm_file("qasm_test")

qc_test_file.draw("mpl")

qc = QuantumCircuit(2, 2)

qc.h(0)
qc.cnot(0, 1)

qc.barrier()

##########################

backend = Aer.get_backend("unitary_simulator")

unitary = execute(qc, backend).result().get_unitary()

unitary

fig, ax = plt.subplots(1, 2, figsize=(8, 3))

sns.heatmap(np.real(unitary), annot=True, ax=ax[0])
ax[0].set_title("Real part")

sns.heatmap(np.imag(unitary), annot=True, ax=ax[1])
ax[1].set_title("Imaginary part")

plt.tight_layout()
plt.show()

qc = QuantumCircuit(3)

qc.h(0)
qc.cnot(0, 1)
qc.cnot(0, 2)

show_figure(qc.draw("mpl"))

####################################

sv = Statevector.from_label("000")

sv = sv.evolve(qc)

print(f'Statevector:\n\n{sv.data}')

show_figure(plot_state_hinton(sv))

####################################

gate = qc.to_gate()

gate.name = "my_gate"

####################################

qc2 = QuantumCircuit(3)

qc2.append(gate, [0, 1, 2])

print("Gate in the circuit:")
show_figure(qc2.draw("mpl"))

print("Decomposed gate in the circuit:")
show_figure(qc2.decompose().draw("mpl"))

# that's more useful when "backend" is a real hardware, of course
job = execute(qc, backend)
job_monitor(job)

bell = QuantumCircuit(2, 2)
bell.h(0)
bell.cx(0, 1)

meas = QuantumCircuit(2, 2)
meas.measure([0,1], [0,1])

circ = bell.compose(meas)

show_figure(bell.draw("mpl"))

show_figure(meas.draw("mpl"))

show_figure(circ.draw("mpl"))

backend = Aer.get_backend("qasm_simulator")

job = execute(circ, backend, shots=1e5, seed_simulator=42)
results = job.result()
counts = results.get_counts()

plot_histogram(counts, title="Results", figsize=(6, 4))

simulator = Aer.get_backend("statevector_simulator")

job = execute(bell, simulator)
results = job.result()

psi = results.get_statevector(bell)

print(psi)

# "here we see that there is no information about the quantum state in the single qubit space as all vectors are zero."
# if it was a separable state, it would be shown!
show_figure(plot_bloch_multivector(psi))

show_figure(plot_state_city(psi))
show_figure(plot_state_hinton(psi))

show_figure(plot_state_qsphere(psi))

show_figure(plot_bloch_vector([1,0,0], figsize=(2, 2)))

show_figure(plot_bloch_vector([0,1,0], figsize=(2, 2)))

show_figure(plot_bloch_vector([0,0,1], figsize=(2, 2)))

# already saved my token!
# for more info: https://quantum-computing.ibm.com/lab/docs/iql/manage/account/ibmq
    
provider = IBMQ.load_account()
accountProvider = IBMQ.get_provider(hub='ibm-q')

provider.backends()

provider.backends(simulator=False)

for bcknd in provider.backends(simulator=False):
    
    print(f"\nBackend: {bcknd.name()}")
    
    backend = provider.get_backend(bcknd.name())

    show_figure(plot_gate_map(backend, figsize=(3, 3)))
    
    print(f"Coupling map:\n\n{bcknd._configuration.coupling_map}\n")
    
    print("="*50)
    print("="*50)

for bcknd in provider.backends(simulator=False):
    
    print(f"\nBackend: {bcknd.name()}")
    
    backend = provider.get_backend(bcknd.name())

    show_figure(plot_error_map(backend))
    
    print("="*50)
    print("="*50)