import numpy as np
from .graph import Graph
from types import MethodType
from scipy.sparse import eye

def adjacent(self, u, v):
    if u >= self._num_vert or v >= self._num_vert:
        raise ValueError("Received vertices " + str(u) + " and " + str(v)
                         + ". Maximum expected value is "
                         + str(self._num_vert - 1) + ".")
    return ((u < self._num_vert1 and v >= self._num_vert1)
            or (v < self._num_vert1 and u >= self._num_vert2))

def _entry(self, row, col):
    entry = 1
    if row < self._num_vert1:
        entry += row*self._num_vert2
        entry += col - self._num_vert1
        return entry

    entry += self.number_of_edges()
    entry += (row - self._num_vert1)*self._num_vert1
    entry += col
    return entry


def _find_entry(self, entry):
    num_edges = self.number_of_edges()
    if entry < num_edges:
        row = entry // self._num_vert1
        col = entry % self._num_vert1
        return (row, col)

    entry -= num_edges
    row = entry // self._num_vert2
    col = entry % self._num_vert2
    return (row, col)

def _neighbor_index(self, vertex, neigh):
    # TODO: throw value error if not adjacent
    if neigh < self._num_vert1:
        return neigh
    return neigh - self._num_vert1

def neighbors(self, vertex):
    if vertex >= self._num_vert1:
        return np.arange(self._num_vert1)
    return np.arange(self._num_vert1, self._num_vert)

def number_of_vertices(self):
    return self._num_vert

def number_of_edges(self):
    return self._num_vert1*self._num_vert2

def degree(self, vertex):
    if vertex >= 0 and vertex < self._num_vert1:
        return self._num_vert2
    if vertex >= self._num_vert1 and vertex < self._num_vert: 
        return self._num_vert1

    raise ValueError("Vertex out of range. Expected value from 0 to "
                     + str(self._num_vert - 1) + ". But received "
                     + str(vertex) + " instead.")

def adjacency_matrix(self):
    # it is more efficient to store the dense adj matrix than
    # the sparse one when explicit values are used
    A = np.zeros((self._num_vert1, self._num_vert1), dtype=np.int8)
    B = np.ones((self._num_vert1, self._num_vert2), dtype=np.int8)
    C = np.ones((self._num_vert2, self._num_vert1), dtype=np.int8)
    D = np.zeros((self._num_vert2, self._num_vert2), dtype=np.int8)
    return np.block([[A, B],
                     [C, D]])

def laplacian_matrix(self):
    A = self._num_vert2 * np.eye(self._num_vert1, dtype=np.int32)
    B = -np.ones((self._num_vert1, self._num_vert2), dtype=np.int32)
    C = -np.ones((self._num_vert2, self._num_vert1), dtype=np.int32)
    D = self._num_vert1 * np.eye(self._num_vert2, dtype=np.int32)
    return np.block([[A, B],
                     [C, D]])

def CompleteBipartite(num_vert1, num_vert2, multiedges=None, weights=None):
    r"""
    Complete bipartite graph.

    Parameters
    ----------
    num_vert1: int
        Number of vertices of the first part.

    num_vert2: int
        Number of vertices of the second part.

    multiedges, weights: scipy.sparse.csr_array, default=None
        See :ref:`graph_constructors`.

    Returns
    -------
    :class:`hiperwalk.Graph`
        See :ref:`graph_constructors` for details.

    See Also
    --------
    :ref:`graph_constructors`.

    Notes
    -----
    The vertices in the first part are labeled from
    0 to ``num_vert1 - 1`` and the vertices of the
    second part are labeled from
    ``num_vert1`` to ``num_vert1 + num_vert2 - 1``.
    """

    if weights is not None or multiedges is not None:
        raise NotImplementedError()

    if num_vert1 <= 0 or num_vert2 <= 0:
        raise ValueError("There must be at least one vertex "
                         + "in each part.")

    # toy graph
    g = Graph(eye(num_vert1 + num_vert2).tocsr())

    # changes attributes
    del g._adj_matrix
    g._adj_matrix = None
    g._num_loops = 0

    g._num_vert1 = int(num_vert1)
    g._num_vert2 = int(num_vert2)
    g._num_vert = g._num_vert1 + g._num_vert2

    g.adjacent = MethodType(adjacent, g)
    g._entry = MethodType(_entry, g)
    g._find_entry = MethodType(_find_entry, g)
    g._neighbor_index = MethodType(_neighbor_index, g)
    g.neighbors = MethodType(neighbors, g)
    g.number_of_vertices = MethodType(number_of_vertices, g)
    g.number_of_edges = MethodType(number_of_edges, g)
    g.degree = MethodType(degree, g)
    g.adjacency_matrix = MethodType(adjacency_matrix, g)
    g.laplacian_matrix = MethodType(laplacian_matrix, g)

    return g
