import ddg
import numpy as np

import bpy
from functools import lru_cache


# [helper-functions]
##########################################
#  HELPER FUNCTIONS
##########################################


def circles(fct):
    """
    Returns a function that returns the discrete curvature circle at given index
    of the given discrete curve fct.
    The i'th curvature circle is given by the unique circle through the
    points fct(i-1), fct(i), fct(i+1).
    Domain: from 1 to n-1.
    """
    euc_3 = ddg.geometry.euclidean(3)

    @lru_cache(maxsize=128)
    def circle(i):
        c, r, n = ddg.math.euclidean.circle_through_three_points(
            fct(i - 1), fct(i), fct(i + 1), atol=1e-17
        )
        euc_circle = euc_3.sphere_from_affine_point_and_normals(c, r, n)
        return ddg.geometry.conversion.euclidean_sphere_to_quadric(euc_circle)

    return circle


def inverse_project_fct(fct, sphere):
    """
    Returns a function that returns the projection of
    a given discrete curve fct to the quadric `sphere`.
    Each vertex of the discrete curve is inverse stereographically
    projected to `sphere`.
    """

    @lru_cache(maxsize=128)
    def inverse_project_v(i):
        p = ddg.geometry.subspaces.Point(ddg.math.projective.homogenize(fct(i)))
        return ddg.geometry.projections.inverse_stereographic_project(p, quadric=sphere)

    return inverse_project_v


def m_circles(g, sphere):
    """
    Assumes, that the function g returns Points on the quadric `sphere`.
    Returns a function that returns the Moebius circles.
    The i'th Moebius circle is given by the intersection of the sphere with
    the plane spanned by the points fct(i-1), fct(i), fct(i+1).
    Domain: from 1 to n-1.
    """

    @lru_cache(maxsize=128)
    def m_circle(i):
        plane = ddg.geometry.intersection.join(g(i - 1), g(i), g(i + 1))
        return ddg.geometry.intersection.meet(plane, sphere)

    return m_circle


def project_fct(g, plane):
    """
    Assumes, that the function fct returns projective objects
    in RP3.
    Returns a function that projects the objects to the plane, resulting
    in planar objects of the same type.
    Domain: from 0 to n.
    """

    @lru_cache(maxsize=128)
    def project_obj(i):
        proj_on_plane = ddg.geometry.projections.stereographic_project(
            g(i), hyperplane=plane
        )
        return proj_on_plane

    return project_obj


# [helper-functions]

# [setup]
#############################################
# CLEAR AND ADD COLLECTION FOR STATIC OBJECTS
#############################################

# Animated objects will be linked to their own collections
ddg.visualization.blender.scene.clear(remove_collections=True)
static_coll = ddg.visualization.blender.collection.collection("static")

##########################################
# EUC PLANE AND MOB SPHERE
##########################################

plane_pt, plane_directions = [0, 0, -1], [[1, 0, 0], [0, 1, 0]]
plane = ddg.geometry.subspaces.subspace_from_affine_points_and_directions(
    plane_pt, plane_directions
)
north_pole = ddg.geometry.projections.north_pole(3)
sphere = ddg.geometry.quadrics.Quadric(np.diag([1, 1, 1, -1]))
# [setup]

# [curve-and-functions]
##########################################
# PLANE CURVE
##########################################
# (-a,0) and (a,0) are  the centers of circles which generate cardioid
a = 0.3
# translation of cardioid
b = [2.3, 0]


def f(t):
    return np.array(
        [
            2 * a * (1 - np.cos(t)) * np.cos(t) + b[0],
            2 * a * (1 - np.cos(t)) * np.sin(t) + b[1],
            -1,
        ]
    )


samples = 20  # 24 * 6 - 1
sampling_curve = [samples, "t"]
snet = ddg.SmoothNet(f, [[0, 2 * np.pi, True]])
dnet = ddg.sample_smooth_net(snet, sampling=sampling_curve)

##########################################
# FUNCTION CALLS
##########################################

# circles_fct(i) returns i'th curvature circle (as Quadric)
circles_fct = circles(dnet.fct)


# centers_fct(i) returns the center of the i'th curvature circle (as Point/Subspace)
@lru_cache(maxsize=128)
def centers_fct(i):
    circle = ddg.geometry.conversion.quadric_to_euclidean_sphere(circles_fct(i))
    return ddg.geometry.subspaces.Point(circle.center.point)


