import copy
import random
from itertools import combinations, groupby

import networkx as nx
import numpy as np
import scipy.sparse.linalg as sla
from qiskit.opflow import I, Z


def get_graph(nodes, seedin):
    """
    Generate networkX graph where each node is connected to all other nodes.
    The weights of the connections are random and uniformly distributed between -1 to 1
    """
    random.seed(seedin)

    G = nx.Graph()
    for nn in range(0, nodes):
        G.add_nodes_from([(nn, {"weight": 0})])
        # G.add_nodes_from([(nn)])

    edges = combinations(range(nodes), 2)
    for _, node_edges in groupby(edges, key=lambda x: x[0]):
        for e in node_edges:
            G.add_edges_from([(e[0], e[1], {"weight": random.uniform(-1, 1)})])

    return G


def get_nReg_MaxCut_graph(n, nodes, seedin):
    """
    Generate networkX graph where each node has n connections to a random slection of
    the other nodes.
    The weights of the connections are random and uniformly distributed between -1 to 1
    """
    random.seed(seedin)

    G = nx.Graph()
    for nn in range(0, nodes):
        G.add_nodes_from([(nn)])

    F = nx.random_regular_graph(n, nodes)
    for e in F.edges:
        G.add_edges_from([(e[0], e[1], {"weight": random.uniform(-1, 1)})])

    return G


def get_rand_QUBO(nodes, seedin):
    """
    Generate random QUBO matrix
    The weights of the connections are random and uniformly distributed between -1 to 1
    """
    random.seed(seedin)

    QUBO = np.zeros((nodes, nodes), dtype=float)
    for n in range(nodes):
        QUBO[n][n] = random.uniform(-1, 1)
        for m in range(n + 1, nodes):
            QUBO[n][m] = random.uniform(-1, 1)

    return QUBO


def get_nReg_MaxCut_QUBO(n, nodes, seedin):
    """
    Generate QUBO matrix where each node has n connections to a random slection of
    the other nodes.
    The weights of the connections are random and uniformly distributed between -1 to 1
    """

    G = get_nReg_MaxCut_graph(n, nodes, seedin)

    return get_QUBO(G)


def get_rand_PauliSumOp(nodes, seedin):
    """
    Generate random PauliSumOp
    The weights of the connections are random and uniformly distributed between -1 to 1
    """

    G = get_graph(nodes, seedin)

    return get_PauliSumOp(G)


def get_nReg_MaxCut_PauliSumOp(n, nodes, seedin):
    """
    Generate PauliSumOp where each node has n connections to a random slection of
    the other nodes.
    The weights of the connections are random and uniformly distributed between -1 to 1
    """

    G = get_nReg_MaxCut_graph(n, nodes, seedin)

    return get_PauliSumOp(G)


def get_PauliSumOp(G):

    Ham = None

    for nn in list(G.nodes()):
        try:
            G.nodes[nn]["weight"]
        except:
            # if not specified, node weights will be considered zero
            G.nodes[nn]["weight"] = 0

        if G.nodes[nn]["weight"] != 0:

            if nn == 0:
                temp = Z
            else:
                temp = I

            for pp in range(0, nn - 1):
                temp = temp ^ I

            if nn != 0:
                temp = temp ^ Z

            for pp in range(nn, len(G.nodes()) - 1):
                temp = temp ^ I

            if Ham is None:
                Ham = G.nodes[nn]["weight"] * temp
            else:
                Ham = Ham + G.nodes[nn]["weight"] * temp

    for pair in list(G.edges()):
        try:
            G.edges[pair]["weight"]
        except:
            # if not specified, edge weight will be set equal to 1.0
            G.edges[pair]["weight"] = 1.0

        if pair[0] == 0:
            temp = Z
        else:
            temp = I

        for pp in range(0, pair[0] - 1):
            temp = temp ^ I

        if pair[0] != 0:
            temp = temp ^ Z

        for pp in range(pair[0], pair[1] - 1):
            temp = temp ^ I

        temp = temp ^ Z

        for pp in range(pair[1], len(G.nodes()) - 1):
            temp = temp ^ I

        if Ham is None:
            Ham = G.edges[pair]["weight"] * temp
        else:
            Ham = Ham + G.edges[pair]["weight"] * temp

    return Ham


def get_QUBO(G):

    nodes = len(G.nodes)
    Ising_Mat = np.zeros((nodes, nodes), dtype=float)

    for nn in list(G.nodes()):
        try:
            G.nodes[nn]["weight"]
        except:
            # if not specified, node weights will be considered zero
            G.nodes[nn]["weight"] = 0

        Ising_Mat[nn][nn] = G.nodes[nn]["weight"]

    for pair in list(G.edges()):
        try:
            G.edges[pair]["weight"]
        except:
            # if not specified, edge weight will be set equal to 1.0
            G.edges[pair]["weight"] = 1.0

        Ising_Mat[pair[0]][pair[1]] = G.edges[pair]["weight"]

    return Ising_Mat


def get_Ising(G):

    Q = get_QUBO(G)
    Ising_mat = convert_QUBO_to_Ising(Q)

    return Ising_mat


