from typing import Tuple

import numpy as np

from seqgen.pypulseq.opts import Opts


def calc_ramp(
    k0: np.ndarray,
    k_end: np.ndarray,
    max_grad: np.ndarray = np.zeros(0),
    max_points: int = 500,
    max_slew: np.ndarray = np.zeros(0),
    system: Opts = Opts(),
) -> Tuple[np.ndarray, bool]:
    """
    Join the points `k0` and `k_end` in three-dimensional  k-space in minimal time, observing the gradient and slew
    limits (`max_grad` and `max_slew` respectively), and the gradient strength `G0` before `k0[:, 1]` and `Gend` after
    `k_end[:, 1]`. In the context of a fixed gradient dwell time this is a discrete problem with an a priori unknown
    number of discretization steps. Therefore this method tries out the optimization with 0 steps, then 1 step, and so
    on, until  all conditions can be fulfilled, thus yielding a short connection.

    Parameters
    ----------
    k0 : numpy.ndarray
        Two preceding points in k-space. Shape is `[3, 2]`. From these points, the starting gradient will be calculated.
    k_end : numpy.ndarray
        Two following points in k-space. Shape is `[3, 2]`. From these points, the target gradient will be calculated.
    max_grad : float or array_like, default=0
        Maximum total gradient strength. Either a single value or one value for each coordinate, of shape `[3, 1]`.
    max_points : int, default=500
        Maximum number of k-space points to be used in connecting `k0` and `k_end`.
    max_slew : float or array_like, default=0
        Maximum total slew rate. Either a single value or one value for each coordinate, of shape `[3, 1]`.
    system : Opts, default=Opts()
        System limits.

    Returns
    -------
    k_out : numpy.ndarray
        Connected k-space trajectory.
    success : bool
        Boolean flag indicating if `k0` and `k_end` were successfully joined.
    """

    def __inside_limits(grad, slew):
        if mode == 0:
            grad2 = np.sum(np.square(grad), axis=1)
            slew2 = np.sum(np.square(slew), axis=1)
            ok = np.all(np.max(grad2) <= np.square(max_grad)) and np.all(
                np.max(slew2) <= np.square(max_slew)
            )
        else:
            ok = (np.sum(np.max(np.abs(grad), axis=1) <= max_grad) == 3) and (
                np.sum(np.max(np.abs(slew), axis=1) <= max_slew) == 3
            )

        return ok

    def __joinleft0(k0, k_end, use_points, G0, G_end):
        if use_points == 0:
            G = np.stack((G0, (k_end - k0) / grad_raster, G_end)).T
            S = (G[:, 1:] - G[:, :-1]) / grad_raster

            k_out_left = np.zeros((3, 0))
            success = __inside_limits(G, S)

            return success, k_out_left

        dk = (k_end - k0) / (use_points + 1)
        kopt = k0 + dk
        Gopt = (kopt - k0) / grad_raster
        Sopt = (Gopt - G0) / grad_raster

        okGopt = np.sum(np.square(Gopt)) <= np.square(max_grad)
        okSopt = np.sum(np.square(Sopt)) <= np.square(max_slew)

        if okGopt and okSopt:
            k_left = kopt
        else:
            a = np.multiply(max_grad, grad_raster)
            b = np.multiply(max_slew, grad_raster**2)

            dkprol = G0 * grad_raster
            dkconn = dk - dkprol

            ksl = k0 + dkprol + dkconn / np.linalg.norm(dkconn) * b
            Gsl = (ksl - k0) / grad_raster
            okGsl = np.sum(np.square(Gsl)) <= np.square(max_grad)

            kgl = k0 + np.multiply(dk / np.linalg.norm(dk), a)
            Ggl = (kgl - k0) / grad_raster
            Sgl = (Ggl - G0) / grad_raster
            okSgl = np.sum(np.square(Sgl)) <= np.square(max_slew)

            if okGsl:
                k_left = ksl
            elif okSgl:
                k_left = kgl
            else:
                c = np.linalg.norm(dkprol)
                c1 = np.divide(np.square(a) - np.square(b) + np.square(c), (2 * c))
                h = np.sqrt(np.square(a) - np.square(c1))
                kglsl = k0 + np.multiply(c1, np.divide(dkprol, np.linalg.norm(dkprol)))
                projondkprol = (kgl * dkprol.T) * (dkprol / np.linalg.norm(dkprol))
                hdirection = kgl - projondkprol
                kglsl = kglsl + h * hdirection / np.linalg.norm(hdirection)
                k_left = kglsl

        success, k = __joinright0(
            k_left, k_end, (k_left - k0) / grad_raster, G_end, use_points - 1
        )
        if len(k) != 0:
            if len(k.shape) == 1:
                k = k.reshape((len(k), 1))
            if len(k_left.shape) == 1:
                k_left = k_left.reshape((len(k_left), 1))
            k_out_left = np.hstack((k_left, k))
        else:
            k_out_left = k_left

        return success, k_out_left

    def __joinleft1(k0, k_end, use_points, G0, G_end):
        if use_points == 0:
            G = np.stack((G0, (k_end - k0) / grad_raster, G_end))
            S = (G[:, 1:] - G[:, :-1]) / grad_raster

            k_out_left = np.zeros((3, 0))
            success = __inside_limits(G, S)

            return success, k_out_left

        k_left = np.zeros(3)

        dk = (k_end - k0) / (use_points + 1)
        kopt = k0 + dk
        Gopt = (kopt - k0) / grad_raster
        Sopt = (Gopt - G0) / grad_raster

        okGopt = np.abs(Gopt) <= max_grad
        okSopt = np.abs(Sopt) <= max_slew

        dkprol = G0 * grad_raster
        dkconn = dk - dkprol

        ksl = k0 + dkprol + np.multiply(np.sign(dkconn), max_slew) * grad_raster**2
        Gsl = (ksl - k0) / grad_raster
        okGsl = np.abs(Gsl) <= max_grad

        kgl = k0 + np.multiply(np.sign(dk), max_grad) * grad_raster**2
        Ggl = (kgl - k0) / grad_raster
        Sgl = (Ggl - G0) / grad_raster
        okSgl = np.abs(Sgl) <= max_slew

        for ii in range(3):
            if okGopt[ii] == 1 and okSopt[ii] == 1:
                k_left[ii] = kopt[ii]
            elif okGsl[ii] == 1:
                k_left[ii] = ksl[ii]
            elif okSgl[ii] == 1:
                k_left[ii] = kgl[ii]
            else:
                print("Unknown error")

        success, k = __joinright1(
            k0=k_left,
            k_end=k_end,
            use_points=use_points - 1,
            G0=(k_left - k0) / grad_raster,
            G_end=G_end,
        )
        if len(k) != 0:
            if len(k.shape) == 1:
                k = k.reshape((len(k), 1))
            if len(k_left.shape) == 1:
                k_left = k_left.reshape((len(k_left), 1))
            k_out_left = np.hstack((k_left, k))
        else:
            k_out_left = k_left

        return success, k_out_left

    def __joinright0(k0, k_end, use_points, G0, G_end):
        if use_points == 0:
            G = np.stack((G0, (k_end - k0) / grad_raster, G_end)).T
            S = (G[:, 1:] - G[:, :-1]) / grad_raster

            k_out_right = np.zeros((3, 0))
            success = __inside_limits(G, S)

            return success, k_out_right

        dk = (k0 - k_end) / (use_points + 1)
        kopt = k_end + dk
        Gopt = (k_end - kopt) / grad_raster
        Sopt = (G_end - Gopt) / grad_raster

        okGopt = np.sum(np.square(Gopt)) <= np.square(max_grad)
        okSopt = np.sum(np.square(Sopt)) <= np.square(max_slew)

        if okGopt and okSopt:
            k_right = kopt
        else:
            a = np.multiply(max_grad, grad_raster)
            b = np.multiply(max_slew, grad_raster**2)

            dkprol = -G_end * grad_raster
            dkconn = dk - dkprol

            ksl = k_end + dkprol + dkconn / np.linalg.norm(dkconn) * b
            Gsl = (k_end - ksl) / grad_raster
            okGsl = np.sum(np.square(Gsl)) <= np.square(max_grad)

            kgl = k_end + np.multiply(dk / np.linalg.norm(dk), a)
            Ggl = (k_end - kgl) / grad_raster
            Sgl = (G_end - Ggl) / grad_raster
            okSgl = np.sum(np.square(Sgl)) <= np.square(max_slew)

            if okGsl:
                k_right = ksl
            elif okSgl:
                k_right = kgl
            else:
                c = np.linalg.norm(dkprol)
                c1 = np.divide(np.square(a) - np.square(b) + np.square(c), (2 * c))
                h = np.sqrt(np.square(a) - np.square(c1))
                kglsl = k_end + np.multiply(
                    c1, np.divide(dkprol, np.linalg.norm(dkprol))
                )
                projondkprol = (kgl * dkprol.T) * (dkprol / np.linalg.norm(dkprol))
                hdirection = kgl - projondkprol
                kglsl = kglsl + h * hdirection / np.linalg.norm(hdirection)
                k_right = kglsl

        success, k = __joinleft0(
            k0=k0,
            k_end=k_right,
            G0=G0,
            G_end=(k_end - k_right) / grad_raster,
            use_points=use_points - 1,
        )
        if len(k) != 0:
            if len(k.shape) == 1:
                k = k.reshape((len(k), 1))
            if len(k_right.shape) == 1:
                k_right = k_right.reshape((len(k_right), 1))
            k_out_right = np.hstack((k, k_right))
        else:
            k_out_right = k_right

        return success, k_out_right

    def __joinright1(k0, k_end, use_points, G0, G_end):
        if use_points == 0:
            G = np.stack((G0, (k_end - k0) / grad_raster, G_end))
            S = (G[:, 1:] - G[:, :-1]) / grad_raster

            k_out_right = np.zeros((3, 0))
            success = __inside_limits(G, S)

            return success, k_out_right

        k_right = np.zeros(3)

        dk = (k0 - k_end) / (use_points + 1)
        kopt = k_end + dk
        Gopt = (k_end - kopt) / grad_raster
        Sopt = (G_end - Gopt) / grad_raster

        okGopt = np.abs(Gopt) <= max_grad
        okSopt = np.abs(Sopt) <= max_slew

        dkprol = -G_end * grad_raster
        dkconn = dk - dkprol

        ksl = k_end + dkprol + np.multiply(np.sign(dkconn), max_slew) * grad_raster**2
        Gsl = (k_end - ksl) / grad_raster
        okGsl = np.abs(Gsl) <= max_grad

        kgl = k_end + np.multiply(np.sign(dk), max_grad) * grad_raster
        Ggl = (k_end - kgl) / grad_raster
        Sgl = (G_end - Ggl) / grad_raster
        okSgl = np.abs(Sgl) <= max_slew

        for ii in range(3):
            if okGopt[ii] == 1 and okSopt[ii] == 1:
                k_right[ii] = kopt[ii]
            elif okGsl[ii] == 1:
                k_right[ii] = ksl[ii]
            elif okSgl[ii] == 1:
                k_right[ii] = kgl[ii]
            else:
                print("Unknown error")

        success, k = __joinleft1(
            k0=k0,
            k_end=k_right,
            use_points=use_points - 1,
            G0=G0,
            G_end=(k_end - k_right) / grad_raster,
        )
        if len(k) != 0:
            if len(k.shape) == 1:
                k = k.reshape((len(k), 1))
            if len(k_right.shape) == 1:
                k_right = k_right.reshape((len(k_right), 1))
            k_out_right = np.hstack((k, k_right))
        else:
            k_out_right = k_right

        return success, k_out_right

    # =========
    # MAIN FUNCTION
    # =========
    if np.all(np.where(max_grad <= 0)):
        max_grad = [system.max_grad]
    if np.all(np.where(max_slew <= 0)):
        max_slew = [system.max_slew]

    grad_raster = system.grad_raster_time

    if len(max_grad) == 1 and len(max_slew) == 1:
        mode = 0
    elif len(max_grad) == 3 and len(max_slew) == 3:
        mode = 1
    else:
        raise ValueError("Input value max grad or max slew in invalid format.")

    G0 = (k0[:, 1] - k0[:, 0]) / grad_raster
    G_end = (k_end[:, 1] - k_end[:, 0]) / grad_raster
    k0 = k0[:, 1]
    k_end = k_end[:, 0]

    success = 0
    k_out = np.zeros((3, 0))
    use_points = 0

    while success == 0 and use_points <= max_points:
        if mode == 0:
            if np.linalg.norm(G0) > max_grad or np.linalg.norm(G_end) > max_grad:
                break
            success, k_out = __joinleft0(
                k0=k0, k_end=k_end, G0=G0, G_end=G_end, use_points=use_points
            )
        else:
            if np.abs(G0) > np.abs(max_grad) or np.abs(G_end) > np.abs(max_grad):
                break
            success, k_out = __joinleft1(
                k0=k0, k_end=k_end, use_points=use_points, G0=G0, G_end=G_end
            )
        use_points += 1

    return k_out, success