# spherical_fct(i) returns the inverse stereographic
# projection of the i'th vertex (as Point/Subspace)
spherical_fct = inverse_project_fct(dnet.fct, sphere)
# m_circles_fct(i) returns the i'th curvature circle on the Moebius sphere (as Quadric)
m_circles_fct = m_circles(spherical_fct, sphere)


# m_centers_fct(i) returns polar point of the plane corresponding
# to the i'th curvature circle (as Point/Subspace)
@lru_cache(maxsize=128)
def m_centers_fct(i):
    return sphere.polarize(m_circles_fct(i).subspace)


# Function returning the stereographic projection of the projective objects.
projected_vertices_fct = project_fct(spherical_fct, plane)
projected_m_circles_fct = project_fct(m_circles_fct, plane)
projected_m_centers_fct = project_fct(m_centers_fct, plane)
# [curve-and-functions]

# [visualization]
##########################################
#  VISUALIZATION
##########################################

# Materials
orange = ddg.visualization.blender.material.material("orange", (1, 0.026, 0))
blue = ddg.visualization.blender.material.material("blue", (0.019, 0.052, 0.445))
lightgreen = ddg.visualization.blender.material.material(
    "lightgreen", (0, 0.42, 0.078), 0, 0
)
transparent_cone = ddg.visualization.blender.material.material(
    "transparent_cone", (0.2, 0.154, 0.154), alpha=0.3
)
transparency_sphere = 0.8
transparent_sphere = ddg.visualization.blender.material.material(
    "transparent_sphere", (0, 0.168, 0.6), 0.5, alpha=transparency_sphere
)
transparent_plane = ddg.visualization.blender.material.material(
    "transparent_plane", (0, 0.05, 0.005), 0.4
)
transparent_m_planes = ddg.visualization.blender.material.material(
    "transparent_m_planes", (0.06, 0.06, 0.06), alpha=0.9
)

# Distinguish weather to use settings for a smooth
# or a discrete version of the animation.
# For more than 40 samples we interpret the
# curve as a smooth curve.
discrete_animation = samples < 40

# Sampling and size settings for objects to visualize
plane_size = (4.5, 4.5)

bevel_curvature_circles = 0.01 if discrete_animation else 0.008
bevel_curve = 0.01

sampling_general = [0.1, 100, "c"]
sampling_polar_plane = 1
point_size = 0.02
# [visualization]

# [animation-1]
##########################################
#  ANIMATION
##########################################
# Animation 1: Euclidean curvature circles
# Animation 2: Euclidean curvature circles with tracking of the evolute
# Animation 3: Fade in of the Moebius sphere
# Animation 4: Projection of planar vertices to the sphere
# Animation 5: Moebius circles as intersections of the sphere
# with planes through three consecutive points
# Animation 6: Project circles and polar points back to the plane
# Animation 7: Trace curvature circles on the plane and on the sphere

# For internal snapshot testing set animation_no=6
# For internal testing the max_samples are set low.
# For a qualitative good picture increase these, e.g. to 512
animation_no = 6
output_dir = "/tmp/"
max_samples = 8


# Static objects
# ----------------

# Plane
plane_bobj = ddg.to_blender_object_helper(
    plane,
    domain=[[-plane_size[0], plane_size[0]], [-plane_size[1], plane_size[1]]],
    sampling=[2, "t"],
    material=transparent_plane,
    name="plane",
    collection=static_coll,
)

# Curve
ddg.to_blender_object_helper(
    dnet,
    material=orange,
    curve_properties={"bevel_depth": bevel_curve},
    name="discrete curve",
    collection=static_coll,
)

# Moebius sphere
if animation_no > 2:
    sphere_bobj = ddg.to_blender_object_helper(
        sphere,
        sampling=sampling_general,
        material=transparent_sphere,
        shade_smooth=True,
        name="sphere",
        collection=static_coll,
    )

# Fade in of the Moebius sphere
if animation_no == 3:
    frames_fade_in_sphere = 4 * 24
    ddg.visualization.blender.animation.animate_opacity(
        transparent_sphere,
        0,
        frames_fade_in_sphere,
        frames_fade_in_sphere + 1,
        frames_fade_in_sphere + 1,
        opacity_outer=0,
        opacity_inner=transparency_sphere,
    )

