cimport h3lib
from .h3lib cimport bool, int64_t, H3int, H3ErrorCodes

from .util cimport (
    check_cell,
    check_res,
    check_distance,
)

from .error_system cimport (
    check_for_error,
    check_for_error_msg,
)

from .memory cimport (
    H3MemoryManager,
    int_mv,
)

# todo: add notes about Cython exception handling


# bool is a python type, so we don't need the except clause
cpdef bool is_valid_cell(H3int h):
    """Validates an H3 cell (hexagon or pentagon)

    Returns
    -------
    boolean
    """
    return h3lib.isValidCell(h) == 1


cpdef bool is_pentagon(H3int h):
    return h3lib.isPentagon(h) == 1


cpdef int get_base_cell_number(H3int h) except -1:
    check_cell(h)

    return h3lib.getBaseCellNumber(h)


cpdef int get_resolution(H3int h) except -1:
    """Returns the resolution of an H3 Index
    0--15
    """
    check_cell(h)

    return h3lib.getResolution(h)


cpdef int grid_distance(H3int h1, H3int h2) except -1:
    """ Compute the grid distance between two cells
    """
    cdef:
        int64_t distance

    check_cell(h1)
    check_cell(h2)

    check_for_error(
        h3lib.gridDistance(h1, h2, &distance)
    )

    return distance

cpdef H3int[:] grid_disk(H3int h, int k):
    """ Return cells at grid distance `<= k` from `h`.
    """
    cdef:
        int64_t n

    check_cell(h)
    check_distance(k)

    check_for_error(
        h3lib.maxGridDiskSize(k, &n)
    )

    hmm = H3MemoryManager(n)
    check_for_error(
        h3lib.gridDisk(h, k, hmm.ptr)
    )
    mv = hmm.to_mv()

    return mv


cpdef H3int[:] _ring_fallback(H3int h, int k):
    """
    `ring` tries to call `h3lib.hexRing` first; if that fails, we call
    this function, which relies on `h3lib.kRingDistances`.

    Failures for `h3lib.hexRing` happen when the algorithm runs into a pentagon.
    """
    cdef:
        int64_t n
        int[:] distances  ## todo: weird, this needs to be specified to avoid errors. cython bug?

    check_cell(h)
    check_distance(k)

    check_for_error(
        h3lib.maxGridDiskSize(k, &n)
    )
    hmm = H3MemoryManager(n)

    # parallel array of cell distances from `h`.
    # idea: instead of a memoryview object, have a my_mv object that can return a ptr, but does the correct logic when it has length 0
    # then can always just return the pointer, instead of the weird &mv[0] syntax
    distances = int_mv(n)
    check_for_error(
        h3lib.gridDiskDistances(h, k, hmm.ptr, &distances[0])
    )

    for i,v in enumerate(distances):
        if v != k:
            hmm.ptr[i] = 0

    mv = hmm.to_mv()

    return mv

cpdef H3int[:] grid_ring(H3int h, int k):
    """ Return cells at grid distance `== k` from `h`.
    Collection is "hollow" for k >= 1.
    """
    check_cell(h)
    check_distance(k)

    n = 6*k if k > 0 else 1
    hmm = H3MemoryManager(n)
    err = h3lib.gridRingUnsafe(h, k, hmm.ptr)
    if err:
        mv = _ring_fallback(h, k)
    else:
        mv = hmm.to_mv()

    return mv

cpdef H3int cell_to_parent(H3int h, res=None) except 0:
    cdef:
        H3int parent

    check_cell(h) # todo: do we want to check for validity here? or leave correctness to the user?

    if res is None:
        res = get_resolution(h) - 1

    err = h3lib.cellToParent(h, res, &parent)
    if err:
        msg = 'Invalid parent resolution {} for cell {}.'
        msg = msg.format(res, hex(h))
        check_for_error_msg(err, msg)

    return parent


cpdef H3int[:] cell_to_children(H3int h, res=None):
    cdef:
        int64_t n

    check_cell(h)

    if res is None:
        res = get_resolution(h) + 1

    err = h3lib.cellToChildrenSize(h, res, &n)
    if err:
        msg = 'Invalid child resolution {} for cell {}.'
        msg = msg.format(res, hex(h))
        check_for_error_msg(err, msg)

    hmm = H3MemoryManager(n)
    check_for_error(
        h3lib.cellToChildren(h, res, hmm.ptr)
    )
    mv = hmm.to_mv()

    return mv


cpdef H3int cell_to_center_child(H3int h, res=None) except 0:
    cdef:
        H3int child

    check_cell(h)

    if res is None:
        res = get_resolution(h) + 1

    err = h3lib.cellToCenterChild(h, res, &child)
    if err:
        msg = 'Invalid child resolution {} for cell {}.'
        msg = msg.format(res, hex(h))
        check_for_error_msg(err, msg)

    return child



cpdef H3int[:] compact_cells(const H3int[:] hu):
    # todo: fix this with my own Cython object "wrapper" class?
    #   everything has a .ptr interface?
    # todo: the Clib can handle 0-len arrays because it **avoids**
    # dereferencing the pointer, but Cython's syntax of
    # `&hu[0]` **requires** a dereference. For Cython, checking for array
    # length of zero and returning early seems like the easiest solution.
    # note: open to better ideas!

    if len(hu) == 0:
        return H3MemoryManager(0).to_mv()

    for h in hu: ## todo: should we have an array version? would that be faster?
        check_cell(h)

    cdef size_t n = len(hu)
    hmm = H3MemoryManager(n)
    check_for_error(
        h3lib.compactCells(&hu[0], hmm.ptr, n)
    )
    mv = hmm.to_mv()

    return mv


