import functools
import os
import random
from copy import deepcopy

import sys
import time
import matplotlib.patches as pat
import matplotlib.pyplot as plt
import networkx as nx
from decorator import contextmanager

import libs.geometry as geo


def debug(fn):
    if not os.getenv("LOCAL"):
        return fn

    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        kwargs_cp = deepcopy(kwargs)
        ret = fn(*args, **kwargs)
        elapsed_ms = (time.perf_counter() - start_time) * 1000
        print("DEBUG [{:4d} ms]: {}({}) -> {}".format(
            int(elapsed_ms),
            fn.__name__,
            ", ".join(
                list(map(str, args))
                + ["{}={}".format(k, str(v)) for k, v in kwargs_cp.items()]
            ),
            ret
        ))
        return ret

    return wrapper


@contextmanager
def timer(name):
    st = time.perf_counter()
    yield
    t = int((time.perf_counter() - st) * 1000)
    print(f'[{name}] {t:4d} ms', file=sys.stderr)


def plot_figure(*figures):
    """
    geometry.py のクラスを matplotlib で描画
    :param figures:
    """
    alpha = 0.4
    ax = plt.axes()
    for f in figures:
        # isinstance だとコピペしたクラスが別のクラスとして扱われてしまうのでクラス名で文字列比較
        if type(f).__name__ == geo.Polygon.__name__:
            ax.add_patch(pat.Polygon([(p.x, p.y) for p in f.points], alpha=alpha))
        elif type(f).__name__ == geo.Circle.__name__:
            ax.add_patch(pat.Circle((f.o.x, f.o.y), f.r, alpha=alpha))
        elif type(f).__name__ == geo.Segment.__name__:
            ax.plot((f.p1.x, f.p2.x), (f.p1.y, f.p2.y))
        elif type(f).__name__ == geo.Point.__name__:
            ax.plot((f.x,), (f.y,), 'o')
        elif isinstance(f, complex):
            ax.plot((f.real,), (f.imag,), 'o')
        elif type(f).__name__ == geo.Line.__name__:
            raise NotImplementedError()
        else:
            raise NotImplementedError()
    plt.axis('scaled')
    ax.set_aspect('equal')
    plt.show()


def generate_undirected_graph(V, E, origin=1):
    """
    V 頂点 E 辺の単純連結無向グラフ
    :param int V:
    :param int E:
    :param int origin:
    :rtype: list of (int, int)
    """
    if E < V - 1:
        raise ValueError('辺が少なすぎます')
    if E > V * (V - 1) // 2:
        raise ValueError('辺が多すぎます')

    #: :type: nx.Graph
    G = nx.random_tree(V)
    if V * (V - 1) // 2 <= 1e6:
        # 存在しない辺を全部とってきてその中から選ぶ
        edges = list(nx.non_edges(G))
        random.shuffle(edges)
        G.add_edges_from(edges[:E - V + 1])
    else:
        # 辺を適当に生成して存在しなければ追加
        cnt = V - 1
        while cnt < E:
            v = random.randint(0, V - 1)
            u = random.randint(0, V - 1)
            if v == u or G.has_edge(v, u):
                continue
            G.add_edge(v, u)
            cnt += 1
    ret = []
    for v, u in G.edges():
        ret.append((v + origin, u + origin))
    return ret


def generate_directed_graph(V, E, origin=1):
    """
    V 頂点 E 辺の単純連結有向グラフ
    :param int V:
    :param int E:
    :param int origin:
    :rtype: list of (int, int)
    """
    if E < V - 1:
        raise ValueError('辺が少なすぎます')
    if E > V * (V - 1):
        raise ValueError('辺が多すぎます')

    tree = []
    for v, u in nx.random_tree(V).edges():
        if random.randint(0, 1):
            v, u = u, v
        tree.append((v, u))
    #: :type: nx.DiGraph
    G = nx.from_edgelist(tree, create_using=nx.DiGraph)
    if V * (V - 1) <= 1e6:
        # 存在しない辺を全部とってきてその中から選ぶ
        edges = list(nx.non_edges(G))
        random.shuffle(edges)
        G.add_edges_from(edges[:E - V + 1])
    else:
        # 辺を適当に生成して存在しなければ追加
        cnt = V - 1
        while cnt < E:
            v = random.randint(0, V - 1)
            u = random.randint(0, V - 1)
            if v == u or G.has_edge(v, u):
                continue
            G.add_edge(v, u)
            cnt += 1
    ret = []
    for v, u in G.edges():
        ret.append((v + origin, u + origin))
    return ret


def generate_oriented_graph(V, E, origin=1):
    """
    両方向の辺がある頂点対を持たない有向グラフを生成
    https://en.wikipedia.org/wiki/Directed_graph#Types_of_directed_graphs
    :param int V:
    :param int E:
    :param int origin:
    :rtype: list of (int, int)
    """
    ret = []
    for v, u in generate_undirected_graph(V, E, origin=origin):
        if random.randint(0, 1):
            v, u = u, v
        ret.append((v, u))
    return ret


def generate_tree(V, directed=False, origin=1):
    """
    V 頂点の木を生成する
    :param int V:
    :param bool directed:
    :param int origin:
    """
    if directed:
        return generate_directed_graph(V=V, E=V - 1, origin=origin)
    else:
        return generate_undirected_graph(V=V, E=V - 1, origin=origin)


def plot_graph(edges, directed=False):
    """
    :param typing.List[typing.Tuple[int]] edges: (from, to, [weight])
    :param bool directed:
    """
    if not edges:
        return
    weighted = len(edges[0]) == 3

    cls = nx.DiGraph if directed else nx.Graph
    G = cls()
    if weighted:
        G.add_weighted_edges_from(edges)
        pos = nx.spring_layout(G)
        labels = nx.draw_networkx_edge_labels(
            G, pos, edge_labels={(v, u): w for v, u, w in edges})
        for label in labels.values():
            # ラベルの傾きを修正
            label.set_rotation('horizontal')
        nx.draw_networkx(G, pos, with_labels=True, alpha=0.5)
    else:
        G.add_edges_from(edges)
        nx.draw_networkx(G, alpha=0.5)
    plt.axis("off")
    plt.show()