def get_Ham_from_graph(G):

    Ham = []
    for nn in list(G.nodes()):
        try:
            G.nodes[nn]["weight"]
        except:
            # if not specified, node weights will be considered zero
            G.nodes[nn]["weight"] = 0

        if G.nodes[nn]["weight"] != 0:
            if nn == 0:
                temp = "Z"
            else:
                temp = "I"

            for pp in range(0, nn - 1):
                temp = temp + "I"

            if nn != 0:
                temp = temp + "Z"

            for pp in range(nn, len(G.nodes()) - 1):
                temp = temp + "I"

            Ham.append((G.nodes[nn]["weight"], temp, nn))

    for pair in list(G.edges()):
        try:
            G.edges[pair]["weight"]
        except:
            # if not specified, edge weight will be set equal to 1.0
            G.edges[pair]["weight"] = 1.0

        if pair[0] == 0:
            temp = "Z"
        else:
            temp = "I"

        for pp in range(0, pair[0] - 1):
            temp = temp + "I"

        if pair[0] != 0:
            temp = temp + "Z"

        for pp in range(pair[0], pair[1] - 1):
            temp = temp + "I"

        temp = temp + "Z"

        for pp in range(pair[1], len(G.nodes()) - 1):
            temp = temp + "I"

        Ham.append((G.edges[pair]["weight"], temp, pair))

    return Ham


def get_Ham_from_PauliSumOp(H_pauliSum):

    Ham = []
    for nn in range(len(H_pauliSum._primitive)):

        op_str = str(H_pauliSum._primitive[nn]._pauli_list[0])

        pair = []
        for l in range(len(op_str)):
            if op_str[l] == "Z":
                pair.append(l)

        if len(pair) > 1:
            pair = tuple(pair)
        else:
            pair = pair[0]

        Ham.append((np.real(H_pauliSum._primitive[nn]._coeffs[0]), op_str, pair))

    return Ham


def get_Ham_from_QUBO(QUBO_mat):

    nodes = np.size(QUBO_mat[0])

    Ham = []
    for nn in range(nodes):

        if QUBO_mat[nn][nn] != 0:
            if nn == 0:
                temp = "Z"
            else:
                temp = "I"

            for pp in range(0, nn - 1):
                temp = temp + "I"

            if nn != 0:
                temp = temp + "Z"

            for pp in range(nn, len(QUBO_mat) - 1):
                temp = temp + "I"

            Ham.append((QUBO_mat[nn][nn], temp, nn))

    for p1 in range(nodes):
        for p2 in range(p1 + 1, nodes):

            if np.abs(QUBO_mat[p1][p2]) > 1e-5:
                if p1 == 0:
                    temp = "Z"
                else:
                    temp = "I"

                for pp in range(0, p1 - 1):
                    temp = temp + "I"

                if p1 != 0:
                    temp = temp + "Z"

                for pp in range(p1, p2 - 1):
                    temp = temp + "I"

                temp = temp + "Z"

                for pp in range(p2, nodes - 1):
                    temp = temp + "I"

                pair = tuple([p1, p2])
                Ham.append((QUBO_mat[p1][p2], temp, pair))

    return Ham


def convert_QUBO_to_Ising(QUBO_mat):

    nodes = np.size(QUBO_mat[0])
    Ising_mat = QUBO_mat / 4

    for nn in range(nodes):
        Ising_mat[nn][nn] = QUBO_mat[nn][nn] / 2 + sum(QUBO_mat[nn][nn + 1 :] / 4)

    for nn in range(nodes):
        Ising_mat[nn][nn] += sum(QUBO_mat[:nn, nn] / 4)

    return Ising_mat


def get_graph_from_Ham(H):

    G = nx.Graph()
    for nn in range(len(H)):
        Num_z = H[nn][1].count("Z")
        if Num_z > 2:
            print(
                "Error: cannot create networkX graph. Hamiltonian has more than pairwise connections"
            )
        elif Num_z == 1:
            ind = H[nn][1].find("Z")
            G.add_nodes_from([(ind, {"weight": H[nn][0]})])
        else:
            G.add_edges_from([(H[nn][2][0], H[nn][2][1], {"weight": H[nn][0]})])

    return G


def get_PauliSumOp_from_Ham(H):

    Ham = None

    for nn in range(len(H)):

        if H[nn][1][0] == "Z":
            temp = 2 * Z + I
        else:
            temp = I
        for mm in range(1, len(H[nn][1])):
            if H[nn][1][mm] == "Z":
                temp = temp ^ 2 * Z + I
            else:
                temp = temp ^ I

        if Ham is None:
            Ham = H[nn][0] * temp
        else:
            Ham = Ham + H[nn][0] * temp

    return Ham


def get_exact_en(G, nodes):

    if nodes > 24:
        Egs_exact = "!!problem too big for exact solution!!"
    else:

        edges = np.zeros((len(G.edges()), 2), dtype=int)
        we = np.zeros(len(G.edges()), dtype=float)
        iter = 0
        for x in G.edges():
            edges[iter, 0] = x[0]
            edges[iter, 1] = x[1]
            we[iter] = G.edges[x]["weight"]
            iter = iter + 1

        wn = np.zeros(len(G.nodes()), dtype=float)
        iter = 0
        for i in G.nodes():
            try:
                wn[iter] = G.nodes[i]["weight"]
            except:
                wn[iter] = 0
            iter = iter + 1

        def TensorProd(A):

            out = np.kron(A[0], A[1])
            for oo in range(2, len(A)):
                out = np.kron(out, A[oo])

            return out

        sz = [[1.0, 0.0], [0.0, 0.0]]

        A0 = []
        for n in range(0, nodes):
            A0.append(np.eye(2))

        Ham = np.zeros((np.power(2, nodes), np.power(2, nodes)), dtype=float)
        for p in range(0, nodes):
            A = copy.deepcopy(A0)
            A[p] = sz
            Ham += wn[p] * TensorProd(A)

        for p in range(0, len(G.edges())):
            A = copy.deepcopy(A0)
            A[edges[p][0]] = sz
            A[edges[p][1]] = sz
            Ham += we[p] * TensorProd(A)

        Egs_exact, V = sla.eigs(Ham, 1, which="SR")
        Egs_exact = np.real(Egs_exact[0])

        # Egs_exact = NumPyMinimumEigensolver().compute_minimum_eigenvalue(G).eigenvalue

    return Egs_exact
