# -*- coding: utf-8 -*-

# Copyright 2018, IBM.
#
# This source code is licensed under the Apache License, Version 2.0 found in
# the LICENSE.txt file in the root directory of this source tree.

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

import time
import threading
import types
from IPython.display import display                              # pylint: disable=import-error
from IPython.core.magic import line_magic, Magics, magics_class  # pylint: disable=import-error
from IPython.core import magic_arguments                         # pylint: disable=import-error
import ipywidgets as widgets                                     # pylint: disable=import-error
import matplotlib.pyplot as plt                                  # pylint: disable=import-error
from qiskit.tools.monitor.backend_overview import get_unique_backends
from qiskit.tools.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='', cell=None):  # pylint: disable=W0613
        """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')
        pend_label = widgets.Label(value='Pending Jobs')
        least_label = widgets.Label(value='Least Busy')
        oper_label = widgets.Label(
            value='Operational', layout=widgets.Layout(margin='5px 0px 0px 0px'))
        t1_label = widgets.Label(
            value='Avg. T1', layout=widgets.Layout(margin='10px 0px 0px 0px'))
        t2_label = widgets.Label(
            value='Avg. T2', layout=widgets.Layout(margin='10px 0px 0px 0px'))

        labels_widget = widgets.VBox([qubit_label, pend_label, least_label,
                                      oper_label, t1_label, t2_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=W0201
        backend_grid._update = types.MethodType(  # pylint: disable=W0201
            update_backend_info, backend_grid)

        backend_grid._thread = threading.Thread(  # pylint: disable=W0201
            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=W0703
                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="<h4>{name}</h4>".format(name=backend.name()),
                        layout=widgets.Layout())

    n_qubits = config['n_qubits']

    qubit_count = widgets.HTML(value="<h5><b>{qubits}</b></h5>".format(qubits=n_qubits),
                               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']])/n_qubits, 1)
    t1_widget = widgets.HTML(value="<h5>{t1} {units}</h5>".format(t1=avg_t1, units=t1_units),
                             layout=widgets.Layout())

    t2_units = props['qubits'][0][1]['unit']
    avg_t2 = round(sum([q[1]['value'] for q in props['qubits']])/n_qubits, 1)
    t2_widget = widgets.HTML(value="<h5>{t2} {units}</h5>".format(t2=avg_t2, units=t2_units),
                             layout=widgets.Layout())

    out = widgets.VBox([name, cmap, qubit_count, pending,
                        least_busy, is_oper, t1_widget, t2_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.currentThread()
    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=W0703
                    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[4].value = "<h5 style='color:#34bc6e'>True</h5>"
                else:
                    self.children[var].children[4].value = "<h5 style='color:#dc267f'>False</h5>"

                self.children[var].children[3].children[1].value = pending[var]
                self.children[var].children[3].children[1].max = max(
                    self.children[var].children[3].children[1].max, pending[var]+10)
                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
