import numpy as np
from . import _spath


def shortest_path(arr, reach=1, axis=-1, output_indexlist=False):
    """Find the shortest path through an n-d array from one side to another.

    Parameters
    ----------
    arr : ndarray of float64
    reach : int, optional
        By default (``reach = 1``), the shortest path can only move
        one row up or down for every step it moves forward (i.e.,
        the path gradient is limited to 1). `reach` defines the
        number of elements that can be skipped along each non-axis
        dimension at each step.
    axis : int, optional
        The axis along which the path must always move forward (default -1)
    output_indexlist : bool, optional
        See return value `p` for explanation.

    Returns
    -------
    p : iterable of int
        For each step along `axis`, the coordinate of the shortest path.
        If `output_indexlist` is True, then the path is returned as a list of
        n-d tuples that index into `arr`. If False, then the path is returned
        as an array listing the coordinates of the path along the non-axis
        dimensions for each step along the axis dimension. That is,
        `p.shape == (arr.shape[axis], arr.ndim-1)` except that p is squeezed
        before returning so if `arr.ndim == 2`, then
        `p.shape == (arr.shape[axis],)`
    cost : float
        Cost of path.  This is the absolute sum of all the
        differences along the path.

    """
    # First: calculate the valid moves from any given position. Basically,
    # always move +1 along the given axis, and then can move anywhere within
    # a grid defined by the reach.
    if axis < 0:
        axis += arr.ndim
    offset_ind_shape = (2 * reach + 1,) * (arr.ndim - 1)
    offset_indices = np.indices(offset_ind_shape) - reach
    offset_indices = np.insert(offset_indices, axis, np.ones(offset_ind_shape), axis=0)
    offset_size = np.multiply.reduce(offset_ind_shape)
    offsets = np.reshape(offset_indices, (arr.ndim, offset_size), order='F').T

    # Valid starting positions are anywhere on the hyperplane defined by
    # position 0 on the given axis. Ending positions are anywhere on the
    # hyperplane at position -1 along the same.
    non_axis_shape = arr.shape[:axis] + arr.shape[axis + 1 :]
    non_axis_indices = np.indices(non_axis_shape)
    non_axis_size = np.multiply.reduce(non_axis_shape)
    start_indices = np.insert(non_axis_indices, axis, np.zeros(non_axis_shape), axis=0)
    starts = np.reshape(start_indices, (arr.ndim, non_axis_size), order='F').T
    end_indices = np.insert(
        non_axis_indices,
        axis,
        np.full(non_axis_shape, -1, dtype=non_axis_indices.dtype),
        axis=0,
    )
    ends = np.reshape(end_indices, (arr.ndim, non_axis_size), order='F').T

    # Find the minimum-cost path to one of the end-points
    m = _spath.MCP_Diff(arr, offsets=offsets)
    costs, traceback = m.find_costs(starts, ends, find_all_ends=False)

    # Figure out which end-point was found
    for end in ends:
        cost = costs[tuple(end)]
        if cost != np.inf:
            break
    traceback = m.traceback(end)

    if not output_indexlist:
        traceback = np.array(traceback)
        traceback = np.concatenate(
            [traceback[:, :axis], traceback[:, axis + 1 :]], axis=1
        )
        traceback = np.squeeze(traceback)

    return traceback, cost
