# Copyright (c) 2021 Institute for Quantum Computing, Baidu Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Use MBQC-QAOA algorithm to solve MaxCut problem and compare the solution with the circuit model
"""

from time import perf_counter
from sympy import symbols
from networkx import draw, Graph, circular_layout
import matplotlib.pyplot as plt
from paddle import seed, optimizer
from paddle_quantum.mbqc.QAOA.qaoa import get_solution_string, MBQC_QAOA_Net, Circuit_QAOA_Net

__all__ = [
    "is_graph_valid",
    "plot_graph",
    "graph_to_poly",
    "plot_solution",
    "mbqc_maxcut"
]


def is_graph_valid(graph):
    r"""检查输入的图是否符合规范。

    为了规范输入标准，我们约定输入的图没有孤立节点，即每个节点至少有边与之相连。图的节点编号为连续的自然数 ``"[1, ..., n]"``。

    Args:
       graph (list): 输入的图 ``"G = [V, E]"``，其中，``"V"`` 为节点集合，``"E"`` 为边集合

   代码示例:

    .. code-block:: python

        from paddle_quantum.mbqc.QAOA.maxcut import is_graph_valid
        V = [1,2,3,4]
        E = [(1,2), (2, 3)]
        G = [V, E]
        is_graph_valid(G)

    ::

        ValueError: input graph is not valid!
        The input graph is not valid: there is at least one isolated vertex.
   """
    # Obtain flat list
    V = graph[0]
    E = graph[1]
    flat_list = []
    for edge in E:
        flat_list += list(edge)
    # Check if there are isolated vertices
    if len(set(V).difference(flat_list)) != 0:
        print("The input graph is not valid: there is at least one isolated vertex.")
        raise ValueError("input graph is not valid!")
    else:
        print("The input graph is valid.")


def plot_graph(graph, title, pos=None, node_color=None, style=None):
    r"""定义画图函数。

    Args:
        graph (list): 输入的图 ``G = [V, E]``，其中，``V`` 为节点集合，``E`` 为边集合
        title (str): 画图对应的标题
        pos (dict): 图中各个节点的坐标，以字典的形式输入，例如：{'G': (0, 1)}
        node_color (list): 节点的颜色，与节点的标签的顺序一一对应，例如：['blue', 'red'] 对应节点 ['G1', 'G2']
        style (list): 边的样式，与边的标签的顺序一一对应，例如：['solid', 'solid'] 对应边[('G1', 'G2'), ('G3', 'G4')]

    代码示例:

    .. code-block:: python

        from paddle_quantum.mbqc.QAOA.maxcut import plot_graph
        V = ['0', '1', '2', '3']
        E = [('0', '1'), ('1', '2'), ('2', '3'), ('3', '0')]
        G = [V, E]
        node_color = ['blue', 'yellow', 'red', 'black']
        style = ['-', '--', ':', 'solid']
        pos = {'0': (0, 1),'1': (1, 1), '2': (1, 0), '3': (0, 0)}
        title = 'A demo of "plot_graph"'
        plot_graph(G, title, pos, node_color, style)
    """
    # Obtain graph
    V = graph[0]
    E = graph[1]
    qubit_num = len(V)
    G = Graph()
    G.add_nodes_from(V)
    G.add_edges_from(E)

    # Open the plot figure
    plt.figure()
    plt.ion()
    plt.cla()
    plt.title(title)
    # Set parameters for nodes
    pos = circular_layout(G) if pos is None else pos
    node_color = ["blue" for _ in list(range(qubit_num))] if node_color is None else node_color
    style = ["solid" for (_, _) in E] if style is None else style
    options = {
        "with_labels": True,
        "font_size": 20,
        "font_weight": "bold",
        "font_color": "white",
        "node_size": 2000,
        "width": 2
    }

    # Draw the graph
    draw(G=G, pos=pos, node_color=node_color, style=style, **options)
    ax = plt.gca()
    ax.margins(0.20)
    plt.axis("off")
    plt.pause(1)
    # plt.ioff()
    # plt.show()


def graph_to_poly(graph):
    r"""将图转化为对应 PUBO 问题的目标多项式。

    为了代码规范，我们要求输入的图的节点编号为 ``"[1,...,n]"``。

    Args:
        graph (list): 用户输入的图，图的形式为 ``"[V, E]"``， ``"V" `` 为点集合， ``"E"`` 为边集合

    Returns:
        list: 列表第一个元素为多项式的变量个数，第二个元素为符号化的目标多项式

    代码示例：

    .. code-block:: python

        from paddle_quantum.mbqc.QAOA.maxcut import graph_to_poly
        graph = [[1, 2, 3, 4], [(1, 2), (2, 3), (3, 4), (4, 1)]]
        polynomial = graph_to_poly(graph)
        print("Corresponding objective polynomial of the graph is: \r\n", polynomial)

    ::

        Corresponding objective polynomial of the graph is:
        [4, -2*x_1*x_2 - 2*x_1*x_4 + 2*x_1 - 2*x_2*x_3 + 2*x_2 - 2*x_3*x_4 + 2*x_3 + 2*x_4]
    """
    # Get the vertices and edges
    V = graph[0]
    E = graph[1]
    qubit_num = len(V)

    # Set symbol variables
    vars_x = {i: symbols('x_' + str(i)) for i in range(1, qubit_num + 1)}

    # Get the objective polynomial
    poly_symbol = 0
    for edge in E:
        poly_symbol += vars_x[edge[0]] + vars_x[edge[1]] - 2 * vars_x[edge[0]] * vars_x[edge[1]]
    # Set the polynomial
    polynomial = [qubit_num, poly_symbol]
    return polynomial


def plot_solution(graph, string):
    r"""画出对应最大割问题的解。

    Args:
        graph (list): 用户输入的图，图的形式为 ``"[V, E]"``， ``"V"`` 为点集合， ``"E"`` 为边集合
        string (str): 得到的最终解对应的比特串，列如：``"010101"`` ...

    代码示例:

    .. code-block:: python

        from paddle_quantum.mbqc.QAOA.maxcut import plot_solution
        V = [0, 1, 2, 3]
        E = [(0, 1), (1, 2), (2, 3), (3, 0)]
        G = [V, E]
        plot_solution(G, '1010')
    """
    # Plot the figure of bitstring using matplotlib and networkx
    V = graph[0]
    E = graph[1]
    n = len(V)
    title = "MaxCut solution"
    node_cut = ["blue" if string[v - 1] == "1" else "red" for v in list(range(1, n + 1))]
    edge_cut = ["solid" if string[u - 1] == string[v - 1] else "dashed" for (u, v) in E]
    plot_graph(graph=graph, title=title, node_color=node_cut, style=edge_cut)


def mbqc_maxcut(GRAPH, DEPTH, SEED, LR, ITR, EPOCH, SHOTS=1024):
    r"""定义 MBQC 模型下的 MaxCut 主函数。

    Args:
        GRAPH (list): 输入的图 ``G = [V, E]``，其中，``V`` 为节点集合，``E`` 为边集合
        DEPTH (int): QAOA 算法的深度
        SEED (int): paddle 的训练种子
        LR (float): 学习率
        ITR (int): 单次轮回的迭代次数
        EPOCH (int): 轮回数
        SHOTS (int): 获得最终比特串时设定的测量次数

    Returns:
        list: 列表第一个元素为求得的最优解，第二个元素为对应的目标函数的值

    代码示例：

    .. code-block:: python

        from paddle_quantum.mbqc.QAOA.maxcut import mbqc_maxcut
        V = [1, 2, 3, 4]
        E = [(1, 2), (2, 3), (3, 4), (4, 1)]
        GRAPH = [V, E]
        mbqc_opt = mbqc_maxcut(GRAPH=GRAPH, P=2, SEED=1024, LR=0.1, ITR=120, EPOCH=1, shots=1024)
        print("Optimal solution from MBQC: ", mbqc_opt[0])
        print("Optimal value from MBQC", mbqc_opt[1])

    ::

        Corresponding polynomial is:
        -2*x_1*x_2 - 2*x_1*x_4 + 2*x_1 - 2*x_2*x_3 + 2*x_2 - 2*x_3*x_4 + 2*x_3 + 2*x_4
        iter: 10   loss_MBQC: -3.3919
        iter: 20   loss_MBQC: -3.8858
        iter: 30   loss_MBQC: -3.9810
        iter: 40   loss_MBQC: -3.9582
        iter: 50   loss_MBQC: -3.9967
        iter: 60   loss_MBQC: -3.9972
        iter: 70   loss_MBQC: -3.9999
        iter: 80   loss_MBQC: -3.9997
        iter: 90   loss_MBQC: -3.9999
        iter: 100   loss_MBQC: -4.0000
        iter: 110   loss_MBQC: -4.0000
        iter: 120   loss_MBQC: -4.0000
        训练得到的最优参数 gamma:  [1.57244132 0.78389509]
        训练得到的最优参数 beta:  [5.105461   0.78446647]
        MBQC 模型下训练用时为： 8.373932838439941
        MBQC 模型得到的最优解为： 1010
        MBQC 模型得到的最优值为： 4
    """
    plot_graph(graph=GRAPH, title="Graph to be cut")

    # Obtain the objective polynomial
    polynomial = graph_to_poly(GRAPH)
    print("Corresponding polynomial is:\r\n", polynomial[1])

    start_time = perf_counter()
    seed(SEED)
    mbqc_net = MBQC_QAOA_Net(DEPTH)
    # Choose Adams optimizer (or SGD optimizer)
    opt = optimizer.Adam(learning_rate=LR, parameters=mbqc_net.parameters())
    # opt = optimizer.SGD(learning_rate = LR, parameters = mbqc_net.parameters())

    # Start training
    for epoch in range(EPOCH):
        for itr in range(1, ITR + 1):
            loss, state_out = mbqc_net(poly=polynomial)
            loss.backward()
            opt.minimize(loss)
            opt.clear_grad()
            if itr % 10 == 0:
                print("iter:", itr, "  loss_MBQC:", "%.4f" % loss.numpy())
    print("Optimal parameter gamma: ", mbqc_net.gamma.numpy())
    print("Optimal parameter beta: ", mbqc_net.beta.numpy())
    end_time = perf_counter()
    print("MBQC running time: ", end_time - start_time)

    # Decode the MaxCut solution from the final state
    mbqc_solution = get_solution_string(state_out, SHOTS)
    plot_solution(GRAPH, mbqc_solution)

    # Evaluate the number of cuts
    var_num, poly_symbol = polynomial
    relation = {symbols('x_' + str(j + 1)): int(mbqc_solution[j]) for j in range(var_num)}
    mbqc_value = int(poly_symbol.evalf(subs=relation))
    mbqc_opt = [mbqc_solution, mbqc_value]

    return mbqc_opt


def circuit_maxcut(SEED, GRAPH, DEPTH, LR, ITR, EPOCH, SHOTS):
    r"""定义电路模型下的 MaxCut 主函数。

    Args:
        SEED (int): paddle 的训练种子
        GRAPH (list): 输入的图 ``G = [V, E]``，其中，``V`` 为节点集合，``E`` 为边集合
        DEPTH (int): QAOA 算法的深度
        LR (float): 学习率
        ITR (int): 单次轮回的迭代次数
        EPOCH (int): 轮回数
        SHOTS (int): 获得最终比特串时设定的测量次数

    Returns:
        str: 最大割问题最优解对应的比特串
    """
    plot_graph(graph=GRAPH, title="Graph to be cut")
    start_time_PQ = perf_counter()
    # Set cost Hamiltonian
    H_D_list = [[-0.5, 'z' + str(u - 1) + ',z' + str(v - 1)] for (u, v) in GRAPH[1]]

    # Initialize
    seed(SEED)
    pq_net = Circuit_QAOA_Net(DEPTH)
    opt = optimizer.Adam(learning_rate=LR, parameters=pq_net.parameters())
    for epoch in range(EPOCH):
        for itr in range(1, ITR + 1):
            loss, cir = pq_net(GRAPH, H_D_list)
            loss.backward()
            opt.minimize(loss)
            opt.clear_grad()
            if itr % 10 == 0:
                print("iter:", itr, "  loss_cir:", "%.4f" % loss.numpy())
    print("Optimal parameter gamma: ", pq_net.gamma.numpy())
    print("Optimal parameter beta: ", pq_net.beta.numpy())
    end_time_PQ = perf_counter()
    print("Circuit running time: ", end_time_PQ - start_time_PQ)

    # Obtain the bit string
    prob_measure = cir.measure(shots=SHOTS, plot=False)
    cut_bitstring = max(prob_measure, key=prob_measure.get)
    plot_solution(GRAPH, cut_bitstring)
    return cut_bitstring
