import time
import functools
from weakref import WeakKeyDictionary
from typing import Union, Optional, Callable

import numpy as np
import scipy.sparse as sp

from AnyQt.QtCore import QTimer, QSize, Qt

import Orange
from Orange.data import Table, Domain, StringVariable
from Orange.widgets import gui
from Orange.widgets.settings import Setting, SettingProvider
from Orange.widgets.utils.concurrents import ConcurrentWidgetMixin
from Orange.widgets.utils.plot import OWPlotGUI
from Orange.widgets.visualize.utils.widget import OWDataProjectionWidget
from Orange.widgets.widget import Input, Output

from orangecontrib.network.network.base import Network
from orangecontrib.network.network.layout import fruchterman_reingold
from orangecontrib.network.widgets.graphview import GraphView
from orangewidget.widget import Message, Msg
from orangecontrib.network.i18n_config import *

FR_ALLOWED_TIME = 30


def __(key):
    return i18n.t('network.OWNxExplorer.' + key)

# This decorator doesn't belong here. When Orange 3.37 is released
# (hopefully with https://github.com/biolab/orange3/pull/6612), this code
# should be removed and the decorator imported from Orange.util.

# This should look like decorator, not a class, pylint: disable=invalid-name
class allot:
    """
    Decorator that allows a function only a specified portion of time per call.

    Usage:

    ```
    @allot(0.2, overflow=of)
    def f(x):
       ...
    ```

    The above function is allotted 0.2 second per second. If it runs for 0.2 s,
    all subsequent calls in the next second (after the start of the call) are
    ignored. If it runs for 0.1 s, subsequent calls in the next 0.5 s are
    ignored. If it runs for a second, subsequent calls are ignored for 5 s.

    An optional overflow function can be given as a keyword argument
    `overflow`. This function must have the same signature as the wrapped
    function and is called instead of the original when the call is blocked.

    If the overflow function is not given, the wrapped function must not return
    result. This is because without the overflow function, the wrapper has no
    value to return when the call is skipped.

    The decorator adds a method `call` to force the call, e.g. by calling
    f.call(5), in the above case. The used up time still counts for the
    following (non-forced) calls.

    The decorator also adds two attributes:

    - f.last_call_duration is the duration of the last call (in seconds)
    - f.no_call_before contains the time (time.perf_counter) when the next
        call will be made.

    The decorator can be used for functions and for methods.

    A non-parametrized decorator doesn't block any calls and only adds
    last_call_duration, so that it can be used for timing.
    """
    def __new__(cls: type, arg: Union[None, float, Callable], *,
                overflow: Optional[Callable] = None,
                _bound_methods: Optional[WeakKeyDictionary] = None):
        self = super().__new__(cls)

        if arg is None or isinstance(arg, float):
            # Parametrized decorator
            if arg is not None:
                assert arg > 0

            def set_func(func):
                self.__init__(func,
                              overflow=overflow,
                              _bound_methods=_bound_methods)
                self.allotted_time = arg
                return self

            return set_func

        else:
            # Non-parametrized decorator
            self.allotted_time = None
            return self

    def __init__(self,
                 func: Callable, *,
                 overflow: Optional[Callable] = None,
                 _bound_methods: Optional[WeakKeyDictionary] = None):
        assert callable(func)
        self.func = func
        self.overflow = overflow
        functools.update_wrapper(self, func)

        self.no_call_before = 0
        self.last_call_duration = None

        # Used by __get__; see a comment there
        if _bound_methods is None:
            self.__bound_methods = WeakKeyDictionary()
        else:
            self.__bound_methods = _bound_methods

    # If we are wrapping a method, __get__ is called to bind it.
    # Create a wrapper for each instance and store it, so that each instance's
    # method gets its share of time.
    def __get__(self, inst, cls):
        if inst is None:
            return self

        if inst not in self.__bound_methods:
            # __bound_methods caches bound methods per instance. This is not
            # done for perfoamnce. Bound methods can be rebound, even to
            # different instances or even classes, e.g.
            # >>> x = f.__get__(a, A)
            # >>> y = x.__get__(b, B)
            # >>> z = x.__get__(a, A)
            # After this, we want `x is z`, there shared caching. This looks
            # bizarre, but let's keep it safe. At least binding to the same
            # instance, f.__get__(a, A),__get__(a, A), sounds reasonably
            # possible.
            cls = type(self)
            bound_overflow = self.overflow and self.overflow.__get__(inst, cls)
            decorator = cls(
                self.allotted_time,
                overflow=bound_overflow,
                _bound_methods=self.__bound_methods)
            self.__bound_methods[inst] = decorator(self.func.__get__(inst, cls))

        return self.__bound_methods[inst]

    def __call__(self, *args, **kwargs):
        if time.perf_counter() < self.no_call_before:
            if self.overflow is None:
                return None
            return self.overflow(*args, **kwargs)
        return self.call(*args, **kwargs)

    def call(self, *args, **kwargs):
        start = time.perf_counter()
        result = self.func(*args, **kwargs)
        self.last_call_duration = time.perf_counter() - start
        if self.allotted_time is not None:
            if self.overflow is None:
                assert result is None, "skippable function cannot return a result"
            self.no_call_before = start + self.last_call_duration / self.allotted_time
        return result


