import os

import numpy as np

from fury import actor
from fury.lib import FloatArray, Texture
from fury.shaders import (
    attribute_to_actor,
    compose_shader,
    import_fury_shader,
    shader_to_actor,
)
from fury.texture.utils import uv_calculations
from fury.utils import minmax_norm, numpy_to_vtk_image_data, set_polydata_tcoords


def sh_odf(centers, coeffs, degree, sh_basis, scales, opacity):
    """
    Visualize one or many ODFs with different features.

    Parameters
    ----------
    centers : ndarray(N, 3)
        ODFs positions.
    coeffs : ndarray
        2D ODFs array in SH coefficients.
    sh_basis: str, optional
        Type of basis (descoteaux, tournier)
        'descoteaux' for the default ``descoteaux07`` DIPY basis.
        'tournier' for the default ``tournier07`` DIPY basis.
    degree: int, optional
        Index of the highest used band of the spherical harmonics basis. Must
        be even, at least 2 and at most 12.
    scales : float or ndarray (N, )
        ODFs size.
    opacity : float
        Takes values from 0 (fully transparent) to 1 (opaque).

    Returns
    -------
    box_actor: Actor

    """
    odf_actor = actor.box(centers=centers, scales=scales)
    odf_actor.GetMapper().SetVBOShiftScaleMethod(False)
    odf_actor.GetProperty().SetOpacity(opacity)

    big_centers = np.repeat(centers, 8, axis=0)
    attribute_to_actor(odf_actor, big_centers, "center")

    minmax = np.array([coeffs.min(axis=1), coeffs.max(axis=1)]).T
    big_minmax = np.repeat(minmax, 8, axis=0)
    attribute_to_actor(odf_actor, big_minmax, "minmax")

    odf_actor_pd = odf_actor.GetMapper().GetInput()

    n_glyphs = coeffs.shape[0]
    # Coordinates to locate the data of each glyph in the texture.
    uv_vals = np.array(uv_calculations(n_glyphs))
    num_pnts = uv_vals.shape[0]

    # Definition of texture coordinates to be associated with the actor.
    t_coords = FloatArray()
    t_coords.SetNumberOfComponents(2)
    t_coords.SetNumberOfTuples(num_pnts)
    [t_coords.SetTuple(i, uv_vals[i]) for i in range(num_pnts)]

    set_polydata_tcoords(odf_actor_pd, t_coords)

    # The coefficient data is stored in a texture to be passed to the shaders.

    # Data is normalized to a range of 0 to 1.
    arr = minmax_norm(coeffs)
    # Data is turned into values within the RGB color range, and then converted
    # into a vtk image data.
    arr *= 255
    grid = numpy_to_vtk_image_data(arr.astype(np.uint8))

    # Vtk image data is associated to a texture.
    texture = Texture()
    texture.SetInputDataObject(grid)
    texture.Update()

    # Texture is associated with the actor
    odf_actor.GetProperty().SetTexture("texture0", texture)

    max_num_coeffs = coeffs.shape[-1]
    max_sh_degree = int((np.sqrt(8 * max_num_coeffs + 1) - 3) / 2)
    max_poly_degree = 2 * max_sh_degree + 2
    viz_sh_degree = max_sh_degree

    # The number of coefficients is associated to the order of the SH
    odf_actor.GetShaderProperty().GetFragmentCustomUniforms().SetUniformf(
        "shDegree", viz_sh_degree
    )

    # Start of shader implementation

    vs_dec = """
        uniform float shDegree;

        in vec3 center;
        in vec2 minmax;

        flat out float numCoeffsVSOutput;
        flat out float maxPolyDegreeVSOutput;
        out vec4 vertexMCVSOutput;
        out vec3 centerMCVSOutput;
        out vec2 minmaxVSOutput;
        out vec3 camPosMCVSOutput;
        out vec3 camRightMCVSOutput;
        out vec3 camUpMCVSOutput;
        """

    vs_impl = """
        numCoeffsVSOutput = (shDegree + 1) * (shDegree + 2) / 2;
        maxPolyDegreeVSOutput = 2 * shDegree + 2;
        vertexMCVSOutput = vertexMC;
        centerMCVSOutput = center;
        minmaxVSOutput = minmax;
        camPosMCVSOutput = -MCVCMatrix[3].xyz * mat3(MCVCMatrix);
        camRightMCVSOutput = vec3(
            MCVCMatrix[0][0], MCVCMatrix[1][0], MCVCMatrix[2][0]);
        camUpMCVSOutput = vec3(
            MCVCMatrix[0][1], MCVCMatrix[1][1], MCVCMatrix[2][1]);
        """

    shader_to_actor(odf_actor, "vertex", decl_code=vs_dec, impl_code=vs_impl)

    # The index of the highest used band of the spherical harmonics basis. Must
    # be even, at least 2 and at most 12.
    def_sh_degree = f"#define SH_DEGREE {max_sh_degree}"

    # The number of spherical harmonics basis functions
    def_sh_count = f"#define SH_COUNT {max_num_coeffs}"

    # Degree of polynomials for which we have to find roots
    def_max_degree = f"#define MAX_DEGREE {max_poly_degree}"

    # If GL_EXT_control_flow_attributes is available, these defines should be
    # defined as [[unroll]] and [[loop]] to give reasonable hints to the
    # compiler. That avoids register spilling, which makes execution
    # considerably faster.
    def_gl_ext_control_flow_attributes = """
        #ifndef _unroll_
            #define _unroll_
        #endif
        #ifndef _loop_
            #define _loop_
        #endif
        """

    # When there are fewer intersections/roots than theoretically possible,
    # some array entries are set to this value
    def_no_intersection = "#define NO_INTERSECTION 3.4e38"

    # pi and its reciprocal
    def_pis = """
        #define M_PI 3.141592653589793238462643
        #define M_INV_PI 0.318309886183790671537767526745
        """

    fs_vs_vars = """
        flat in float numCoeffsVSOutput;
        flat in float maxPolyDegreeVSOutput;
        in vec4 vertexMCVSOutput;
        in vec3 centerMCVSOutput;
        in vec2 minmaxVSOutput;
        in vec3 camPosMCVSOutput;
        in vec3 camRightMCVSOutput;
        in vec3 camUpMCVSOutput;
        """

    coeffs_norm = import_fury_shader(os.path.join("utils", "minmax_norm.glsl"))

    eval_sh_composed = ""
    for i in range(2, max_sh_degree + 1, 2):
        eval_sh = import_fury_shader(
            os.path.join("ray_tracing", "odf", sh_basis, "eval_sh_" + str(i) + ".frag")
        )
        eval_sh_grad = import_fury_shader(
            os.path.join(
                "ray_tracing", "odf", sh_basis, "eval_sh_grad_" + str(i) + ".frag"
            )
        )
        eval_sh_composed = compose_shader([eval_sh_composed, eval_sh, eval_sh_grad])

    # Searches a single root of a polynomial within a given interval.
    #   param out_root The location of the found root.
    #   param out_end_value The value of the given polynomial at end.
    #   param poly Coefficients of the polynomial for which a root should be
    #       found.
    #       Coefficient poly[i] is multiplied by x^i.
    #   param begin The beginning of an interval where the polynomial is
    #       monotonic.
    #   param end The end of said interval.
    #   param begin_value The value of the given polynomial at begin.
    #   param error_tolerance The error tolerance for the returned root
    #       location.
    #       Typically the error will be much lower but in theory it can be
    #       bigger.
    #
    #   return true if a root was found, false if no root exists.
    newton_bisection = import_fury_shader(
        os.path.join("utils", "newton_bisection.frag")
    )

    # Finds all roots of the given polynomial in the interval [begin, end] and
    # writes them to out_roots. Some entries will be NO_INTERSECTION but other
    # than that the array is sorted. The last entry is always NO_INTERSECTION.
    find_roots = import_fury_shader(os.path.join("utils", "find_roots.frag"))

    # Evaluates the spherical harmonics basis in bands 0, 2, ..., SH_DEGREE.
    # Conventions are as in the following paper.
    # M. Descoteaux, E. Angelino, S. Fitzgibbons, and R. Deriche. Regularized,
    # fast, and robust analytical q-ball imaging. Magnetic Resonance in
    # Medicine, 58(3), 2007. https://doi.org/10.1002/mrm.21277
    #   param outSH Values of SH basis functions in bands 0, 2, ...,
    #       SH_DEGREE in this order.
    #   param point The point on the unit sphere where the basis should be
    #       evaluated.
    eval_sh = import_fury_shader(os.path.join("ray_tracing", "odf", "eval_sh.frag"))

    # Evaluates the gradient of each basis function given by eval_sh() and the
    # basis itself
    eval_sh_grad = import_fury_shader(
        os.path.join("ray_tracing", "odf", "eval_sh_grad.frag")
    )

    # Outputs a matrix that turns equidistant samples on the unit circle of a
    # homogeneous polynomial into coefficients of that polynomial.
    get_inv_vandermonde = import_fury_shader(
        os.path.join("ray_tracing", "odf", "get_inv_vandermonde.frag")
    )

    # Determines all intersections between a ray and a spherical harmonics
    # glyph.
    #   param out_ray_params The ray parameters at intersection points. The
    #       points themselves are at ray_origin + out_ray_params[i] * ray_dir.
    #       Some entries may be NO_INTERSECTION but other than that the array
    #       is sorted.
    #   param sh_coeffs SH_COUNT spherical harmonic coefficients defining the
    #       glyph. Their exact meaning is defined by eval_sh().
    #   param ray_origin The origin of the ray, relative to the glyph center.
    #   param ray_dir The normalized direction vector of the ray.
    ray_sh_glyph_intersections = import_fury_shader(
        os.path.join("ray_tracing", "odf", "ray_sh_glyph_intersections.frag")
    )

    # Provides a normalized normal vector for a spherical harmonics glyph.
    #   param sh_coeffs SH_COUNT spherical harmonic coefficients defining the
    #       glyph. Their exact meaning is defined by eval_sh().
    #   param point A point on the surface of the glyph, relative to its
    #       center.
    #
    #   return A normalized surface normal pointing away from the origin.
    get_sh_glyph_normal = import_fury_shader(
        os.path.join("ray_tracing", "odf", "get_sh_glyph_normal.frag")
    )

    # Applies the non-linearity that maps linear RGB to sRGB
    linear_to_srgb = import_fury_shader(os.path.join("lighting", "linear_to_srgb.frag"))

    # Inverse of linear_to_srgb()
    srgb_to_linear = import_fury_shader(os.path.join("lighting", "srgb_to_linear.frag"))

    # Turns a linear RGB color (i.e. rec. 709) into sRGB
    linear_rgb_to_srgb = import_fury_shader(
        os.path.join("lighting", "linear_rgb_to_srgb.frag")
    )

    # Inverse of linear_rgb_to_srgb()
    srgb_to_linear_rgb = import_fury_shader(
        os.path.join("lighting", "srgb_to_linear_rgb.frag")
    )

    # Logarithmic tonemapping operator. Input and output are linear RGB.
    tonemap = import_fury_shader(os.path.join("lighting", "tonemap.frag"))

    # Blinn-Phong illumination model
    blinn_phong_model = import_fury_shader(
        os.path.join("lighting", "blinn_phong_model.frag")
    )

    # fmt: off
    fs_dec = compose_shader([
        def_sh_degree, def_sh_count, def_max_degree,
        def_gl_ext_control_flow_attributes, def_no_intersection, def_pis,
        fs_vs_vars, coeffs_norm, eval_sh_composed, newton_bisection, find_roots,
        eval_sh, eval_sh_grad, get_inv_vandermonde, ray_sh_glyph_intersections,
        get_sh_glyph_normal, blinn_phong_model, linear_to_srgb, srgb_to_linear,
        linear_rgb_to_srgb, srgb_to_linear_rgb, tonemap
    ])
    # fmt: on

    shader_to_actor(odf_actor, "fragment", decl_code=fs_dec)

    point_from_vs = "vec3 pnt = vertexMCVSOutput.xyz;"

    # Ray origin is the camera position in world space
    ray_origin = "vec3 ro = camPosMCVSOutput;"

    # Ray direction is the normalized difference between the fragment and the
    # camera position/ray origin
    ray_direction = "vec3 rd = normalize(pnt - ro);"

    # Light direction in a retroreflective model is the normalized difference
    # between the camera position/ray origin and the fragment
    light_direction = "vec3 ld = normalize(ro - pnt);"

    # Define SH coefficients (measured up to band 8, noise beyond that)
    sh_coeffs = """
        float i = 1 / (numCoeffsVSOutput * 2);
        float shCoeffs[SH_COUNT];
        for(int j=0; j < numCoeffsVSOutput; j++){
            shCoeffs[j] = rescale(
                texture(
                    texture0,
                    vec2(i + j / numCoeffsVSOutput, tcoordVCVSOutput.y)).x,
                    0, 1, minmaxVSOutput.x, minmaxVSOutput.y
            );
        }
        """

    # Perform the intersection test
    intersection_test = """
        float rayParams[MAX_DEGREE];
        rayGlyphIntersections(
            rayParams, shCoeffs, ro - centerMCVSOutput, rd, int(shDegree),
            int(numCoeffsVSOutput), int(maxPolyDegreeVSOutput), M_PI,
            NO_INTERSECTION
        );
        """

    # Identify the first intersection
    first_intersection = """
        float firstRayParam = NO_INTERSECTION;
        _unroll_
        for (int i = 0; i != maxPolyDegreeVSOutput; ++i) {
            if (rayParams[i] != NO_INTERSECTION && rayParams[i] > 0.0) {
                firstRayParam = rayParams[i];
                break;
            }
        }
        """

    # Evaluate shading for a directional light
    directional_light = """
        vec3 color = vec3(1.);
        if (firstRayParam != NO_INTERSECTION) {
            vec3 intersection = ro - centerMCVSOutput + firstRayParam * rd;
            vec3 normal = getShGlyphNormal(shCoeffs, intersection,
                          int(shDegree), int(numCoeffsVSOutput));
            vec3 colorDir = srgbToLinearRgb(abs(normalize(intersection)));
            float attenuation = dot(ld, normal);
            color = blinnPhongIllumModel(
                attenuation, lightColor0, colorDir, specularPower,
                specularColor, ambientColor);
        } else {
            discard;
        }
        """

    frag_output = """
        vec3 outColor = linearRgbToSrgb(tonemap(color));
        fragOutput0 = vec4(outColor, opacity);
        """

    fs_impl = compose_shader(
        [
            point_from_vs,
            ray_origin,
            ray_direction,
            light_direction,
            sh_coeffs,
            intersection_test,
            first_intersection,
            directional_light,
            frag_output,
        ]
    )

    shader_to_actor(odf_actor, "fragment", impl_code=fs_impl, block="picking")

    return odf_actor
