"""
Example: Envelope, Evolute, Orthogonal Trajectory and Involute of Curves
"""

from functools import lru_cache

import bpy

#############################################
# Setup
#############################################
# [setup-1]
# Import necessary libraries
import numpy as np

import ddg
from ddg.blender.material import material
from ddg.geometry.euclidean_models import ProjectiveModel as euc
from ddg.math.euclidean import reflection_in_a_hyperplane
from ddg.math.projective import dehomogenize, homogenize

# [setup-1]

# [setup-2]
# Clear the existing objects in the Blender scene
ddg.blender.scene.clear()


ddg.blender.material.clear()
# [setup-2]


#############################################
#  general-functions
#############################################
# Warning regarding the caching in this file:
#

# [general-functions-1]
def tangent_edges(fct):
    """Returns a function that, for a given index,
    returns the edge tangent line with given index of the
    discrete curve in the input.
    The i'th edge tangent line is the join of fct(i) and fct(i+1).
    """

    @lru_cache(maxsize=128)
    def tangent_edge(i):
        tangent = ddg.geometry.subspace_from_affine_points(fct(i), fct(i + 1))
        return ddg.geometry.orthonormalize_and_center_subspace(
            tangent, np.sum([fct(i), fct(i + 1)], axis=0) / 2
        )

    return tangent_edge


def normal_edges(fct):
    """Returns a function that, for a given index,
    returns the edge normal line with given index of the
    discrete curve in the input.
    The i'th edge normal line is the perpendicular
    bisector of fct(i) and fct(i+1).
    """

    @lru_cache(maxsize=128)
    def normal_edge(i):
        normal = euc.perpendicular_bisector(
            ddg.geometry.subspace_from_affine_points(fct(i)),
            ddg.geometry.subspace_from_affine_points(fct(i + 1)),
        )
        return ddg.geometry.orthonormalize_and_center_subspace(
            normal, np.sum([fct(i), fct(i + 1)], axis=0) / 2
        )

    return normal_edge


def normal_vertices(fct):
    """Returns a function that, for a given index,
    returns the vertex normal line with given index of the
    discrete curve in the input.
    The i'th vertex normal line is the orientation reversing
    angle bisector of the (i-1)'st and i'th edge tangent line.
    """

    @lru_cache(maxsize=128)
    def normal_vertex(i):
        normal = euc.angle_bisector_orientation_reversing(
            tangent_edges(fct)(i - 1), tangent_edges(fct)(i)
        )
        return ddg.geometry.orthonormalize_and_center_subspace(normal, fct(i))

    return normal_vertex


# [general-functions-1]

# [general-functions-2]
def envelope(g):
    """The return value of the function g is assumed to be a line.
    Then this function returns a function that, for a given index i,
    returns the intersection of the (i-1)'st and i'th
    line of g.
    """

    @lru_cache(maxsize=128)
    def new_curve_fct(i):
        point = ddg.geometry.intersect(g(i - 1), g(i))
        return point.affine_point

    return new_curve_fct


@lru_cache(maxsize=128)
def orthogonal_trajectory(g, starting_point):
    """The return value of the function g is assumed to be a line.
    Then this function returns a function that, for a given index i,
    returns the i'th iterative reflection of the starting point
    on the first i lines of g.
    Example: new_curve_fct(0) returns the starting_point,
    new_curve_fct(1) returns the starting point reflected on the
    line g(0), new_curve_fct(2) returns the point new_curve_fct(1)
    reflected on the line g(1).
    """

    @lru_cache(maxsize=128)
    def new_curve_fct(i):
        if i == 0:
            return starting_point
        else:
            n, l = ddg.geometry.normal(g(i - 1)), ddg.geometry.level(g(i - 1))
            return dehomogenize(
                reflection_in_a_hyperplane(n, l) @ homogenize(new_curve_fct(i - 1))
            )

    return new_curve_fct


# [general-functions-2]