def run(positions, edges, observe_weights, init_temp, k, state):
    def update(positions, progress):
        state.set_progress_value(progress)
        if not large_graph:
            state.set_partial_result(positions)
        if state.is_interruption_requested():
            raise Exception  # pylint: disable=broad-exception-raised

    nnodes = positions.shape[0]
    large_graph = nnodes + edges.shape[0] > 30000
    sample_ratio = None if nnodes < 1000 else 1000 / nnodes
    fruchterman_reingold(
        positions, edges, observe_weights,
        FR_ALLOWED_TIME, k, init_temp, sample_ratio,
        callback=update)
    return positions



class OWNxExplorer(OWDataProjectionWidget):
    name = __("name")
    description = __("desc")
    icon = "icons/NetworkExplorer.svg"
    priority = 6420

    class Inputs:
        node_data = Input("Node Data", Table, label=i18n.t("network.common.node_data"))
        node_subset = Input("Node Subset", Table, label=i18n.t("network.common.node_subset"))
        network = Input("Network", Network, default=True, label=i18n.t("network.common.network"))
        node_distances = Input("Node Distances", Orange.misc.DistMatrix, label=i18n.t('network.common.node_distance'))

    class Outputs(OWDataProjectionWidget.Outputs):
        subgraph = Output("Selected sub-network", Network, label=i18n.t('network.common.select_sub-network'))
        unselected_subgraph = Output("Remaining sub-network", Network, label=i18n.t('network.common.select_sub-network'))
        distances = Output("Distance matrix", Orange.misc.DistMatrix, label=i18n.t('network.common.distance_matrix'))

    UserAdviceMessages = [
        Message(__("msg.connect_component"),
                Message.Information),
    ]

    GRAPH_CLASS = GraphView
    graph = SettingProvider(GraphView)

    layout_density = Setting(10)
    observe_weights = Setting(True)

    mark_hops = Setting(1)
    mark_min_conn = Setting(5)
    mark_max_conn = Setting(5)
    mark_most_conn = Setting(1)

    alpha_value = 255  # Override the setting from parent

    class Warning(OWDataProjectionWidget.Warning):
        distance_matrix_mismatch = Msg(__("msg.distance_matrix_mismatch"))
        no_graph_found = Msg(__('msg.no_graph_found'))

    class Error(OWDataProjectionWidget.Error):
        data_size_mismatch = widget.Msg(__("msg.data_size_mismatch"))
        network_too_large = widget.Msg(__('msg.network_too_large'))
        # single_node_graph = widget.Msg(__('msg.single_node_graph'))

    def __init__(self):
        # These are already needed in super().__init__()
        self.number_of_nodes = 0
        self.number_of_edges = 0
        self.nHighlighted = 0
        self.nSelected = 0
        self.nodes_per_edge = 0
        self.edges_per_node = 0

        self.mark_mode = 0
        self.mark_text = ""

        OWDataProjectionWidget.__init__(self)
        ConcurrentWidgetMixin.__init__(self)

        self.network = None
        self.node_data = None
        self.distance_matrix = None
        self.edges = None
        self.positions = None

        self.marked_nodes = None
        self.searchStringTimer = QTimer(self)
        self.searchStringTimer.timeout.connect(self.update_marks)
        self.set_mark_mode()
        self.setMinimumWidth(600)

    def sizeHint(self):
        return QSize(800, 600)

    def _add_controls(self):
        # pylint: disable=attribute-defined-outside-init
        self.gui = OWPlotGUI(self)
        self._add_info_box()
        self.gui.point_properties_box(self.controlArea)
        self._add_effects_box()
        self.gui.plot_properties_box(self.controlArea)
        self._add_mark_box()
        self.controls.attr_label.activated.connect(self.on_change_label_attr)

    def _add_info_box(self):
        # pylint: disable=attribute-defined-outside-init
        info = gui.vBox(self.controlArea, box=__("box_layout"))
        gui.label(
            info, self,
            __("node_detail"))
        gui.label(
            info, self, __('edge_detail'))
        lbox = gui.hBox(info)
        self.relayout_button = gui.button(
            lbox, self, __("btn.improve"), callback=self.improve, autoDefault=False,
            tooltip=__("btn.improve_tip"))

        self.stop_button = gui.button(
            lbox, self, __('btn.stop'), callback=self.stop_relayout, autoDefault=False,
            hidden=True)
        self.randomize_button = gui.button(
            lbox, self, __("btn.re_layout"), callback=self.restart, autoDefault=False,
            tooltip=__("btn.re_layout_tip"))
        gui.hSlider(info, self, "layout_density", minValue=1, maxValue=50,
                    label=__("label_gravity"), orientation=Qt.Horizontal,
                    callback_finished=self.improve,
                    tooltip=__("label_gravity_tip"))
        gui.checkBox(info, self, "observe_weights",
                     label=__("label_observe_weights"),
                     callback=self.improve)

    def _add_effects_box(self):
        # pylint: disable=attribute-defined-outside-init
        gbox = self.gui.create_gridbox(self.controlArea, box=__("box_size"))
        self.gui.add_widget(self.gui.PointSize, gbox)
        gbox.layout().itemAtPosition(1, 0).widget().setText(__('row.node_size'))
        self.gui.add_control(
            gbox, gui.hSlider, __("row.edge_width"),
            master=self, value='graph.edge_width',
            minValue=1, maxValue=10, step=1,
            callback=self.graph.update_edges)
        box = gui.vBox(None)
        gbox.layout().addWidget(box, 3, 0, 1, 2)
        gui.separator(box)
        self.checkbox_relative_edges = gui.checkBox(
            box, self, 'graph.relative_edge_widths',
            __('checkbox.relative_edges'),
            callback=self.graph.update_edges)
        self.checkbox_show_weights = gui.checkBox(
            box, self, 'graph.show_edge_weights',
            __('checkbox.show_weights'),
            callback=self.graph.update_edge_labels)
        self.checkbox_show_weights = gui.checkBox(
            box, self, 'graph.label_selected_edges',
            __('checkbox.select_edges'),
            callback=self.graph.update_edge_labels)

        # This is ugly: create a slider that controls alpha_value so that
        # parent can enable and disable it - although it's never added to any
        # layout and visible to the user
        gui.hSlider(None, self, "graph.alpha_value")

    def _add_mark_box(self):
        # pylint: disable=attribute-defined-outside-init
        hbox = gui.hBox(None, box=True)
        self.mainArea.layout().addWidget(hbox)
        vbox = gui.hBox(hbox)

        def spin(value, label, minv, maxv):
            return gui.spin(
                vbox, self, value, label=label, minv=minv, maxv=maxv,
                step=1,
                alignment=Qt.AlignRight, callback=self.update_marks).box

        def text_line():
            def set_search_string_timer():
                self.searchStringTimer.stop()
                self.searchStringTimer.start(300)

            return gui.lineEdit(
                gui.hBox(vbox), self, "mark_text", label=__("label_text"),
                orientation=Qt.Horizontal, minimumWidth=50,
                callback=set_search_string_timer, callbackOnType=True).box

        def _mark_by_labels(marker):
            txt = self.mark_text.lower()
            if not txt:
                return None
            labels = self.get_label_data()
            if labels is None:
                return None
            return marker(np.char.array(labels), txt)

        def mark_label_equals():
            return _mark_by_labels(
                lambda labels, txt: np.flatnonzero(labels.lower() == txt))

        def mark_label_starts():
            return _mark_by_labels(
                lambda labels, txt: np.flatnonzero(labels.lower().startswith(txt)))

        def mark_label_contains():
            return _mark_by_labels(
                lambda labels, txt: np.flatnonzero(labels.lower().find(txt) != -1))

        def mark_text():
            txt = self.mark_text.lower()
            if not txt or self.data is None:
                return None
            return np.array(
                [i for i, inst in enumerate(self.data)
                 if txt in "\x00".join(map(str, inst.list)).lower()])

        def mark_reachable():
            selected = self.graph.get_selection()
            if selected is None:
                return None
            return self.get_reachable(selected)

        def mark_close():
            selected = self.graph.get_selection()
            if selected is None:
                return None
            neighbours = set(selected)
            last_round = list(neighbours)
            for _ in range(self.mark_hops):
                next_round = set()
                for neigh in last_round:
                    next_round |= set(self.network.neighbours(neigh))
                neighbours |= next_round
                last_round = next_round
            neighbours -= set(selected)
            return np.array(list(neighbours))

        def mark_from_input():
            if self.subset_data is None or self.data is None:
                return None
            ids = set(self.subset_data.ids)
            return np.array(
                [i for i, ex in enumerate(self.data) if ex.id in ids])

        def mark_most_connections():
            n = self.mark_most_conn
            if n >= self.number_of_nodes:
                return np.arange(self.number_of_nodes)
            degrees = self.network.degrees()
            # pylint: disable=invalid-unary-operand-type
            min_degree = np.partition(degrees, -n)[-n]
            return np.flatnonzero(degrees >= min_degree)

        def mark_more_than_any_neighbour():
            degrees = self.network.degrees()
            return np.array(
                [node for node, degree in enumerate(degrees)
                 if degree > np.max(degrees[self.network.neighbours(node)],
                                    initial=0)])

        def mark_more_than_average_neighbour():
            degrees = self.network.degrees()
            return np.array(
                [node for node, degree, neighbours in (
                    (node, degree, self.network.neighbours(node))
                     for node, degree in enumerate(degrees))
                 if degree > (np.mean(degrees[neighbours]) if neighbours.size else 0)
                 ]
            )

        self.mark_criteria = [
            (__("gbox.mark_criteria"), None, lambda: np.zeros((0,))),
            (__("gbox.mark_node_labelled_as"), text_line(), mark_label_equals),
            (__("gbox.mark_label_starts"), text_line(), mark_label_starts),
            (__("gbox.mark_label_contains"), text_line(), mark_label_contains),
            (__("gbox.mark_text"), text_line(), mark_text),
            (__("gbox.mark_reachable"), None, mark_reachable),

            (__("gbox.mark_close"),
             spin("mark_hops", __("number_hops"), 1, 20),
             mark_close),

            (__("gbox.mark_from_input"), None, mark_from_input),

            (__("gbox.mark_connection"),
             spin("mark_max_conn", __("max_connection"), 0, 1000),
             lambda: np.flatnonzero(self.network.degrees() <= self.mark_max_conn)),

            (__("gbox.mark_min_conn"),
             spin("mark_min_conn", __("min_connection"), 1, 1000),
             lambda: np.flatnonzero(self.network.degrees() >= self.mark_min_conn)),

            (__("gbox.mark_most_connections"),
             spin("mark_most_conn", __("mark_number"), 1, 1000),
             mark_most_connections),

            (__("gbox.mark_more_than_any_neighbour"), None,
             mark_more_than_any_neighbour),

            (__("gbox.mark_more_than_average_neighbour"), None,
             mark_more_than_average_neighbour)
        ]

        cb = gui.comboBox(
            hbox, self, "mark_mode",
            items=[item for item, *_ in self.mark_criteria],
            maximumContentsLength=-1, callback=self.set_mark_mode)
        hbox.layout().insertWidget(0, cb)

        gui.rubber(hbox)
        self.btselect = gui.button(
            hbox, self, __("btn.select"), callback=self.select_marked)
        self.btadd = gui.button(
            hbox, self, __("btn.select_add_marked"), callback=self.select_add_marked)
        self.btgroup = gui.button(
            hbox, self, __("btn.select_as_group"), callback=self.select_as_group)

    def set_mark_mode(self, mode=None):
        if mode is not None:
            self.mark_mode = mode
        for i, (_, widget, _) in enumerate(self.mark_criteria):
            if widget:
                if i == self.mark_mode:
                    widget.show()
                else:
                    widget.hide()
        self.searchStringTimer.stop()
        self.update_marks()

    def update_marks(self):
        if self.network is None:
            return
        self.marked_nodes = self.mark_criteria[self.mark_mode][2]()
        if self.marked_nodes is not None and not self.marked_nodes.size:
            self.marked_nodes = None
        self.graph.update_marks()
        if self.graph.label_only_selected:
            self.graph.update_labels()
        self.update_selection_buttons()

    def update_selection_buttons(self):
        if self.marked_nodes is None:
            self.btselect.hide()
            self.btadd.hide()
            self.btgroup.hide()
            return
        else:
            self.btselect.show()

        selection = self.graph.get_selection()
        if len(selection) == 0 or np.max(selection) == 0:
            self.btadd.hide()
            self.btgroup.hide()
        elif np.max(selection) == 1:
            self.btadd.setText(__("msg.add_select"))
            self.btadd.show()
            self.btgroup.hide()
        else:
            self.btadd.setText(__("msg.add_group"))
            self.btadd.show()
            self.btgroup.show()

    def selection_changed(self):
        super().selection_changed()
        self.nSelected = 0 if self.selection is None else len(self.selection)
        self.update_selection_buttons()
        self.update_marks()

    def select_marked(self):
        self.graph.selection_select(self.marked_nodes)

    def select_add_marked(self):
        self.graph.selection_append(self.marked_nodes)

    def select_as_group(self):
        self.graph.selection_new_group(self.marked_nodes)

    def on_change_label_attr(self):
        if self.mark_mode in (1, 2):
            self.update_marks()

    @Inputs.node_data
    def set_node_data(self, data):
        self.node_data = data

    @Inputs.node_subset
    def set_node_subset(self, data):
        # It would be better to call super, but this fails because super
        # is decorated to set the partial summary for signal "Subset Data",
        # which does not exist for this widget (OWNxExplorer.Inputs is not
        # derived from OWDataProjectionWidget.Inputs in order to rename the
        # signal)
        self.subset_data = data

    @Inputs.node_distances
    def set_items_distance_matrix(self, matrix):
        self.distance_matrix = matrix
        self.positions = None

    @Inputs.network
    def set_graph(self, graph):

        def set_graph_none(error=None):
            if error is not None:
                error()
            self.network = None
            self.number_of_nodes = self.edges_per_node = 0
            self.number_of_edges = self.nodes_per_edge = 0

        def compute_stats():
            self.number_of_nodes = graph.number_of_nodes()
            self.number_of_edges = graph.number_of_edges()
            self.edges_per_node = self.number_of_edges / self.number_of_nodes
            self.nodes_per_edge = \
                self.number_of_nodes / max(1, self.number_of_edges)

        self.mark_text = ""
        self.set_mark_mode(0)
        self.positions = None

        if not graph or graph.number_of_nodes() == 0:
            set_graph_none()
            return
        if graph.number_of_nodes() + graph.number_of_edges() > 100000:
            set_graph_none(self.Error.network_too_large)
            return
        self.Error.clear()

        self.network = graph
        compute_stats()

    def handleNewSignals(self):
        network = self.network

        def set_actual_data():
            self.closeContext()
            self.Error.data_size_mismatch.clear()
            self.Warning.no_graph_found.clear()
            if network is None:
                if self.node_data is not None:
                    self.Warning.no_graph_found()
                return
            n_nodes = len(self.network.nodes)
            if self.node_data is not None:
                if len(self.node_data) != n_nodes:
                    self.Error.data_size_mismatch()
                    self.data = None
                else:
                    self.data = self.node_data
            if self.node_data is None:
                if isinstance(network.nodes, Table):
                    self.data = network.nodes
                elif isinstance(network.nodes, np.ndarray) \
                        and (len(network.nodes.shape) == 1
                             or network.nodes.shape[1] == 1):
                    self.data = Table.from_numpy(
                        Domain([], None, [StringVariable("label")]),
                        np.zeros((len(network.nodes),0)),
                        None,
                        metas=network.nodes.reshape((n_nodes, 1))
                    )
                else:
                    self.data = None

            if self.data is not None:
                # Replicate the necessary parts of set_data
                self.valid_data = np.full(len(self.data), True, dtype=bool)
                self.init_attr_values()
                self.openContext(self.data)
                self.cb_class_density.setEnabled(self.can_draw_density())

        def set_actual_edges():
            def set_checkboxes(value):
                self.checkbox_show_weights.setEnabled(value)
                self.checkbox_relative_edges.setEnabled(value)

            self.Warning.distance_matrix_mismatch.clear()

            if self.network is None:
                self.edges = None
                set_checkboxes(False)
                return

            set_checkboxes(True)
            if network.number_of_edges(0):
                self.edges = network.edges[0].edges.tocoo()
            else:
                self.edges = sp.coo_matrix((0, 3))
            if self.distance_matrix is not None:
                if len(self.distance_matrix) != self.number_of_nodes:
                    self.Warning.distance_matrix_mismatch()
                else:
                    self.edges.data = np.fromiter(
                        (self.distance_matrix[u, v]
                         for u, v in zip(self.edges.row, self.edges.col)),
                        dtype=np.int32, count=len(self.edges.row)
                    )
            if np.allclose(self.edges.data, 0):
                self.edges.data[:] = 1
                set_checkboxes(False)
            elif len(set(self.edges.data)) == 1:
                set_checkboxes(False)

        self.cancel()
        set_actual_data()
        super()._handle_subset_data()
        if self.positions is None:
            set_actual_edges()
            self.set_random_positions()
            self.graph.reset_graph()
            self.relayout(True)
        else:
            self.graph.update_point_props()
        self.update_marks()
        self.update_selection_buttons()

    def init_attr_values(self):
        super().init_attr_values()
        if self.node_data is None \
                and self.data is not None \
                and isinstance(self.network.nodes, np.ndarray):
            assert len(self.data.domain.metas) == 1
            self.attr_label = self.data.domain.metas[0]

    def randomize(self):
        self.set_random_positions()
        self.graph.update_coordinates()

    def set_random_positions(self):
        if self.network is None:
            self.positions = None
        else:
            self.positions = np.random.uniform(size=(self.number_of_nodes, 2))

    def get_reachable(self, initial):
        to_check = list(initial)
        reachable = set(to_check)
        for node in to_check:
            new_checks = set(self.network.neighbours(node)) - reachable
            to_check += new_checks
            reachable |= new_checks
        return np.array(to_check)

    def send_data(self):
        super().send_data()

        Outputs = self.Outputs
        selected_indices = self.graph.get_selection()
        if selected_indices is None or len(selected_indices) == 0:
            Outputs.subgraph.send(None)
            Outputs.unselected_subgraph.send(self.network)
            Outputs.distances.send(None)
            return

        selection = self.graph.selection
        subgraph = self.network.subgraph(selected_indices)
        subgraph.nodes = \
            self._get_selected_data(self.data, selected_indices, selection)
        Outputs.subgraph.send(subgraph)
        Outputs.unselected_subgraph.send(
            self.network.subgraph(np.flatnonzero(selection == 0)))
        distances = self.distance_matrix
        if distances is None:
            Outputs.distances.send(None)
        else:
            Outputs.distances.send(distances.submatrix(sorted(selected_indices)))

    def get_coordinates_data(self):
        if self.positions is not None:
            return self.positions.T
        else:
            return None, None

    def get_embedding(self):
        return self.positions

    def get_subset_mask(self):
        if self.data is None:
            return None
        return super().get_subset_mask()

    def get_edges(self):
        return self.edges

    def is_directed(self):
        return self.network is not None and self.network.edges[0].directed

    def get_marked_nodes(self):
        return self.marked_nodes

    def set_buttons(self, running):
        self.stop_button.setHidden(not running)
        self.relayout_button.setHidden(running)
        self.randomize_button.setHidden(running)

    def stop_relayout(self):
        self.cancel()

    def restart(self):
        self.relayout(restart=True)

    def improve(self):
        self.relayout(restart=False)

    def relayout(self, restart):
        if self.edges is None:
            return
        if restart or self.positions is None:
            self.set_random_positions()

        Simplifications = self.graph.Simplifications
        self.graph.set_simplifications(
            Simplifications.NoDensity
            + Simplifications.NoLabels * (len(self.graph.labels) > 20)
            + Simplifications.NoEdgeLabels * (len(self.graph.edge_labels) > 20)
            + Simplifications.NoEdges * (self.number_of_edges > 1000))

        init_temp = 0.05 if restart else 0.2
        k = self.layout_density / 10 / np.sqrt(self.number_of_nodes)
        self.set_buttons(running=True)
        self.start(run, self.positions, self.edges, self.observe_weights, init_temp, k)

    def cancel(self):
        self.set_buttons(running=False)
        super().cancel()

    def on_done(self, positions):  # pylint: disable=arguments-renamed
        self.positions = positions
        self.set_buttons(running=False)
        self.graph.set_simplifications(
            self.graph.Simplifications.NoSimplifications)
        self.graph.update_coordinates()

    @allot(0.02)
    def on_partial_result(self, positions):  # pylint: disable=arguments-renamed
        self.positions = positions
        self.graph.update_coordinates()

    def onDeleteWidget(self):
        self.shutdown()
        super().onDeleteWidget()

    def send_report(self):
        if self.network is None:
            return

        self.report_items('Graph info', [
            ("Number of vertices", self.network.number_of_nodes()),
            ("Number of edges", self.network.number_of_edges()),
            ("Vertices per edge", round(self.nodes_per_edge, 3)),
            ("Edges per vertex", round(self.edges_per_node, 3)),
        ])
        if self.data is not None:
            self.report_data("Data", self.data)
        if any((self.attr_color, self.attr_shape,
                self.attr_size, self.attr_label)):
            self.report_items(
                "Visual settings",
                [("Color", self._get_caption_var_name(self.attr_color)),
                 ("Label", self._get_caption_var_name(self.attr_label)),
                 ("Shape", self._get_caption_var_name(self.attr_shape)),
                 ("Size", self._get_caption_var_name(self.attr_size))])
        self.report_plot()


def main():
    # pylint: disable=import-outside-toplevel, unused-import
    from Orange.widgets.utils.widgetpreview import WidgetPreview
    from orangecontrib.network.network.readwrite \
        import read_pajek, transform_data_to_orange_table
    from os.path import join, dirname

    network = read_pajek(join(dirname(dirname(__file__)), 'networks', 'leu_by_genesets.net'))
    # network = read_pajek(
    #    join(dirname(dirname(__file__)), 'networks', 'dicty_publication.net'))
    #network = read_pajek(join(dirname(dirname(__file__)), 'networks', 'davis.net'))
    #transform_data_to_orange_table(network)
    WidgetPreview(OWNxExplorer).run(set_graph=network)


if __name__ == "__main__":
    main()
