# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2018.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""A module for monitoring backends."""

import time
import threading
import types
from IPython.display import display
from IPython.core.magic import line_magic, Magics, magics_class
from IPython.core import magic_arguments
import matplotlib.pyplot as plt
import ipywidgets as widgets
from qiskit.tools.monitor.overview import get_unique_backends
from qiskit.visualization.gate_map import plot_gate_map


@magics_class
class BackendOverview(Magics):
    """A class of status magic functions."""

    @line_magic
    @magic_arguments.magic_arguments()
    @magic_arguments.argument(
        "-i", "--interval", type=float, default=60, help="Interval for status check."
    )
    def qiskit_backend_overview(self, line=""):
        """A Jupyter magic function to monitor backends."""
        args = magic_arguments.parse_argstring(self.qiskit_backend_overview, line)

        unique_hardware_backends = get_unique_backends()
        _value = "<h2 style ='color:#ffffff; background-color:#000000;"
        _value += "padding-top: 1%; padding-bottom: 1%;padding-left: 1%;"
        _value += "margin-top: 0px'>Backend Overview</h2>"
        backend_title = widgets.HTML(value=_value, layout=widgets.Layout(margin="0px 0px 0px 0px"))

        build_back_widgets = [backend_widget(b) for b in unique_hardware_backends]

        _backends = []
        # Sort backends by operational or not
        oper_ord_backends = []
        for n, back in enumerate(unique_hardware_backends):
            if back.status().operational:
                oper_ord_backends = [build_back_widgets[n]] + oper_ord_backends
                _backends = [back] + _backends
            else:
                oper_ord_backends = oper_ord_backends + [build_back_widgets[n]]
                _backends = _backends + [back]

        qubit_label = widgets.Label(value="Num. Qubits")
        qv_label = widgets.Label(value="Quantum Vol.")
        pend_label = widgets.Label(
            value="Pending Jobs", layout=widgets.Layout(margin="5px 0px 0px 0px")
        )
        least_label = widgets.Label(
            value="Least Busy", layout=widgets.Layout(margin="10px 0px 0px 0px")
        )
        oper_label = widgets.Label(
            value="Operational", layout=widgets.Layout(margin="5px 0px 0px 0px")
        )
        t12_label = widgets.Label(
            value="Avg. T1 / T2", layout=widgets.Layout(margin="10px 0px 0px 0px")
        )
        cx_label = widgets.Label(
            value="Avg. CX Err.", layout=widgets.Layout(margin="8px 0px 0px 0px")
        )
        meas_label = widgets.Label(
            value="Avg. Meas. Err.", layout=widgets.Layout(margin="8px 0px 0px 0px")
        )

        labels_widget = widgets.VBox(
            [
                qubit_label,
                qv_label,
                pend_label,
                oper_label,
                least_label,
                t12_label,
                cx_label,
                meas_label,
            ],
            layout=widgets.Layout(margin="295px 0px 0px 0px", min_width="100px"),
        )

        backend_grid = GridBox_with_thread(
            children=oper_ord_backends,
            layout=widgets.Layout(
                grid_template_columns="250px " * len(unique_hardware_backends),
                grid_template_rows="auto",
                grid_gap="0px 25px",
            ),
        )

        backend_grid._backends = _backends  # pylint: disable=attribute-defined-outside-init
        backend_grid._update = types.MethodType(  # pylint: disable=attribute-defined-outside-init
            update_backend_info, backend_grid
        )

        backend_grid._thread = threading.Thread(  # pylint: disable=attribute-defined-outside-init
            target=backend_grid._update, args=(args.interval,)
        )
        backend_grid._thread.start()

        back_box = widgets.HBox([labels_widget, backend_grid])

        back_monitor = widgets.VBox([backend_title, back_box])
        display(back_monitor)


class GridBox_with_thread(widgets.GridBox):  # pylint: disable=invalid-name
    """A GridBox that will close an attached thread"""

    def __del__(self):
        """Object disposal"""
        if hasattr(self, "_thread"):
            try:
                self._thread.do_run = False
                self._thread.join()
            except Exception:  # pylint: disable=broad-except
                pass
        self.close()


