import warnings
from qiskit import QuantumRegister, ClassicalRegister
from qiskit import QuantumCircuit, execute, Aer, transpile, IBMQ
from qiskit.tools.monitor import job_monitor
from qiskit.circuit.library import QFT
from qiskit.visualization import plot_bloch_multivector, plot_histogram, array_to_latex
warnings.filterwarnings("ignore", category=DeprecationWarning)
import numpy as np
import pandas as pd
import math

from qiskit.quantum_info import Statevector

from scipy.stats import chi2_contingency, ttest_ind

import unittest
import hypothesis.strategies as st
from hypothesis import given, settings

pi = np.pi

# q = QuantumRegister(2)
# c = ClassicalRegister(1)
# qc2 = QuantumCircuit(q,c)
# qc2.x(1)
# qc2.h(0)
# qc2.cnot(0,1)

qc2 = QuantumCircuit(2)
# qc2.x(1)
# qc2.h(0)

# qc2.measure(q[0],c)
# qc2.measure(q[1],c)

# THIS DOESNT WORK
# qc2.h(0)
# qc2.cnot(0,1)
# qc2.h(1)
# qc2.h(0)

# qc2.x(0)
# qc2.h(0)
# qc2.cnot(0,1)
# qc2.h(1)
# qc2.h(0)

# 4 BELL STATES
# qc2.x(1)
# qc2.h(0)
# qc2.cnot(0,1)
# qc2.h(1)

qc2.h(1)
qc2.cp(pi/2,1,0)
qc2.h(0)
print(qc2) 
sv = Statevector.from_label('01')
sv.evolve(qc2)
print(sv)

backend = Aer.get_backend('aer_simulator') 
qc2.measure_all()

# sv = Statevector.from_label('01')
# sv.evolve(qc2)
# print(sv)

qc2.save_statevector()
# probs = psi.probabilities()
# print(probs)
final_state = backend.run(qc2).result().get_statevector()
print(final_state)
# display(array_to_latex(final_state, prefix="\\text{Statevector} = "))

job = execute(qc2, backend, shots=10000, memory=True)#run the circuit 1000000 times
counts = job.result().get_counts()#return the result counts
print(counts)

qc2 = QuantumCircuit(2)
# qc2.p(0.5*2*math.pi/100, 1)
qc2.h(0)
# qc2.cnot(0,1)
# qc2.x(0)
# qc2.x(1)
# qc2.h(0)
# qc2.cnot(0,1)
# qc2.h(1)
print(qc2) 
backend = Aer.get_backend('aer_simulator') 

sv1 = Statevector.from_label('00')
print(type(sv1))
wow = sv1.evolve(qc2)
probs = wow.probabilities()
probsD = wow.probabilities_dict()
print(sv1)

for values in probsD.values():
    round(values,2)

d = {key: round(values,2) for key,values in probsD.items()}
print("probs dictionary rounded",d)
print("probs for qubit1 (not rounded): ",wow.probabilities_dict([0]))
print("probs for qubit2 (not rounded): ", wow.probabilities_dict([1]))
# print("probs: ", type(probs))
# print("probs Dictionary: ", probsD)
# probs_qubit_0 = wow.probabilities([0])
# probs_qubit_1 = wow.probabilities([1])
# print('Qubit-0 probs: {}'.format(probs_qubit_0))
# print('Qubit-1 probs: {}'.format(probs_qubit_1))
# print(wow.probabilities_dict([0]))
# print(wow.probabilities_dict([1]))

for values in d.values():
    # print(values)
    if (values==0.5):
        print("true")
    else:
        print("false")

qc2.measure_all()
sv = qc2.save_statevector()
print("sv: " ,sv)
# probs = psi.probabilities()
# print(probs)
final_state = backend.run(qc2).result().get_statevector()
print("final : " ,final_state)
job = execute(qc2, backend, shots=10000, memory=True)#run the circuit 1000000 times
counts = job.result().get_counts()#return the result counts
print(counts)

