# -*- cython -*-
#
# Tempita-templated Cython file
#
"""
Fast snippets for LIL matrices.
"""

from __future__ import absolute_import

{{py:

IDX_TYPES = {
    "int32": "cnp.npy_int32",
    "int64": "cnp.npy_int64",
}

VALUE_TYPES = {
    "bool_": "cnp.npy_bool",
    "int8": "cnp.npy_int8",
    "uint8": "cnp.npy_uint8",
    "int16": "cnp.npy_int16",
    "uint16": "cnp.npy_uint16",
    "int32": "cnp.npy_int32",
    "uint32": "cnp.npy_uint32",
    "int64": "cnp.npy_int64",
    "uint64": "cnp.npy_uint64",
    "float32": "cnp.npy_float32",
    "float64": "cnp.npy_float64",
    "longdouble": "long double",
    "complex64": "float complex",
    "complex128": "double complex",
    "clongdouble": "long double complex",
}

def get_dispatch(types):
    for pyname, cyname in types.items():
        yield pyname, cyname

def get_dispatch2(types, types2):
    for pyname, cyname in types.items():
        for pyname2, cyname2 in types2.items():
            yield pyname, pyname2, cyname, cyname2

def define_dispatch_map(map_name, prefix, types):
    result = ["cdef dict %s = {\n" % map_name]
    for pyname, cyname in types.items():
        a = "np.dtype(np.%s)" % (pyname,)
        b = prefix + "_" + pyname
        result.append('%s: %s,' % (a, b))
    result.append("}\n\n")
    return "\n".join(result)

def define_dispatch_map2(map_name, prefix, types, types2):
    result = ["cdef dict %s = {\n" % map_name]
    for pyname, cyname in types.items():
        for pyname2, cyname2 in types2.items():
            a = "(np.dtype(np.%s), np.dtype(np.%s))" % (pyname, pyname2)
            b = prefix + "_" + pyname + "_" + pyname2
            result.append('%s: %s,' % (a, b))
    result.append("}\n\n")
    return "\n".join(result)
}}

cimport cython
cimport numpy as cnp
import numpy as np


cnp.import_array()


@cython.wraparound(False)
cpdef lil_get1(cnp.npy_intp M, cnp.npy_intp N, object[:] rows, object[:] datas,
               cnp.npy_intp i, cnp.npy_intp j):
    """
    Get a single item from LIL matrix.

    Doesn't do output type conversion. Checks for bounds errors.

    Parameters
    ----------
    M, N, rows, datas
        Shape and data arrays for a LIL matrix
    i, j : int
        Indices at which to get

    Returns
    -------
    x
        Value at indices.

    """
    cdef list row, data

    if i < -M or i >= M:
        raise IndexError('row index (%d) out of bounds' % (i,))
    if i < 0:
        i += M

    if j < -N or j >= N:
        raise IndexError('column index (%d) out of bounds' % (j,))
    if j < 0:
        j += N

    row = rows[i]
    data = datas[i]
    cdef cnp.npy_intp pos = bisect_left(row, j)

    if pos != len(data) and row[pos] == j:
        return data[pos]
    else:
        return 0


@cython.wraparound(False)
cpdef int lil_insert(cnp.npy_intp M, cnp.npy_intp N, object[:] rows,
                     object[:] datas, cnp.npy_intp i, cnp.npy_intp j,
                     object x) except -1:
    """
    Insert a single item to LIL matrix.

    Checks for bounds errors and deletes item if x is zero.

    Parameters
    ----------
    M, N, rows, datas
        Shape and data arrays for a LIL matrix
    i, j : int
        Indices at which to get
    x
        Value to insert.

    """
    cdef list row, data

    if i < -M or i >= M:
        raise IndexError('row index (%d) out of bounds' % (i,))
    if i < 0:
        i += M

    if j < -N or j >= N:
        raise IndexError('column index (%d) out of bounds' % (j,))
    if j < 0:
        j += N

    row = rows[i]
    data = datas[i]

    cdef cnp.npy_intp pos = bisect_left(row, j)
    if x == 0:
        if pos < len(row) and row[pos] == j:
            del row[pos]
            del data[pos]
    else:
        if pos == len(row):
            row.append(j)
            data.append(x)
        elif row[pos] != j:
            row.insert(pos, j)
            data.insert(pos, x)
        else:
            data[pos] = x


