import warnings

import bpy
import numpy as np

import ddg
from ddg.geometry.conversion import quadric_to_subspaces
from ddg.geometry.quadrics import Quadric, intersect_quadric_subspace, polarize
from ddg.geometry.subspaces import Subspace, join_subspaces, reflect_in_hyperplane
from ddg.math.projective import homogenize
from ddg.visualization.blender.animation import set_keyframe
from ddg.visualization.blender.camera import camera
from ddg.visualization.blender.collection import clear, collection
from ddg.visualization.blender.light import light
from ddg.visualization.blender.material import material
from ddg.visualization.blender.render import (
    set_film_transparency,
    set_render_output_images,
    set_render_stamp_note,
    setup_eevee_renderer,
)


# [billiard-map]
def billard_map(conic, incidence_chord):
    """
    Returns a reflected point given incidence chord of a conic.

    Parameters
    ----------
    conic : ddg.geometry.quadrics.Quadric
        A conic in projective space.
    incidence_chord : Tuple of two ddg.geometry.subspaces.Subspace.Point
        A chord defined by two points on conic.

    Returns
    -------
    ddg.geometry.subspaces.Subspace.Point
    """
    p0, p1 = incidence_chord
    chord_subspace = join_subspaces(p0, p1)

    # Get tangent line to conic at p1.
    tangent_subspace = polarize(p1, conic)

    assert p1 in conic
    assert p1 in tangent_subspace

    # Reflect the chord in the tangent line.
    reflected_chord = reflect_in_hyperplane(chord_subspace, tangent_subspace)
    # Get other intersection point with conic.
    intersection_points = intersect_quadric_subspace(conic, reflected_chord)
    p, q = quadric_to_subspaces(intersection_points)
    if p == p1:
        reflected_point = q
    else:
        reflected_point = p

    return reflected_point


# [billiard-map]

# [run-step]
def run_step(i):
    # The animation is designed to iteratively run through the steps.
    if len(queue) < i:
        warnings.warn(
            f"Attempted billiards iteration {i} "
            "before calling the previous iterations. "
            "Please run them in order."
        )
        return None

    else:
        p0 = queue[i - 2]
        p1 = queue[i - 1]

        # 1st step: Clear animated objects and visualize 0'th point.
        if i == 1:
            clear(collections=[animated_col])
            ddg.to_blender_object_helper(
                p1.embed(),
                sphere_radius=points_size,
                material=point_material,
                collection=animated_col,
            )

        # When running the animation the fist time new points have to be generated.
        if len(queue) == i:
            reflected_point = billard_map(ellipse, (p0, p1))
            queue.append(reflected_point)

        # When running the animation multiple times points in the queue can be accessed.
        elif len(queue) > i:
            reflected_point = queue[i]

    # New chord generated by reflection.
    chord = join_subspaces(p1, reflected_point)

    # Embed and visualize the new point and the new chord line.
    ddg.to_blender_object_helper(
        reflected_point.embed(),
        sphere_radius=points_size,
        material=point_material,
        collection=animated_col,
    )
    ddg.to_blender_object_helper(
        chord.embed(),
        domain=[[0, 1]],
        sampling=[0.006, 300, "c"],
        curve_properties={"bevel_depth": chord_thickness},
        material=chord_material,
        collection=animated_col,
    )


# [run-step]