#############################################
# Example
#############################################
# [example-1]
def example(type_):
    if type_ == "smooth":
        sampling_curve = np.pi / 50
        bevel_curve = 0.05
        bevel_line = 0.02
        involute_start = (-10, 3)
        edge_domain = [-5, 5]
    elif type_ == "discrete":
        sampling_curve = np.pi / 2
        bevel_curve = 0.1
        bevel_line = 0.06
        involute_start = (-9, -5)
        edge_domain = [-10, 10]
    else:
        raise ValueError(f"{type_} has to be one of 'smooth' or 'discrete'.")

    # [example-1]

    # [example-2]
    # We define a parametrization for a curve.
    a, b = 1, 2

    def parametrization(u):
        return [a * u - b * np.sin(u), a * 1 - b * np.cos(u)]

    # We set a definition domain and initialize the smooth curve.
    smooth_domain = [[-3 * np.pi, 3 * np.pi]]
    snet = ddg.nets.SmoothNet(parametrization, domain=smooth_domain)
    # [example-2]

    # [example-3]
    # Define a sampling and initialize the discrete curve.
    # The domain of the discrete curve is [[0, d]],
    # where d is the number of steps in the smooth domain
    # determined by the sampling.
    dnet = ddg.nets.sample_smooth_net(snet, sampling=sampling_curve)
    # [example-3]

    # [example-4]
    # Define edge tangent line, vertex normal line and edge normal line
    # functions for the given curve.
    tangent_edge = tangent_edges(dnet.fct)
    normal_edge = normal_edges(dnet.fct)
    normal_vertex = normal_vertices(dnet.fct)

    # Define evolutes for edge normal lines and vertex normal lines and
    # an involute for the edge tangent lines.
    evolute_vertex = envelope(normal_edge)
    evolute_edge = envelope(normal_vertex)
    involute = orthogonal_trajectory(tangent_edge, involute_start)
    # [example-4]

    # [example-5]
    # Define normals of the involute.
    normal_edge_involute = normal_edges(involute)

    # Define the evolute of the normals of the involute.
    evolute_involute_edge = envelope(normal_edge_involute)
    # [example-5]

    #############################################
    # Visualization
    #############################################

    # [visualization-1]
    orange = material("orange", (0.8, 0.1, 0.036), 0, 0)
    blue = material("blue", (0.019, 0.052, 0.445), 0, 0)
    turquoise = material("turquoise ", (0.02, 0.8, 0.77), 0, 0)

    def shift_domain(a, b):
        return [[dnet.domain[0][0] + a, dnet.domain[0][1] + b]]

    # [visualization-1]

    # [visualization-2]
    def visualize_2d_curve(dnet_fct, name, domain=dnet.domain):
        dnet = ddg.nets.DiscreteNet(dnet_fct, domain=domain)
        bobj = ddg.blender.convert(dnet, type_ + " " + name, orange)
        bobj.data.bevel_depth = bevel_curve
        return bobj

    def visualize_2d_line(line, name, domain=[0, 1], material=None, collection=None):
        point = ddg.math.projective.dehomogenize(line.points[0])
        direction = line.points[1][:-1]
        curve = ddg.arrays.line_segment_from_point_and_direction(
            point, direction, domain, True
        )
        bobj = ddg.blender.convert(curve, name, material, collection)
        return bobj

    def visualize_2d_lines(g, name, domain=dnet.domain, line_domain=[-5, 5]):
        l = []
        for i in range(*domain[0]):
            bobj = visualize_2d_line(
                g(i),
                name=type_ + " " + name + f"_{i}",
                domain=line_domain,
                material=blue,
            )
            bobj.data.bevel_depth = bevel_line
            l.append(bobj)

        return l

    # [visualization-2]

    # [visualization-3]
    bobj = visualize_2d_curve(dnet.fct, name="Discrete Curve")

    visualize_2d_lines(normal_edge, "edge_normal")
    evolute_edge_bobj = visualize_2d_curve(
        evolute_edge, name="Evolute_edge", domain=shift_domain(1, -1)
    )

    visualize_2d_lines(normal_vertex, "vertex_normal", shift_domain(1, 0))
    evolute_vertex_bobj = visualize_2d_curve(evolute_vertex, name="Evolute_vertex")

    visualize_2d_lines(tangent_edge, "edge_tangent", line_domain=edge_domain)
    involute_bobj = visualize_2d_curve(involute, name="Involute")

    visualize_2d_lines(
        normal_edge_involute, "involute_edge_normal", line_domain=[-10, 10]
    )
    evolute_involute_edge_bobj = visualize_2d_curve(
        evolute_involute_edge, name="Evolute of Involute", domain=shift_domain(1, -1)
    )

    # [visualization-3]

    # [visualization-4]
    # Add a point light and a camera to the scene
    light = ddg.blender.light.light(location=(0, 0, 100), type_="SUN", energy=5)
    camera = ddg.blender.camera.camera(location=(0, 0, 40))
    bpy.context.scene.camera = camera
    ddg.blender.render.setup_cycles_renderer()
    ddg.blender.render.set_film_transparency()
    # [visualization-4]


if __name__ == "__main__":
    example("smooth")