# Moebius curve
# discrete: set of Points, smooth: DiscreteNet -> Blender Curve object
if animation_no > 4:
    if discrete_animation:
        for i in range(samples):
            ddg.to_blender_object_helper(
                spherical_fct(i),
                material=orange,
                sphere_radius=point_size,
                name=f"spherical point {i}",
                collection=static_coll,
            )
    else:

        def spherical_fct_affine(i):
            return spherical_fct(i).affine_point

        ddg.to_blender_object_helper(
            ddg.nets.DiscreteNet(spherical_fct_affine, [[0, samples]]),
            material=orange,
            curve_properties={"bevel_depth": bevel_curve},
            name="spherical curve",
            collection=static_coll,
        )


# [animation-1]

# [animation-2]
def blender_objects(i):
    """
    Return list of Blender objects to animate.
    """

    pts = [
        ddg.geometry.subspaces.subspace_from_affine_points(p)
        for p in [dnet(i - 1), dnet(i), dnet(i + 1)]
    ]
    bobjs = []

    ##########################################
    #  PLANAR
    ##########################################

    # Vertices of the curve
    if animation_no in [1, 2, 4, 5] and discrete_animation:
        pts_tmp = [pts[1]] if animation_no == 4 else pts
        for p in pts_tmp:
            bobjs.append(
                ddg.to_blender_object_helper(
                    p,
                    sphere_radius=point_size,
                    material=orange,
                    name="animated planar point",
                    link=False,
                )
            )

    # Curvature circles
    if animation_no in [1, 2, 6, 7]:
        link = True if animation_no == 7 else False
        bobj_circle = ddg.to_blender_object_helper(
            circles_fct(i),
            sampling=sampling_general,
            curve_properties={"bevel_depth": bevel_curvature_circles},
            material=blue,
            name=f"animated circle {i}",
            link=link,
        )
        if not link:
            bobjs.append(bobj_circle)

    # Curvature circe center and evolute trace
    if animation_no in [2, 6, 7]:
        # Curvature circle center
        bobjs.append(
            ddg.to_blender_object_helper(
                centers_fct(i),
                sphere_radius=point_size,
                material=lightgreen,
                name=f"animated center {i}",
                link=False,
            )
        )

        # Evolute curve
        def centers_fct_affine(i):
            return centers_fct(i).affine_point

        bobjs.append(
            ddg.to_blender_object_helper(
                ddg.nets.DiscreteNet(centers_fct_affine, [[0, i]]),
                material=lightgreen,
                curve_properties={"bevel_depth": bevel_curve},
                name=f"evolute {i}",
                link=False,
            )
        )

    #########################################
    #  PROJECTION TO THE SPHERE
    ##########################################

    # Projection to sphere visualized as line
    if animation_no in [4, 5, 6]:
        pts_tmp = [pts[1]] if animation_no == 4 else pts
        # Line as join of the North Pole and curve's vertex
        if animation_no == 4 or discrete_animation:
            for p in pts_tmp:
                bobjs.append(
                    ddg.to_blender_object_helper(
                        ddg.geometry.intersection.join(north_pole, p),
                        sampling=sampling_general,
                        domain=[[0, 1]],
                        material=orange,
                        curve_properties={"bevel_depth": bevel_curvature_circles},
                        name=f"spherical projection {i}",
                        link=False,
                    )
                )

    # Track of Moebius curve, discrete: set of Points, smooth: DiscreteNet
    if animation_no in [4]:
        if discrete_animation:
            ddg.to_blender_object_helper(
                spherical_fct(i),
                material=orange,
                sphere_radius=point_size,
                name=f"spherical point {i}",
                # link=False,
            )
        else:

            def spherical_fct_affine(i):
                return spherical_fct(i).affine_point

            bobjs.append(
                ddg.to_blender_object_helper(
                    ddg.nets.DiscreteNet(spherical_fct_affine, [[0, i]]),
                    material=orange,
                    curve_properties={"bevel_depth": bevel_curve},
                    name=f"spherical curve {i}",
                    link=False,
                )
            )

    ##########################################
    #  MOEBIUS PLANES AND CIRCLES
    ##########################################
    # Catch if a circle degenerates in a cusp of the curve
    non_degenerate = ddg.math.symmetric_matrices.Signature(2, 1, 0)
    if m_circles_fct(i).signature() == non_degenerate:
        # Moebius planes
        if animation_no in [5]:
            center = ddg.geometry.conversion.quadric_to_euclidean_sphere(
                m_circles_fct(i)
            ).center
            plane = m_circles_fct(i).subspace.orthonormalize_and_center(center=center)
            bobjs.append(
                ddg.to_blender_object_helper(
                    plane,
                    sampling=sampling_polar_plane,
                    bounding=3,
                    material=transparent_m_planes,
                    name=f"Moebius plane {i}",
                    link=False,
                )
            )

        # Moebius curvature circle
        if animation_no in [5, 6, 7]:
            # Track in animation_no = 7
            link = True if animation_no == 7 else False
            bobj_m_circle = ddg.to_blender_object_helper(
                m_circles_fct(i),
                sampling=sampling_general,
                curve_properties={"bevel_depth": bevel_curvature_circles},
                material=blue,
                name=f"Moebius curvature circle {i}",
                link=link,
            )
            if not link:
                bobjs.append(bobj_m_circle)

        # Moebius polar point
        if animation_no in [5, 6, 7]:
            # Track in animation_no = 7
            link = True if animation_no == 7 else False
            bobj_m_center = ddg.to_blender_object_helper(
                m_centers_fct(i),
                material=lightgreen,
                sphere_radius=point_size,
                name=f"Moebius polar point {i}",
                link=link,
            )
            if not link:
                bobjs.append(bobj_m_center)

    ##########################################
    #  PROJECT BACK
    ##########################################
    # Projection cone and line
    if animation_no in [6]:
        cone = ddg.geometry.intersection.join(north_pole, circles_fct(i))

        # Cut cone on one side
        def bb_trafo(bmesh):
            ddg.visualization.blender.bmesh.cut_bounding_box(
                bmesh, np.array([3, 3, 3]), np.array([3, 0, 0])
            )

        # Projection cone
        bobjs.append(
            ddg.to_blender_object_helper(
                cone,
                sampling=sampling_general,
                material=transparent_cone,
                bmesh_transformations=[bb_trafo],
                shade_smooth=True,
                name=f"projection cone {i}",
                link=False,
            )
        )
        # Projection line
        line = ddg.geometry.intersection.join(north_pole, centers_fct(i))
        bobjs.append(
            ddg.to_blender_object_helper(
                line,
                sampling=1,
                domain=[[0, 1]],
                material=lightgreen,
                shade_smooth=True,
                curve_properties={"bevel_depth": bevel_curve},
                name=f"projection line {i}",
                link=False,
            )
        )

    return bobjs