def lil_fancy_get(cnp.npy_intp M, cnp.npy_intp N,
                  object[:] rows,
                  object[:] datas,
                  object[:] new_rows,
                  object[:] new_datas,
                  cnp.ndarray i_idx,
                  cnp.ndarray j_idx):
    """
    Get multiple items at given indices in LIL matrix and store to
    another LIL.

    Parameters
    ----------
    M, N, rows, data
        LIL matrix data, initially empty
    new_rows, new_idx
        Data for LIL matrix to insert to.
        Must be preallocated to shape `i_idx.shape`!
    i_idx, j_idx
        Indices of elements to insert to the new LIL matrix.

    """
    return _LIL_FANCY_GET_DISPATCH[i_idx.dtype](M, N, rows, datas, new_rows, new_datas, i_idx, j_idx)


{{for NAME, IDX_T in get_dispatch(IDX_TYPES)}}
def _lil_fancy_get_{{NAME}}(cnp.npy_intp M, cnp.npy_intp N,
                            object[:] rows,
                            object[:] datas,
                            object[:] new_rows,
                            object[:] new_datas,
                            {{IDX_T}}[:,:] i_idx,
                            {{IDX_T}}[:,:] j_idx):
    cdef cnp.npy_intp x, y
    cdef cnp.npy_intp i, j
    cdef object value
    cdef list new_row
    cdef list new_data

    for x in range(i_idx.shape[0]):
        new_row = []
        new_data = []

        for y in range(i_idx.shape[1]):
            i = i_idx[x,y]
            j = j_idx[x,y]

            value = lil_get1(M, N, rows, datas, i, j)

            if value is not 0:
                # Object identity as shortcut
                new_row.append(y)
                new_data.append(value)

        new_rows[x] = new_row
        new_datas[x] = new_data
{{endfor}}


{{define_dispatch_map('_LIL_FANCY_GET_DISPATCH', '_lil_fancy_get', IDX_TYPES)}}


def lil_fancy_set(cnp.npy_intp M, cnp.npy_intp N,
                  object[:] rows,
                  object[:] data,
                  cnp.ndarray i_idx,
                  cnp.ndarray j_idx,
                  cnp.ndarray values):
    """
    Set multiple items to a LIL matrix.

    Checks for zero elements and deletes them.

    Parameters
    ----------
    M, N, rows, data
        LIL matrix data
    i_idx, j_idx
        Indices of elements to insert to the new LIL matrix.
    values
        Values of items to set.

    """
    if values.dtype == np.bool_:
        # Cython doesn't support np.bool_ as a memoryview type
        values = values.view(dtype=np.uint8)

    assert i_idx.shape[0] == j_idx.shape[0] and i_idx.shape[1] == j_idx.shape[1]
    return _LIL_FANCY_SET_DISPATCH[i_idx.dtype, values.dtype](M, N, rows, data, i_idx, j_idx, values)


{{for PYIDX, PYVALUE, IDX_T, VALUE_T in get_dispatch2(IDX_TYPES, VALUE_TYPES)}}
@cython.boundscheck(False)
@cython.wraparound(False)
def _lil_fancy_set_{{PYIDX}}_{{PYVALUE}}(cnp.npy_intp M, cnp.npy_intp N,
                                         object[:] rows,
                                         object[:] data,
                                         {{IDX_T}}[:,:] i_idx,
                                         {{IDX_T}}[:,:] j_idx,
                                         {{VALUE_T}}[:,:] values):
    cdef cnp.npy_intp x, y
    cdef cnp.npy_intp i, j

    for x in range(i_idx.shape[0]):
        for y in range(i_idx.shape[1]):
            i = i_idx[x,y]
            j = j_idx[x,y]
            lil_insert(M, N, rows, data, i, j, values[x, y])
{{endfor}}


{{define_dispatch_map2('_LIL_FANCY_SET_DISPATCH', '_lil_fancy_set', IDX_TYPES, VALUE_TYPES)}}


