from . import distribution
from . import geometry
from .distribution import Distribution
from .expr import Node
from .geometry import Point, Segment
from .quorum_system import Strategy
from typing import Dict, FrozenSet, List, Optional, Set, Tuple, TypeVar
import collections
import matplotlib
import matplotlib.pyplot as plt


T = TypeVar('T')


def plot_node_load(filename: str,
                   strategy: Strategy[T],
                   nodes: Optional[List[Node[T]]] = None,
                   read_fraction: Optional[Distribution] = None,
                   write_fraction: Optional[Distribution] = None):
    fig, ax = plt.subplots()
    plot_node_load_on(ax, strategy, nodes, read_fraction, write_fraction)
    ax.set_xlabel('Node')
    ax.set_ylabel('Load')
    fig.tight_layout()
    fig.savefig(filename)


def plot_node_load_on(ax: plt.Axes,
                      strategy: Strategy[T],
                      nodes: Optional[List[Node[T]]] = None,
                      read_fraction: Optional[Distribution] = None,
                      write_fraction: Optional[Distribution] = None):
    _plot_node_load_on(ax,
                       strategy,
                       nodes or list(strategy.nodes()),
                       scale=1,
                       scale_by_node_capacity=True,
                       read_fraction=read_fraction,
                       write_fraction=write_fraction)


def plot_node_utilization(filename: str,
                          strategy: Strategy[T],
                          nodes: Optional[List[Node[T]]] = None,
                          read_fraction: Optional[Distribution] = None,
                          write_fraction: Optional[Distribution] = None):
    fig, ax = plt.subplots()
    plot_node_utilization_on(ax, strategy, nodes, read_fraction, write_fraction)
    ax.set_xlabel('Node')
    ax.set_ylabel('Utilization')
    fig.tight_layout()
    fig.savefig(filename)


def plot_node_utilization_on(ax: plt.Axes,
                      strategy: Strategy[T],
                      nodes: Optional[List[Node[T]]] = None,
                      read_fraction: Optional[Distribution] = None,
                      write_fraction: Optional[Distribution] = None):
    _plot_node_load_on(ax,
                       strategy,
                       nodes or list(strategy.nodes()),
                       scale=strategy.capacity(read_fraction, write_fraction),
                       scale_by_node_capacity=True,
                       read_fraction=read_fraction,
                       write_fraction=write_fraction)


def plot_node_throughput(filename: str,
                   strategy: Strategy[T],
                   nodes: Optional[List[Node[T]]] = None,
                   read_fraction: Optional[Distribution] = None,
                   write_fraction: Optional[Distribution] = None):
    fig, ax = plt.subplots()
    plot_node_throughput_on(ax, strategy, nodes, read_fraction, write_fraction)
    ax.set_xlabel('Node')
    ax.set_ylabel('Throughput')
    fig.tight_layout()
    fig.savefig(filename)


def plot_node_throughput_on(ax: plt.Axes,
                            strategy: Strategy[T],
                            nodes: Optional[List[Node[T]]] = None,
                            read_fraction: Optional[Distribution] = None,
                            write_fraction: Optional[Distribution] = None,
                            draw_node_capacities: bool = True):
    nodes = nodes or list(strategy.nodes())
    d = distribution.canonicalize_rw(read_fraction, write_fraction)
    fr = sum(weight * fr for (fr, weight) in d.items())
    fw = 1 - fr
    # TODO(mwhittaker): Explain.
    node_limits = [
        fr * strategy.x_read_probability[node.x] / node_load +
        fw * strategy.x_write_probability[node.x] / node_load
        for node in nodes
        for node_load in [strategy.node_load(
            node,
            read_fraction=read_fraction,
            write_fraction=write_fraction
        )]
    ]
    _plot_node_load_on(ax,
                       strategy,
                       nodes,
                       scale=strategy.capacity(read_fraction, write_fraction),
                       scale_by_node_capacity=False,
                       read_fraction=read_fraction,
                       write_fraction=write_fraction,
                       node_limits=node_limits if draw_node_capacities else None)