# todo: https://stackoverflow.com/questions/50684977/cython-exception-type-for-a-function-returning-a-typed-memoryview
# apparently, memoryviews are python objects, so we don't need to do the except clause
cpdef H3int[:] uncompact_cells(const H3int[:] hc, int res):
    # todo: the Clib can handle 0-len arrays because it **avoids**
    # dereferencing the pointer, but Cython's syntax of
    # `&hc[0]` **requires** a dereference. For Cython, checking for array
    # length of zero and returning early seems like the easiest solution.
    # note: open to better ideas!
    cdef:
        int64_t n


    if len(hc) == 0:
        return H3MemoryManager(0).to_mv()

    for h in hc:
        check_cell(h)

    check_for_error(
        h3lib.uncompactCellsSize(&hc[0], len(hc), res, &n)
    )

    hmm = H3MemoryManager(n)
    check_for_error(
        h3lib.uncompactCells(
            &hc[0], # todo: symmetry here with the wrapper object might be nice. hc.ptr / hc.n
            len(hc),
            hmm.ptr,
            hmm.n,
            res
        )
    )

    mv = hmm.to_mv()

    return mv


cpdef int64_t get_num_cells(int resolution) except -1:
    cdef:
        int64_t num_cells

    check_for_error(
        h3lib.getNumCells(resolution, &num_cells)
    )

    return num_cells


cpdef double average_hexagon_area(int resolution, unit='km^2') except -1:
    cdef:
        double area

    check_for_error(
        h3lib.getHexagonAreaAvgKm2(resolution, &area)
    )

    # todo: multiple units
    convert = {
        'km^2': 1.0,
        'm^2': 1000*1000.0
    }

    try:
        area *= convert[unit]
    except:
        raise ValueError('Unknown unit: {}'.format(unit))

    return area


cpdef double cell_area(H3int h, unit='km^2') except -1:
    cdef:
        double area

    if unit == 'rads^2':
        err = h3lib.cellAreaRads2(h, &area)
    elif unit == 'km^2':
        err = h3lib.cellAreaKm2(h, &area)
    elif unit == 'm^2':
        err = h3lib.cellAreaM2(h, &area)
    else:
        raise ValueError('Unknown unit: {}'.format(unit))

    check_for_error(err)

    return area


cdef _could_not_find_line(err, start, end):
    msg = "Couldn't find line between cells {} and {}"
    msg = msg.format(hex(start), hex(end))

    check_for_error_msg(err, msg)

cpdef H3int[:] grid_path_cells(H3int start, H3int end):
    cdef:
        int64_t n

    # todo: can we segfault here with invalid inputs?
    # Can we trust the c library to validate the start/end cells?
    # probably applies to all size/work pairs of functions...
    err = h3lib.gridPathCellsSize(start, end, &n)

    _could_not_find_line(err, start, end)

    hmm = H3MemoryManager(n)
    err = h3lib.gridPathCells(start, end, hmm.ptr)

    _could_not_find_line(err, start, end)

    # todo: probably here too?
    mv = hmm.to_mv()

    return mv

cpdef bool is_res_class_iii(H3int h):
    return h3lib.isResClassIII(h) == 1


cpdef H3int[:] get_pentagons(int res):
    n = h3lib.pentagonCount()

    hmm = H3MemoryManager(n)
    check_for_error(
        h3lib.getPentagons(res, hmm.ptr)
    )
    mv = hmm.to_mv()

    return mv


cpdef H3int[:] get_res0_cells():
    n = h3lib.res0CellCount()

    hmm = H3MemoryManager(n)
    check_for_error(
        h3lib.getRes0Cells(hmm.ptr)
    )
    mv = hmm.to_mv()

    return mv

# oh, this is returning a set??
# todo: convert to int[:]?
cpdef get_icosahedron_faces(H3int h):
    cdef:
        int n
        int[:] faces  ## todo: weird, this needs to be specified to avoid errors. cython bug?

    check_for_error(
        h3lib.maxFaceCount(h, &n)
    )

    faces = int_mv(n)
    check_for_error(
        h3lib.getIcosahedronFaces(h, &faces[0])
    )

    # todo: wait? do faces start from 0 or 1?
    # we could do this check/processing in the int_mv object
    out = [f for f in faces if f >= 0]

    return out


cpdef (int, int) cell_to_local_ij(H3int origin, H3int h) except *:
    cdef:
        h3lib.CoordIJ c

    err = h3lib.cellToLocalIj(origin, h, 0, &c)
    if err:
        msg = "Couldn't find local (i,j) between cells {} and {}."
        msg = msg.format(hex(origin), hex(h))
        check_for_error_msg(err, msg)

    return c.i, c.j

cpdef H3int local_ij_to_cell(H3int origin, int i, int j) except 0:
    cdef:
        h3lib.CoordIJ c
        H3int out

    c.i, c.j = i, j

    err = h3lib.localIjToCell(origin, &c, 0, &out)
    if err:
        msg = "Couldn't find cell at local ({},{}) from cell {}."
        msg = msg.format(i, j, hex(origin))
        check_for_error_msg(err, msg)

    return out