def assertProbability(qc,qubitChoice: int, qubitState :str, expectedProbability):
    sv = Statevector.from_label("00")
    evl = sv.evolve(qc)
    probs = evl.probabilities_dict([qubitChoice])
    
    probsRound = {key: round(values,2) for key,values in probs.items()}

    for key,value in probsRound.items():
        if(key==qubitState):
            if(value==expectedProbability):
                print("Expected Probability present")
            else:
                raise(AssertionError("Probability not present"))
        else:
            raise(AssertionError("Probability not present"))

qc = QuantumCircuit(2)
qc.h(0)
assertProbability(qc,1,"0",1)
    

psi = Statevector.from_label('10')
print(psi)

print("statevector measurement: " ,psi.measure())
# rev = psi.reverse_qargs()
# Probabilities for measuring both qubits
probs = psi.probabilities()
print('probs: {}'.format(probs))
probsD = psi.probabilities_dict()
print("probs dictionary: ", probsD)

# Probabilities for measuring only qubit-0
probs_qubit_0 = psi.probabilities([0])
print('Qubit-0 probs: {}'.format(probs_qubit_0))
print('Qubit-0 probs: {}'.format(psi.probabilities_dict([0])))

# Probabilities for measuring only qubit-1
probs_qubit_1 = psi.probabilities([1])
print('Qubit-1 probs: {}'.format(probs_qubit_1)) 
print('Qubit-1 probs: {}'.format(psi.probabilities_dict([1])))

def measure_z(circuit, qubit_indexes):
    cBitIndex = 0
    for index in qubit_indexes:
        circuit.measure(index, cBitIndex)
        cBitIndex+=1
    return circuit

