# Helpers for current-flow betweenness and current-flow closness
# Lazy computations for inverse Laplacian and flow-matrix rows.
import networkx as nx

def flow_matrix_row(G, weight='weight', dtype=float, solver='lu'):
    # Generate a row of the current-flow matrix
    import numpy as np
    from scipy import sparse
    from scipy.sparse import linalg
    solvername={"full" :FullInverseLaplacian,
                "lu": SuperLUInverseLaplacian,
                "cg": CGInverseLaplacian}
    n = G.number_of_nodes()
    L = laplacian_sparse_matrix(G, nodelist=range(n), weight=weight, 
                                dtype=dtype, format='csc')
    C = solvername[solver](L, dtype=dtype) # initialize solver
    w = C.w # w is the Laplacian matrix width
    # row-by-row flow matrix
    for u,v,d in G.edges_iter(data=True):
        B = np.zeros(w, dtype=dtype)
        c = d.get(weight,1.0)
        B[u%w] = c
        B[v%w] = -c
        # get only the rows needed in the inverse laplacian 
        # and multiply to get the flow matrix row
        row = np.dot(B, C.get_rows(u,v))  
        yield row,(u,v) 


# Class to compute the inverse laplacian only for specified rows
# Allows computation of the current-flow matrix without storing entire
# inverse laplacian matrix
class InverseLaplacian(object):
    def __init__(self, L, width=None, dtype=None):
        global np
        import numpy as np
        (n,n) = L.shape
        self.dtype = dtype
        self.n = n
        if width is None:
            self.w = self.width(L)
        else:
            self.w = width
        self.C = np.zeros((self.w,n), dtype=dtype)
        self.L1 = L[1:,1:]
        self.init_solver(L)

    def init_solver(self,L):
        pass

    def solve(self,r):
        raise("Implement solver")

    def solve_inverse(self,r):
        raise("Implement solver")


    def get_rows(self, r1, r2):
        for r in range(r1, r2+1):
            self.C[r%self.w, 1:] = self.solve_inverse(r)
        return self.C

    def get_row(self, r):
        self.C[r%self.w, 1:] = self.solve_inverse(r)
        return self.C[r%self.w]


    def width(self,L):
        m=0
        for i,row in enumerate(L):
            w=0
            x,y = np.nonzero(row)
            if len(y) > 0:
                v = y-i
                w=v.max()-v.min()+1
                m = max(w,m)
        return m

class FullInverseLaplacian(InverseLaplacian):
    def init_solver(self,L):
        self.IL = np.zeros(L.shape, dtype=self.dtype)
        self.IL[1:,1:] = np.linalg.inv(self.L1.todense())

    def solve(self,rhs):
        s = np.zeros(rhs.shape, dtype=self.dtype)
        s = np.dot(self.IL,rhs)
        return s

    def solve_inverse(self,r):
        return self.IL[r,1:]


class SuperLUInverseLaplacian(InverseLaplacian):
    def init_solver(self,L):
        from scipy.sparse import linalg
        self.lusolve = linalg.factorized(self.L1.tocsc())

    def solve_inverse(self,r):
        rhs = np.zeros(self.n, dtype=self.dtype)
        rhs[r]=1
        return self.lusolve(rhs[1:])

    def solve(self,rhs):
        s = np.zeros(rhs.shape, dtype=self.dtype)
        s[1:]=self.lusolve(rhs[1:])
        return s



class CGInverseLaplacian(InverseLaplacian):
    def init_solver(self,L):
        global linalg
        from scipy.sparse import linalg
        ilu= linalg.spilu(self.L1.tocsc())
        n=self.n-1
        self.M = linalg.LinearOperator(shape=(n,n), matvec=ilu.solve)

    def solve(self,rhs):
        s = np.zeros(rhs.shape, dtype=self.dtype)
        s[1:]=linalg.cg(self.L1, rhs[1:], M=self.M)[0]
        return s

    def solve_inverse(self,r):
        rhs = np.zeros(self.n, self.dtype)
        rhs[r] = 1
        return linalg.cg(self.L1, rhs[1:], M=self.M)[0]


# graph laplacian, sparse version, will move to linalg/laplacianmatrix.py
def laplacian_sparse_matrix(G, nodelist=None, weight='weight', dtype=None,
                            format='csr'):
    import numpy as np
    import scipy.sparse
    A = nx.to_scipy_sparse_matrix(G, nodelist=nodelist, weight=weight, 
                                  dtype=dtype, format=format)
    (n,n) = A.shape
    data = np.asarray(A.sum(axis=1).T)
    D = scipy.sparse.spdiags(data,0,n,n, format=format)
    return  D - A