if __name__ == "__main__":
    # Delete all objects and their corresponding data.
    clear()
    static_col = collection("static objects")
    animated_col = collection("animated objects")

    # [setup]
    # Choose major and minor axes of the ellipse with a > b.
    a = 3.5
    b = 1.0
    # Create the ellipse and its parameterization.
    ellipse = Quadric(np.diag([1 / a, 1 / b, -1]))
    ellipse_snet = ddg.to_smooth_net(ellipse.embed())
    ellipse_parameterization = ellipse_snet.fct
    ellipse_dnet = ddg.sample_smooth_net(ellipse_snet, [0.006, 300, "c"])

    # A chord is determined by two points on the ellipse.
    # We choose the initial chord using parameterization function of ellipse.
    # Set t0 and t1 accordingly.
    epsilon = 0.1
    t0 = np.pi / 2 - epsilon
    t1 = 3 * np.pi / 2 + epsilon

    s0 = ellipse_parameterization(t0)[:-1]
    s1 = ellipse_parameterization(t1)[:-1]

    # The queue stores the points of the billiard map as ddg.Point's.
    queue = [Subspace(homogenize(s0)), Subspace(homogenize(s1))]
    # [setup]

    # The number of times to run billiard map.
    # [set-number-of-steps]
    N = 50
    # [set-number-of-steps]

    # [visualize-static-blender-objects]
    # -------------
    # Visualization
    # -------------

    # Set size for visualized points.
    points_size = 0.018
    # Set thickness of visualized curves.
    chord_thickness = 0.004
    ellipse_thickness = 0.015

    # Setup camera.
    camera = camera(name="top_camera", location=(0.0, 0.0, 7.0), collection=static_col)

    # Setup light.
    light = light(name="top_light", type_="SUN", energy=2.0, collection=static_col)

    # Setup basic materials.
    point_material = material("point_material", color=(1.0, 0.0, 0.0))
    chord_material = material("line_material", color=(0.0, 0.130, 0.717))
    ellipse_material = material("ellipse_material", color=(0.015, 0.015, 0.015))

    # Visualize ellipse.
    ddg.to_blender_object(
        ellipse_dnet,
        material=ellipse_material,
        attributes={"name": "ellipse"},
        curve_properties={"bevel_depth": ellipse_thickness},
        collection=static_col,
    )
    # Define and visualize its focal points.
    f1 = Subspace([np.sqrt(a - b), 0.0, 0.0, 1.0])
    f2 = Subspace([-np.sqrt(a - b), 0.0, 0.0, 1.0])
    ddg.to_blender_object_helper(
        f1,
        name="first_focal_point",
        sphere_radius=points_size,
        material=ellipse_material,
        collection=static_col,
    )
    ddg.to_blender_object_helper(
        f2,
        name="second_focal_point",
        sphere_radius=points_size,
        material=ellipse_material,
        collection=static_col,
    )
    # [visualize-static-blender-objects]

    # Animation settings
    # [define-callback]
    ddg.visualization.blender.props.add_props_with_callback(run_step, ("i"), 0)
    # [define-callback]

    # Keyframe and frame settings
    scene = bpy.context.scene
    scene.frame_end = N
    # [keyframes]
    set_keyframe(scene, 1, "i", 1)
    set_keyframe(scene, N, "i", N)
    # [keyframes]

    # [render-settings]
    # Setup render settings
    output_dir = "/"
    scene.render.fps = 4
    scene.view_settings.view_transform = "Standard"
    setup_eevee_renderer(scene=scene)
    set_film_transparency(scene=scene)
    set_render_output_images(output_dir, time=False)
    # [render-settings]

    # [render-stamp]
    # Display render stamp of parameters.
    parameters_string = " N = " + str(N)
    parameters_string += "\n a = " + np.format_float_positional(a, precision=3)
    parameters_string += "\n b = " + np.format_float_positional(b, precision=3)
    parameters_string += "\n t₀ = " + np.format_float_positional(t0, precision=3)
    parameters_string += "\n t₁ = " + np.format_float_positional(t1, precision=3)
    set_render_stamp_note(note=parameters_string, scene=scene)

    # Do not display any other render stamp.
    scene.render.use_stamp_date = False
    scene.render.use_stamp_time = False
    scene.render.use_stamp_frame = False
    scene.render.use_stamp_scene = False
    scene.render.use_stamp_labels = False
    scene.render.use_stamp_camera = False
    scene.render.use_stamp_filename = False
    scene.render.use_stamp_render_time = False
    # [render-stamp]