def lil_get_row_ranges(cnp.npy_intp M, cnp.npy_intp N,
                       object[:] rows, object[:] datas,
                       object[:] new_rows, object[:] new_datas,
                       object irows,
                       cnp.npy_intp j_start,
                       cnp.npy_intp j_stop,
                       cnp.npy_intp j_stride,
                       cnp.npy_intp nj):
    """
    Column-slicing fast path for LIL matrices.
    Extracts values from rows/datas and inserts in to
    new_rows/new_datas.
    Parameters
    ----------
    M, N
         Shape of input array
    rows, datas
         LIL data for input array, shape (M, N)
    new_rows, new_datas
         LIL data for output array, shape (len(irows), nj)
    irows : iterator
         Iterator yielding row indices
    j_start, j_stop, j_stride
         Column range(j_start, j_stop, j_stride) to get
    nj : int
         Number of columns corresponding to j_* variables.
    """
    cdef cnp.npy_intp nk, k, j, a, b, m, r, p
    cdef list cur_row, cur_data, new_row, new_data

    if j_stride == 0:
        raise ValueError("cannot index with zero stride")

    for nk, k in enumerate(irows):
        if k >= M or k < -M:
            raise ValueError("row index %d out of bounds" % (k,))
        if k < 0:
            k += M

        if j_stride == 1 and nj == N:
            # full row slice
            new_rows[nk] = list(rows[k])
            new_datas[nk] = list(datas[k])
        else:
            # partial row slice
            cur_row = rows[k]
            cur_data = datas[k]
            new_row = new_rows[nk]
            new_data = new_datas[nk]

            if j_stride > 0:
                a = bisect_left(cur_row, j_start)
                for m in range(a, len(cur_row)):
                    j = cur_row[m]
                    if j >= j_stop:
                        break
                    r = (j - j_start) % j_stride
                    if r != 0:
                        continue
                    p = (j - j_start) // j_stride
                    new_row.append(p)
                    new_data.append(cur_data[m])
            else:
                a = bisect_right(cur_row, j_stop)
                for m in range(a, len(cur_row)):
                    j = cur_row[m]
                    if j > j_start:
                        break
                    r = (j - j_start) % j_stride
                    if r != 0:
                        continue
                    p = (j - j_start) // j_stride
                    new_row.insert(0, p)
                    new_data.insert(0, cur_data[m])


@cython.cdivision(True)
@cython.boundscheck(False)
@cython.wraparound(False)
cdef inline cnp.npy_intp bisect_left(list a, cnp.npy_intp x) except -1:
    """
    Bisection search in a sorted list.

    List is assumed to contain objects castable to integers.

    Parameters
    ----------
    a
        List to search in
    x
        Value to search for

    Returns
    -------
    j : int
        Index at value (if present), or at the point to which
        it can be inserted maintaining order.

    """
    cdef Py_ssize_t hi = len(a)
    cdef Py_ssize_t lo = 0
    cdef Py_ssize_t mid, v

    while lo < hi:
        mid = lo + (hi - lo) // 2
        v = a[mid]
        if v < x:
            lo = mid + 1
        else:
            hi = mid
    return lo


@cython.cdivision(True)
@cython.boundscheck(False)
@cython.wraparound(False)
cdef inline cnp.npy_intp bisect_right(list a, cnp.npy_intp x) except -1:
    """
    Bisection search in a sorted list.

    List is assumed to contain objects castable to integers.

    Parameters
    ----------
    a
        List to search in
    x
        Value to search for
    Returns
    -------
    j : int
        Index immediately at the right of the value (if present), or at
        the point to which it can be inserted maintaining order.
    """
    cdef cnp.npy_intp hi = len(a)
    cdef cnp.npy_intp lo = 0
    cdef cnp.npy_intp mid, v

    while lo < hi:
        mid = (lo + hi) // 2
        v = a[mid]
        if x < v:
            hi = mid
        else:
            lo = mid + 1
    return lo


cdef _fill_dtype_map(map, chars):
    """
    Fill in Numpy dtype chars for problematic types, working around
    Numpy < 1.6 bugs.
    """
    for c in chars:
        if c in "SUVO":
            continue
        dt = np.dtype(c)
        if dt not in map:
            for k, v in map.items():
                if k.kind == dt.kind and k.itemsize == dt.itemsize:
                    map[dt] = v
                    break


cdef _fill_dtype_map2(map):
    """
    Fill in Numpy dtype chars for problematic types, working around
    Numpy < 1.6 bugs.
    """
    for c1 in np.typecodes['Integer']:
        for c2 in np.typecodes['All']:
            if c2 in "SUVO":
                continue
            dt1 = np.dtype(c1)
            dt2 = np.dtype(c2)
            if (dt1, dt2) not in map:
                for k, v in map.items():
                    if (k[0].kind == dt1.kind and k[0].itemsize == dt1.itemsize and
                        k[1].kind == dt2.kind and k[1].itemsize == dt2.itemsize):
                        map[(dt1, dt2)] = v
                        break

_fill_dtype_map(_LIL_FANCY_GET_DISPATCH, np.typecodes['Integer'])
_fill_dtype_map2(_LIL_FANCY_SET_DISPATCH)