# [animation-2]

# [animation-3]
##########################################
# Animation Setup
##########################################

ddg.visualization.blender.animation.clear_animation_data(bpy.context.scene)

callback = ddg.visualization.blender.props.clear_callback(
    "animated objects", blender_objects
)
ddg.visualization.blender.props.add_props_with_callback(callback, ("i"), 0)

ddg.visualization.blender.animation.set_keyframe(bpy.context.scene, 0, "i", 0)
ddg.visualization.blender.animation.set_keyframe(
    bpy.context.scene, samples, "i", samples
)

bpy.context.scene.frame_end = samples if animation_no != 3 else frames_fade_in_sphere
bpy.context.scene.frame_start = 0
bpy.context.scene.frame_current = 0

# [animation-3]

# [rendering-setup]
##########################################
# RENDERING SETUP
##########################################

# Add light and camera
cam = ddg.visualization.blender.camera.camera(location=(5.1, -2, 1.5))
ddg.visualization.blender.camera.look_at_point(cam, (0, 0.5, -1))
light = ddg.visualization.blender.light.light(location=(4, 0, 20), energy=1)
ddg.visualization.blender.light.look_at_point(light, (5, 0, 0))
light.data.angle = 0.17

# Setup rendering
ddg.visualization.blender.render.setup_cycles_renderer(
    scene=None,
    device="CPU",
    noise_threshold=0.01,
    min_samples=0,
    max_samples=max_samples,
    time_limit=0,
    denoise=True,
)
ddg.visualization.blender.render.set_world_background((0.7, 0.7, 0.7, 1), 1)
bpy.context.scene.render.resolution_x = 1500
bpy.context.scene.render.resolution_y = 1500
ddg.visualization.blender.render.set_film_transparency()
bpy.context.scene.view_settings.view_transform = "Standard"
ddg.visualization.blender.render.set_render_output_images(
    output_dir,
    time=False,
    file_format="PNG",
    alpha=True,
)

# [rendering-setup]
##############################################
# SNAPSHOT TESTS  (for internal pyddg testing)
##############################################

from testing.tests.examples.blender.snapshot import opt_in  # noqa: E402

# choose camera for snapshot testing
bpy.context.scene.camera = cam
opt_in()
