# [setup]
import os

import bpy
import numpy as np

import ddg

ddg.blender.scene.clear(deep=True)
ddg.blender.material.clear()
euc = ddg.geometry.euclidean(2)


def s_exp(U, V):
    """
    Function to create an s-exp orthogonal circle pattern in the plane.
    Uses square grid combinatorics with UxV vertices with the
    topology of a cylinder.

    The resulting combinatorics will have black and white
    vertices (v.color = 'b', v.color = 'w') where white
    vertices correspond to circle centers and
    black vertices correspond to points of intersection.

    Returns
    -------
    ddg.halfedge.Surface
        Halfedge object containing the 2d coordinates in the vertex attribute 'co'.
    """

    # Start with ifs grid of cylinder topology and convert it to halfedge
    grid_ = ddg.indexedfaceset.grid_with_periodicity((U, V), periodicity=(0, 1))
    combinatorics = ddg.indexedfaceset.indexed_face_set_to_surface(grid_)
    combinatorics.verts.add_attribute("co")
    for v in combinatorics.verts:
        v.co = grid_.vertex_attributes["uv"][v.ifs_index]
    delattr(combinatorics.verts, "ifs_index")
    delattr(combinatorics.faces, "ifs_face_index")

    # Bicolor vertices
    ddg.halfedge.bicolor_vertices(combinatorics, colors=["w", "b"])
    white_verts = [v for v in combinatorics.verts if v.color == "w"]
    black_verts = [v for v in combinatorics.verts if v.color == "b"]

    # Determine circle centers (see e.g. BHS06: Minimal surfaces from combinatorics)
    combinatorics.verts.add_attribute("radius")
    rho = 2 * np.pi / V
    alph = np.arctanh(0.5 * np.abs(1 - np.exp(2j * rho)))
    max_norm = 0.0

    for v in white_verts:
        i, j = v.co
        c = np.exp(alph * i) * np.exp(1j * rho * j)
        v.co = np.array([c.real, c.imag])
        v.radius = np.sin(rho) * np.abs(c)
        max_norm = max(max_norm, np.linalg.norm(v.co))

    # Scale to unit disc
    if max_norm > 1:
        for v in white_verts:
            v.co = v.co / max_norm
            v.radius = v.radius / max_norm

    # Determine touching points for interior vertices and for boundary vertices
    for v in black_verts:
        out_edges = [e for e in ddg.halfedge.out_edges(v)]
        if len(out_edges) == 4:
            v1 = out_edges[2].head
            v2 = out_edges[0].head

        elif len(out_edges) == 3:
            out_sorted = sorted(
                out_edges, key=lambda e: not ddg.halfedge.is_boundary_vertex(e.head)
            )
            v1, v2 = out_sorted[0].head, out_sorted[1].head

        vec = v2.co - v1.co
        v.co = v1.co + v1.radius * vec / np.linalg.norm(vec)

    return combinatorics


# [setup]

# [construct]
# Parameters that determine the combinatorics
# V must be even to bi-color the vertices.
U, V = 15, 30
assert V % 2 == 0
s_exp_cp = s_exp(U, V)
# [construct]

# [vis]
# Visualization
mats = [
    ddg.blender.material.material(color=(0.0, 0.0, 0.0)),
    ddg.blender.material.material(color=(1.0, 1.0, 1.0)),
]
ddg.blender.edges(s_exp_cp, "combinatorics", radius=0.005, material=mats[0])

for v in s_exp_cp.verts:
    mat = mats[0] if v.color == "b" else mats[1]
    ddg.blender.convert(
        ddg.geometry.Point([*v.co, 1]),
        f"pt_{v.index}",
        point_radius=np.linalg.norm(v.co) ** 0.5 / 100,
        material=mat,
    )
    if v.color == "w":
        sphere = euc.sphere([*v.co, 1], v.radius)
        sphere_array = ddg.arrays.from_quadric_sphere(sphere, 10)
        ddg.blender.convert(
            sphere_array,
            f"sphere_{v.index}",
            material=mats[0],
            curve_radius=np.linalg.norm(v.co) ** 0.5 / 250,
        )
# [vis]

# [obj]
# Export obj
for v in s_exp_cp.verts:
    v.co = np.array([*v.co, 0.0])
dir = os.path.dirname(os.path.realpath(__file__))
ddg.conversion.obj.halfedge.hds_to_obj(s_exp_cp, filename=f"{dir}/s_exp.obj")
# [obj]

# Add light and camera
cam = ddg.blender.camera.camera(location=(0, 0, 3), type_="ORTHO")
bpy.context.scene.camera = cam
ddg.blender.camera.look_at_point(cam, (0, 0, 0))
light = ddg.blender.light.light(location=(0, 0, 0), energy=1)

# Setup rendering
ddg.blender.render.setup_eevee_renderer()
ddg.blender.render.set_world_background((1, 1, 1, 1), 1)
bpy.context.scene.render.resolution_x = 1000
bpy.context.scene.render.resolution_y = 1000
bpy.context.scene.view_settings.view_transform = "Standard"