def assertEntangled(backend,qc,qubit1,qubit2,measurements_to_make,alpha = 0.05):
    if (qc.num_clbits == 0):
        qc.add_register(ClassicalRegister(2))
    elif (qc.num_clbits != 2):
        raise ValueError("QuantumCircuit classical register must be of length 2")
    
    zQuantumCircuit = measure_z(qc, [qubit1, qubit2])
    zJob = execute(zQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    zMemory = zJob.result().get_memory()
    zCounts = zJob.result().get_counts()

    
    # zDf = pd.DataFrame(columns=['q0', 'q1'])
    # for row in zCounts:
    #     zDf = zDf.append({'q0':row[0], 'q1':row[1]}, ignore_index=True)
    # print(zDf.astype(str))


    resDf = pd.DataFrame(columns=['0','1'])
    classical_qubit_index = 1
    for qubit in [qubit1,qubit2]:
        zero_amount, one_amount = 0,0
        for experiment in zCounts:
            if (experiment[2-classical_qubit_index] == '0'):
                zero_amount += zCounts[experiment]
            else:
                one_amount += zCounts[experiment]
        df = {'0':zero_amount, '1':one_amount}
        resDf = resDf.append(df, ignore_index = True)
        classical_qubit_index+=1

    resDf['0'] = resDf['0'].astype(int)
    resDf['1'] = resDf['1'].astype(int)

    print(resDf.astype(str))
    chiVal, pVal, dOfFreedom, exp = chi2_contingency(resDf)
    print("chi square value: ",chiVal,"p value: ",pVal,"expected values: ",exp)
    if(pVal<alpha):
        raise(AssertionError("states are not entangled"))
    else:
        print("states are entangled")


backend = Aer.get_backend('aer_simulator')
qc3 = QuantumCircuit(2)
qc3.h(0)
# qc3.cnot(0,1)
# qc3.x(0)
# qc3.h(0)
# qc3.cnot(0,1)
# qc3.h(1)
# qc3.h(0)
assertEntangled(backend,qc3,0,1,100000,0.05)

# Completed but more testing required
def assertEntangled(backend,qc,qubits_to_assert,measurements_to_make,alpha = 0.05):
    # makes sure qubits_to_assert is a list
    if (not isinstance(qubits_to_assert, list)):
        qubits_to_assert = [qubits_to_assert]

    ## classical register must be of same length as amount of qubits to assert
    ## if there is no classical register add them according to length of qubit list
    if (qc.num_clbits == 0):
        qc.add_register(ClassicalRegister(len(qubits_to_assert)))
    elif (len(qubits_to_assert) != 2):
        raise ValueError("QuantumCircuit classical register must be of length 2")
    
    zQuantumCircuit = measure_z(qc, qubits_to_assert)
    zJob = execute(zQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    zMemory = zJob.result().get_memory()
    print(zMemory)
    q1=[]
    q2=[]

    print("lol ",qubits_to_assert[0])
    print(qubits_to_assert[1])
    newDict = dict.fromkeys(['qubit1','qubit2'])
    newDict = {'qubit1': qubits_to_assert[0],'qubit2':qubits_to_assert[1]}
    print("new dict", newDict)
    # if(qubits_to_assert[0]==0 and qubits_to_assert[1]==0):
    #     qubits_to_assert[1]
    classicalQubitIndex = 1
    for qubit in newDict.keys():
        print("this qubit:",qubit)
        for measurement in zMemory:
            print("this measurement", measurement)
            if (measurement[2-classicalQubitIndex] == '0'):
                print("measure: ",measurement[2-classicalQubitIndex],"also: qubittoassert0",qubits_to_assert[0],"and qubittoassert1: ",qubits_to_assert[1])    
                if(qubit=='qubit1'):
                    q1.append(measurement[2-classicalQubitIndex])
                    print("Added to q1 for measure0:", measurement[2-classicalQubitIndex])
                else:
                    q2.append(measurement[2-classicalQubitIndex])
                    print("Added to q2 for measure0:", measurement[2-classicalQubitIndex])
            else:
                print("measureOTHER: ",measurement[2-classicalQubitIndex], "also: qubittoassert0",qubits_to_assert[0],"and qubittoassert1: ",qubits_to_assert[1]) 
                if(qubit=='qubit1'):
                    q1.append(measurement[2-classicalQubitIndex])
                    print("Added to q1 for measure1:", measurement[2-classicalQubitIndex])
                else:
                    q2.append(measurement[2-classicalQubitIndex])    
                    print("Added to q2 for measure1:", measurement[2-classicalQubitIndex])               
        classicalQubitIndex+=1



    print("Q1: ",q1)
    print("Q2: ",q2)
    measDict = dict.fromkeys(['qubit1','qubit2'])
    measDict = {'qubit1': q1,'qubit2':q2}
    measDf1 = pd.DataFrame.from_dict(measDict,orient='index')
    # measDf = pd.DataFrame.from_dict(measDict)
    # print(measDf)
    measDf12=measDf1.transpose()
    print(measDf12)
    # df = pd.DataFrame.from_dict(a, orient='index')
    # df = df.transpose()

    ct = pd.crosstab(measDf12.qubit1,measDf12.qubit2)

    chiVal, pVal, dOfFreedom, exp = chi2_contingency(ct)
    print("chi square value: ",chiVal,"p value: ",pVal,"expected values: ",exp)
    if(pVal>alpha):
        raise(AssertionError("states are not entangled"))
    else:
        print("states are entangled")
        
backend = Aer.get_backend('aer_simulator')
qr = QuantumRegister(2)
cr=ClassicalRegister(2)
qc3 = QuantumCircuit(qr,cr)
qc3.x(1)

# # qc3.x(1)
# # qc3.x(0)
qc3.h(0)

# qc3.cnot(0,1)
# qc3.h(0)
# qc3.cnot(0,1)
# qc3.x(1)
# qc3.rx(np.pi/2,qr[0])
# qc3.p(10*2*math.pi/100, 0)
# qc3.p(0.5*2*math.pi/100, 1)
print(qc3)
assertEntangled(backend,qc3,[0,1],20,0.05)



# circuit = QuantumCircuit(2)
# circuit.initialize([0, 1/np.sqrt(2), -1.j/np.sqrt(2), 0], circuit.qubits)

# assertEntangled(backend,circuit,[0,1],10,0.05)



def measure_x(circuit, qubitIndexes):
    cBitIndex = 0
    for index in qubitIndexes:
        circuit.h(index)
        circuit.measure(index, cBitIndex)
        cBitIndex+=1
    return circuit

def measure_y(circuit, qubit_indexes):
    cBitIndex = 0
    for index in qubit_indexes:
        circuit.sdg(index)
        circuit.h(index)
        circuit.measure(index, cBitIndex)
        cBitIndex+=1
    return circuit

## assert that qubits are equal
def assertEqual(backend, quantumCircuit, qubits_to_assert, measurements_to_make, alpha):
    ## needs to make at least 2 measurements, one for x axis, one for y axis
    ## realistically we need more for any statistical significance
    if (measurements_to_make < 2):
        raise ValueError("Must make at least 2 measurements")

    # makes sure qubits_to_assert is a list
    if (not isinstance(qubits_to_assert, list)):
        qubits_to_assert = [qubits_to_assert]

    ## classical register must be of same length as amount of qubits to assert
    ## if there is no classical register add them according to length of qubit list
    if (quantumCircuit.num_clbits == 0):
        quantumCircuit.add_register(ClassicalRegister(len(qubits_to_assert)))
    elif (len(qubits_to_assert) != 2):
        raise ValueError("QuantumCircuit classical register must be of length 2")

    ## divide measurements to make by 3 as we need to run measurements twice, one for x and one for y
    measurements_to_make = measurements_to_make // 3

    ## copy the circit and set measurement to y axis
    yQuantumCircuit = measure_y(quantumCircuit.copy(), qubits_to_assert)

    ## measure the x axis
    xQuantumCircuit = measure_x(quantumCircuit.copy(), qubits_to_assert)

    ## measure the z axis
    zQuantumCircuit = measure_z(quantumCircuit, qubits_to_assert)

    ## get y axis results
    yJob = execute(yQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    yMemory = yJob.result().get_memory()
    yCounts = yJob.result().get_counts()

    ## get x axis results
    xJob = execute(xQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    xMemory = xJob.result().get_memory()
    xCounts = xJob.result().get_counts()

    ## get z axis results
    zJob = execute(zQuantumCircuit, backend, shots=measurements_to_make, memory=True)
    zMemory = zJob.result().get_memory()
    zCounts = zJob.result().get_counts()

    resDf = pd.DataFrame(columns=['0','1','+','i','-','-i'])

    classical_qubit_index = 1
    for qubit in qubits_to_assert:
        zero_amount, one_amount, plus_amount, i_amount, minus_amount, minus_i_amount = 0,0,0,0,0,0
        for experiment in xCounts:
            if (experiment[2-classical_qubit_index] == '0'):
                plus_amount += xCounts[experiment]
            else:
                minus_amount += xCounts[experiment]
        for experiment in yCounts:
            if (experiment[2-classical_qubit_index] == '0'):
                i_amount += yCounts[experiment]
            else:
                minus_i_amount += yCounts[experiment]
        for experiment in zCounts:
            if (experiment[2-classical_qubit_index] == '0'):
                zero_amount += zCounts[experiment]
            else:
                one_amount += zCounts[experiment]
        df = {'0':zero_amount, '1':one_amount,
              '+':plus_amount, 'i':i_amount,
              '-':minus_amount,'-i':minus_i_amount}
        resDf = resDf.append(df, ignore_index = True)
        classical_qubit_index+=1

    ## convert the columns to a strict numerical type
    resDf['+'] = resDf['+'].astype(int)
    resDf['i'] = resDf['i'].astype(int)
    resDf['-'] = resDf['-'].astype(int)
    resDf['-i'] = resDf['-i'].astype(int)
    resDf['0'] = resDf['0'].astype(int)
    resDf['1'] = resDf['1'].astype(int)
    print(resDf.astype(str))
    # print(z1)
    arr = resDf.to_numpy()
    q1Vals = arr[0, 0:] # Stores measurements across all axes for the 1st qubit
    print("Measurements for qubit1: ", q1Vals)
    q2Vals = arr[1, 0:] # Stores measurements across all axes for the 2nd qubit
    print("Measurements for qubit2: ", q2Vals)

    tTest, pValue = ttest_ind(q1Vals, q2Vals, alternative = 'two-sided') # Apply t test
    print("stat: ",tTest, "pValue: ", pValue)
    if pValue > alpha:
        print("The two qubits are equal (fail to reject null hypothesis) ")
    else:
        print("There is a significant difference between the two qubits (reject null hypothesis)")

qc = QuantumCircuit(2)
# qc.initialize([0, 1/np.sqrt(2), -1.j/np.sqrt(2), 0], qc.qubits)
# qc.h(0)
# qc.x(1)
# qc.p(0.5*2*math.pi/100, 1)
# qc.h(0)
# qc.h(1)
# qc.p(10*2*math.pi/100, 0)
# qc.p(20*2*math.pi/100, 1)
assertEqual(backend, qc, [0,0], 300000, 0.01)

qc = QuantumCircuit(2,2)
qc.h(0)
qc.h(1)
qc.p(0.5*2*math.pi/100, 1)
# qc.cnot(0,1)

zQuantumCircuit = measure_z(qc, [0,1])
zJob = execute(zQuantumCircuit, backend, shots=100000, memory=True)
zMemory = zJob.result().get_memory()
zCounts = zJob.result().get_counts()

xQuantumCircuit = measure_x(qc, [0,1])
xJob = execute(xQuantumCircuit, backend, shots=100000, memory=True)
xMemory = xJob.result().get_memory()
xCounts = xJob.result().get_counts()

yQuantumCircuit = measure_y(qc, [0,1])
yJob = execute(yQuantumCircuit, backend, shots=100000, memory=True)
yMemory = yJob.result().get_memory()
yCounts = yJob.result().get_counts()

print("z axis : ", zCounts)
print("x axis : ", xCounts)
print("y axis : ", yCounts)

resDf = pd.DataFrame(columns=['0','1','+','i','-','-i'])

classical_qubit_index = 1
for qubit in [0,1]:
    zero_amount, one_amount, plus_amount, i_amount, minus_amount, minus_i_amount = 0,0,0,0,0,0
    for experiment in xCounts:
        if (experiment[2-classical_qubit_index] == '0'):
            plus_amount += xCounts[experiment]
        else:
            minus_amount += xCounts[experiment]
    for experiment in yCounts:
        if (experiment[2-classical_qubit_index] == '0'):
            i_amount += yCounts[experiment]
        else:
            minus_i_amount += yCounts[experiment]
    for experiment in zCounts:
        if (experiment[2-classical_qubit_index] == '0'):
            zero_amount += zCounts[experiment]
        else:
            one_amount += zCounts[experiment]
    df = {'0':zero_amount, '1':one_amount,
            '+':plus_amount, 'i':i_amount,
            '-':minus_amount,'-i':minus_i_amount}
    resDf = resDf.append(df, ignore_index = True)
    classical_qubit_index+=1

print(resDf.astype(str))

lst = [["Ajay", 70], ["Manish", 92], ["Arjun", 42]]
df = pd.DataFrame(lst, columns=["Name", "Marks"], index=["Student1","Student2","Student3"])
df

def getList(dict):
    return list(dict.keys())

qubitVal = {0: [1,0], 1: [0,1]}

values = getList(counts)
if(len(values) == 1):
    print("They are not entangled")
elif(len(values) == 4):
    firstStr = values[0]
    secondStr = values[1]
    thirdStr = values[2]
    fourthStr = values[3]
    print(firstStr,secondStr,thirdStr,fourthStr)
    
    first1 = int(firstStr[0])
    first2 = int(firstStr[1])
    second1 = int(secondStr[0])
    second2 =int(secondStr[1])
    third1 = int(thirdStr[0])
    third2 = int(thirdStr[1])
    fourth1 = int(fourthStr[0])
    fourth2 = int(fourthStr[1])
    firstArr = np.array([qubitVal[first1], qubitVal[first2]])
    secondArr = np.array([qubitVal[second1], qubitVal[second2]])
    thirdArr = np.array([qubitVal[third1], qubitVal[third2]])
    fourthArr = np.array([qubitVal[fourth1], qubitVal[fourth2]])

    firstTensor = np.tensordot(firstArr[0], firstArr[1], axes=0)
    secondTensor = np.tensordot(secondArr[0], secondArr[1], axes=0)
    thirdTensor = np.tensordot(thirdArr[0], thirdArr[1], axes=0)
    fourthTensor = np.tensordot(fourthArr[0], fourthArr[1], axes=0)
    print("Tensors: ",firstTensor, " HI ",secondTensor,thirdTensor,fourthTensor)
    column1 = np.add(firstTensor,secondTensor)
    column2 = np.add(thirdTensor, fourthTensor)
    print("AB: ",column1," CD: ",column2)
    column = np.add(column1,column2)
    print(column)

else:
    firstStr = values[0]
    secondStr = values[1]
    print(firstStr)
    print(secondStr)
    first1 = int(firstStr[0])
    first2 = int(firstStr[1])
    second1 = int(secondStr[0])
    second2 = int(secondStr[1])

    firstArr = np.array([qubitVal[first1], qubitVal[first2]])
    secondArr = np.array([qubitVal[second1], qubitVal[second2]])
    firstTensor= np.tensordot(firstArr[0], firstArr[1], axes=0)
    secondTensor = np.tensordot(secondArr[0], secondArr[1], axes=0)
    print("first: ",firstTensor)
    print("second: ",secondTensor)
    print("final: ")
    column = np.add(firstTensor,secondTensor)
    print(column)

abVal = column[0]
cdVal = column[1]
aVal = abVal[0]
bVal = abVal[1]
cVal = cdVal[0]
dVal = cdVal[1]

if(aVal*dVal!=bVal*cVal):
    print("They are entangled")
else:
    print("Not Entangled")

def getList(dict):
    return list(dict.keys())

# NOTE: Checks if a qubit value is 0 then return a matrix with [1,0] similarly for 1 with [0,1]
qubitVal = {0: [1,0], 1: [0,1]}
# NOTE: Upon analysis the measurement returned by the QC are also present in the bell state of the qc
# NOTE: For example if the counts output 00 and 01 
values = getList(counts) # NOTE: Returns the measurement counts of the circuit and takes the key values ie 00 and 01 and converts it to a list using a function I created
if(len(values) == 1):
    print("They are not entangled")
else:
    # NOTE: Stores the first and second values as strings returned by counts for example: 00 and 01
    firstStr = values[0]
    secondStr = values[1]
    # NOTE: Seperates the digits and stores them as integers 
    first1 = int(firstStr[0])
    first2 = int(firstStr[1])
    second1 = int(secondStr[0])
    second2 = int(secondStr[1])
    # NOTE: Creates a 2x2 matrix to store ___
    firstArr = np.array([qubitVal[first1], qubitVal[first2]])
    secondArr = np.array([qubitVal[second1], qubitVal[second2]])
    # if(second1==1): 
    #     subtract = True
    # NOTE: Performs the tensor product for the ___ for both the first and second values
    firstTensor= np.tensordot(firstArr[0], firstArr[1], axes=0)
    print("first: ", firstArr, "first array first position: ", firstArr[0],"first array second position: ", firstArr[1])
    print("second: ",secondArr,"second array first position: ", secondArr[0], "second array second position: ",secondArr[1])
    secondTensor = np.tensordot(secondArr[0], secondArr[1], axes=0)
    # print("first: ",firstTensor)
    # print("second: ",secondTensor)
    # NOTE: Condition to check if counts returns 4 measurements in which case we repeat the process for the 3rd and 4th measurements
    if(len(values) == 4):
        thirdStr = values[2]
        fourthStr = values[3]
        third1 = int(thirdStr[0])
        third2 = int(thirdStr[1])
        fourth1 = int(fourthStr[0])
        fourth2 = int(fourthStr[1])
        thirdArr = np.array([qubitVal[third1], qubitVal[third2]])
        fourthArr = np.array([qubitVal[fourth1], qubitVal[fourth2]])

        thirdTensor = np.tensordot(thirdArr[0], thirdArr[1], axes=0)
        fourthTensor = np.tensordot(fourthArr[0], fourthArr[1], axes=0)
        print(f"Tensors: {firstTensor}\n ,{secondTensor}\n,{thirdTensor}\n,{fourthTensor}\n")
        column1 = np.add(firstTensor,secondTensor)
        column2 = np.add(thirdTensor, fourthTensor)
        print("AB: ",column1," CD: ",column2)
        column = np.add(column1,column2)
        print(column)
    else:
        print("first: ",firstTensor)
        print("second: ",secondTensor)
        print("final: ")
        # NOTE: adds the two tensorproducts to give our column vector
        # if(subtract==True):
        #     column = np.subtract(firstTensor,secondTensor)
        # else: 
        column = np.add(firstTensor,secondTensor)
        print(column)

# NOTE: Sets the value for our A,B,C,D values
abVal = column[0]
cdVal = column[1]
aVal = abVal[0]
bVal = abVal[1]
cVal = cdVal[0]
dVal = cdVal[1]

# NOTE: Checks our condition
if(aVal*dVal!=bVal*cVal):
    print("They are entangled")
else:
    print("Not Entangled")

def getList(dict):
    return list(dict.keys())

# NOTE: Checks if a qubit value is 0 then return a matrix with [1,0] similarly for 1 with [0,1]
qubitVal = {0: [1,0], 1: [0,1]}
# NOTE: Upon analysis the measurement returned by the QC are also present in the bell state of the qc
# NOTE: For example if the counts output 00 and 01 
values = getList(counts) # NOTE: Returns the measurement counts of the circuit and takes the key values ie 00 and 01 and converts it to a list using a function I created
if(len(values) == 1):
    print("They are not entangled")
else:
    # NOTE: Stores the first and second values as strings returned by counts for example: 00 and 01
    firstStr = values[0]
    secondStr = values[1]
    # NOTE: Seperates the digits and stores them as an integer based on our previous example first1 woul store 0 and first2 would store 0
    first1 = int(firstStr[0])
    first2 = int(firstStr[1])
    second1 = int(secondStr[0])
    second2 = int(secondStr[1])
    # NOTE: Creates a 2x2 matrix to store qubit values ex: first Arr would be [[1,0],[1,0]]
    firstArr = np.array([qubitVal[first1], qubitVal[first2]])
    secondArr = np.array([qubitVal[second1], qubitVal[second2]])
    # NOTE: Performs the tensor product for the ___ for both the first and second values ex: firstTensor is the tensor product of [1,0] and [1,0] 
    firstTensor= np.tensordot(firstArr[0], firstArr[1], axes=0)
    secondTensor = np.tensordot(secondArr[0], secondArr[1], axes=0)
    # NOTE: Condition to check if counts returns 4 measurements in which case we repeat the process for the 3rd and 4th measurements
    if(len(values) == 4):
        thirdStr = values[2]
        fourthStr = values[3]
        third1 = int(thirdStr[0])
        third2 = int(thirdStr[1])
        fourth1 = int(fourthStr[0])
        fourth2 = int(fourthStr[1])
        thirdArr = np.array([qubitVal[third1], qubitVal[third2]])
        fourthArr = np.array([qubitVal[fourth1], qubitVal[fourth2]])

        thirdTensor = np.tensordot(thirdArr[0], thirdArr[1], axes=0)
        fourthTensor = np.tensordot(fourthArr[0], fourthArr[1], axes=0)
        column1 = np.add(firstTensor,secondTensor)
        column2 = np.add(thirdTensor, fourthTensor)
        column = np.add(column1,column2)
        print(column)
    else:
        # NOTE: adds the two tensorproducts to give our column vector
        column = np.add(firstTensor,secondTensor)
        print(column)

# NOTE: Sets the value for our A,B,C,D values
abVal = column[0]
cdVal = column[1]
aVal = abVal[0]
bVal = abVal[1]
cVal = cdVal[0]
dVal = cdVal[1]

# NOTE: Checks our condition
if(aVal*dVal!=bVal*cVal):
    print("They are entangled")
else:
    print("Not Entangled")


# no constructor
# usee gate 