def backend_widget(backend):
    """Creates a backend widget."""
    config = backend.configuration().to_dict()
    props = backend.properties().to_dict()

    name = widgets.HTML(value=f"<h4>{backend.name()}</h4>", layout=widgets.Layout())

    num_qubits = config["n_qubits"]

    qv_val = "-"
    if "quantum_volume" in config.keys():
        if config["quantum_volume"]:
            qv_val = config["quantum_volume"]

    qubit_count = widgets.HTML(
        value=f"<h5><b>{num_qubits}</b></h5>",
        layout=widgets.Layout(justify_content="center"),
    )

    qv_value = widgets.HTML(
        value=f"<h5>{qv_val}</h5>",
        layout=widgets.Layout(justify_content="center"),
    )

    cmap = widgets.Output(
        layout=widgets.Layout(
            min_width="250px",
            max_width="250px",
            max_height="250px",
            min_height="250px",
            justify_content="center",
            align_items="center",
            margin="0px 0px 0px 0px",
        )
    )

    with cmap:
        _cmap_fig = plot_gate_map(backend, plot_directed=False, label_qubits=False)
        if _cmap_fig is not None:
            display(_cmap_fig)
            # Prevents plot from showing up twice.
            plt.close(_cmap_fig)

    pending = generate_jobs_pending_widget()

    is_oper = widgets.HTML(value="<h5></h5>", layout=widgets.Layout(justify_content="center"))

    least_busy = widgets.HTML(value="<h5></h5>", layout=widgets.Layout(justify_content="center"))

    t1_units = props["qubits"][0][0]["unit"]
    avg_t1 = round(sum(q[0]["value"] for q in props["qubits"]) / num_qubits, 1)
    avg_t2 = round(sum(q[1]["value"] for q in props["qubits"]) / num_qubits, 1)
    t12_widget = widgets.HTML(
        value=f"<h5>{avg_t1} / {avg_t2} {t1_units}</h5>",
        layout=widgets.Layout(),
    )

    avg_cx_err = "NA"
    if config["coupling_map"]:
        sum_cx_err = 0
        num_cx = 0
        for gate in props["gates"]:
            if gate["gate"] == "cx":
                for param in gate["parameters"]:
                    if param["name"] == "gate_error":
                        # Value == 1.0 means gate effectively off
                        if param["value"] != 1.0:
                            sum_cx_err += param["value"]
                            num_cx += 1
        if num_cx > 0:
            avg_cx_err = round(sum_cx_err / num_cx, 4)

    cx_widget = widgets.HTML(value=f"<h5>{avg_cx_err}</h5>", layout=widgets.Layout())

    avg_meas_err = 0
    for qub in props["qubits"]:
        for item in qub:
            if item["name"] == "readout_error":
                avg_meas_err += item["value"]
    avg_meas_err = round(avg_meas_err / num_qubits, 4)
    meas_widget = widgets.HTML(value=f"<h5>{avg_meas_err}</h5>", layout=widgets.Layout())

    out = widgets.VBox(
        [
            name,
            cmap,
            qubit_count,
            qv_value,
            pending,
            is_oper,
            least_busy,
            t12_widget,
            cx_widget,
            meas_widget,
        ],
        layout=widgets.Layout(display="inline-flex", flex_flow="column", align_items="center"),
    )

    out._is_alive = True
    return out


def update_backend_info(self, interval=60):
    """Updates the monitor info
    Called from another thread.
    """
    my_thread = threading.current_thread()
    current_interval = 0
    started = False
    all_dead = False
    stati = [None] * len(self._backends)
    while getattr(my_thread, "do_run", True) and not all_dead:
        if current_interval == interval or started is False:
            for ind, back in enumerate(self._backends):
                _value = self.children[ind].children[2].value
                _head = _value.split("<b>")[0]
                try:
                    _status = back.status()
                    stati[ind] = _status
                except Exception:  # pylint: disable=broad-except
                    self.children[ind].children[2].value = _value.replace(
                        _head, "<h5 style='color:#ff5c49'>"
                    )
                    self.children[ind]._is_alive = False
                else:
                    self.children[ind]._is_alive = True
                    self.children[ind].children[2].value = _value.replace(_head, "<h5>")

            idx = list(range(len(self._backends)))
            pending = [s.pending_jobs for s in stati]
            _, least_idx = zip(*sorted(zip(pending, idx)))

            # Make sure least pending is operational
            for ind in least_idx:
                if stati[ind].operational:
                    least_pending_idx = ind
                    break

            for var in idx:
                if var == least_pending_idx:
                    self.children[var].children[6].value = "<h5 style='color:#34bc6e'>True</h5>"
                else:
                    self.children[var].children[6].value = "<h5 style='color:#dc267f'>False</h5>"

                self.children[var].children[4].children[1].max = max(
                    self.children[var].children[4].children[1].max, pending[var] + 10
                )
                self.children[var].children[4].children[1].value = pending[var]
                if stati[var].operational:
                    self.children[var].children[5].value = "<h5 style='color:#34bc6e'>True</h5>"
                else:
                    self.children[var].children[5].value = "<h5 style='color:#dc267f'>False</h5>"

            started = True
            current_interval = 0
        time.sleep(1)
        all_dead = not any(wid._is_alive for wid in self.children)
        current_interval += 1


def generate_jobs_pending_widget():
    """Generates a jobs_pending progress bar widget."""
    pbar = widgets.IntProgress(
        value=0,
        min=0,
        max=50,
        description="",
        orientation="horizontal",
        layout=widgets.Layout(max_width="180px"),
    )
    pbar.style.bar_color = "#71cddd"

    pbar_current = widgets.Label(value=str(pbar.value), layout=widgets.Layout(min_width="auto"))
    pbar_max = widgets.Label(value=str(pbar.max), layout=widgets.Layout(min_width="auto"))

    def _on_max_change(change):
        pbar_max.value = str(change["new"])

    def _on_val_change(change):
        pbar_current.value = str(change["new"])

    pbar.observe(_on_max_change, names="max")
    pbar.observe(_on_val_change, names="value")

    jobs_widget = widgets.HBox(
        [pbar_current, pbar, pbar_max],
        layout=widgets.Layout(max_width="250px", min_width="250px", justify_content="center"),
    )

    return jobs_widget