def _plot_node_load_on(ax: plt.Axes,
                       sigma: Strategy[T],
                       nodes: List[Node[T]],
                       scale: float,
                       scale_by_node_capacity: bool,
                       read_fraction: Optional[Distribution] = None,
                       write_fraction: Optional[Distribution] = None,
                       node_limits: List[float] = None):
    d = distribution.canonicalize_rw(read_fraction, write_fraction)
    x_list = [node.x for node in nodes]
    x_index = {x: i for (i, x) in enumerate(x_list)}
    x_ticks = list(range(len(x_list)))

    def one_hot(quorum: FrozenSet[T]) -> List[float]:
        bar_heights = [0.0] * len(x_list)
        for x in quorum:
            bar_heights[x_index[x]] = 1.
        return bar_heights

    width = 0.8
    def plot_quorums(sigma: Dict[FrozenSet[T], float],
                     fraction: float,
                     bottoms: List[float],
                     capacities: List[float],
                     cmap: matplotlib.colors.Colormap):
        for (i, (quorum, weight)) in enumerate(sigma.items()):
            bar_heights = [scale * fraction * weight * x for x in one_hot(quorum)]
            if scale_by_node_capacity:
                for j in range(len(bar_heights)):
                    bar_heights[j] /= capacities[j]

            ax.bar(x_ticks,
                   bar_heights,
                   bottom=bottoms,
                   color=cmap(0.75 - i * 0.5 / len(sigma)),
                   edgecolor='white', width=width)

            for j, (bar_height, bottom) in enumerate(zip(bar_heights, bottoms)):
                text = ''.join(str(x) for x in sorted(list(quorum))) # type: ignore
                if bar_height != 0:
                    ax.text(x_ticks[j], bottom + bar_height / 2, text,
                            ha='center', va='center')

            for j in range(len(bar_heights)):
                bottoms[j] += bar_heights[j]

    # Plot the quorums.
    fr = sum(weight * fr for (fr, weight) in d.items())
    fw = 1 - fr
    read_capacities = [node.read_capacity for node in nodes]
    write_capacities = [node.write_capacity for node in nodes]
    bottoms = [0.0] * len(x_list)
    plot_quorums(sigma.sigma_r, fr, bottoms, read_capacities,
                 matplotlib.cm.get_cmap('Reds'))
    plot_quorums(sigma.sigma_w, fw, bottoms,
                 write_capacities, matplotlib.cm.get_cmap('Blues'))

    # Plot the limits, if there are any.
    if node_limits is not None:
        for (i, limit) in enumerate(node_limits):
            ax.plot([i - width/2, i + width/2], [limit, limit], color='black')

    ax.set_xticks(x_ticks)
    ax.set_xticklabels(str(x) for x in x_list)


def plot_load_distribution(filename: str,
                           strategy: Strategy[T],
                           nodes: Optional[List[Node[T]]] = None):
    fig, ax = plt.subplots()
    plot_load_distribution_on(ax, strategy, nodes)
    ax.set_xlabel('Read Fraction')
    ax.set_ylabel('Load')
    fig.tight_layout()
    fig.savefig(filename)


def _group(segments: Dict[T, Segment]) -> Dict[Segment, List[T]]:
    groups: Dict[Segment, List[T]] = collections.defaultdict(list)
    for x, segment in segments.items():
        matches = (s for s in groups if segment.approximately_equal(s))
        groups[next(matches, segment)].append(x)
    return groups


def plot_load_distribution_on(ax: plt.Axes,
                              strategy: Strategy[T],
                              nodes: Optional[List[Node[T]]] = None):
    nodes = nodes or list(strategy.nodes())

    # We want to plot every node's load distribution. Multiple nodes might
    # have the same load distribution, so we group the nodes by their
    # distribution. The grouping is a little annoying because two floats
    # might not be exactly equal but pretty close.
    groups = _group({
        node.x: Segment(
            Point(0, strategy.node_load(node, read_fraction=0)),
            Point(1, strategy.node_load(node, read_fraction=1))
        )
        for node in nodes
    })

    # Compute and plot the max of all segments. We plot the load first so that
    # it lies underneath the node loads.
    path = geometry.max_of_segments(list(groups.keys()))
    ax.plot([p[0] for p in path],
            [p[1] for p in path],
            label='load',
            linewidth=4)

    # We plot the node loads second so that they appear above the load.
    for segment, xs in groups.items():
        ax.plot([segment.l.x, segment.r.x],
                [segment.l.y, segment.r.y],
                '--',
                label=','.join(str(x) for x in xs),
                linewidth=2,
                alpha=0.75)
