import numpy as np
import datetime as datetime

import matplotlib.pyplot as plt
from matplotlib import cm, colors

import plotly.graph_objects as go


# ============================================================
# Parameters
# ============================================================

N = 5                            # quintic
α = np.pi / 4                    # projection angle
U_MIN, U_MAX = -1.25, 1.25
V_MIN, V_MAX = 0.0, np.pi / 2
NU, NV = 100, 100                # mesh resolution



# ============================================================
# Core geometry
# ============================================================

def make_parameter_grid(umin=U_MIN, umax=U_MAX, vmin=V_MIN, vmax=V_MAX, nu=NU, nv=NV):
    """
    Construct a rectangular parameter grid in the complex z-plane.

    The parameters u and v define z = u + i v.
    The resulting arrays U, V have shape (nv, nu), with u varying horizontally and v vertically.
    """
    u = np.linspace(umin, umax, nu)
    v = np.linspace(vmin, vmax, nv)

    # U[j, i] is the u-coordinate and V[j, i] is the v-coordinate.
    U, V = np.meshgrid(u, v, indexing='xy')
    
    return U, V


def calabi_yau_sheet(U, V, k1, k2, n=N, alpha=α):
    """
    Compute one sheet of the projected Riemann surface slice inside the given Calabi-Yau manifold.
    The output is a real 3d projection.

    1. Our Calabi-Yau manifold is 
        
        X   :    Z_0^N + Z_1^N + Z_2^N + Z_3^N + Z_4^N = 0   inside   ℙ^4    .
    
    Here, [Z_0 : Z_1 : Z_2 : Z_3 : Z_4] are the homogeneous coordinates on ℙ^4.
    
    2. Consider the open subset X_0 ⊂ X given by Z_0 ≠ 0, hence:
        
        X_0 :    1 + (Z_1/Z_0)^N + (Z_2/Z_0)^N + (Z_3/Z_0)^N + (Z_4/Z_0)^N = 0    .

    3. Study the complex surface X_1 ⊂ X_0 determined by (Z_3/Z_0) = (Z_4/Z_0) = (-1)^{1/N}.
    Let z_1 := Z_1/Z_0 and z_2 := Z_2/Z_0, so that X_1 ⊂ X_0 ⊂ X is given in ℂ^2 by
        
        X_1 :    z_1^N + z_2^N = 1    .

    4. Consider an array of Riemann surfaces inside X_1:
    
        X_2 (k_1,k_2) ⊂ X_1 ⊂ X_0 ⊂ X         ∀  0 ≤ k_1, k_2 ≤ N-1    .
    
    Each X_2 (k_1,k_2) is parameterised by z = U + iV and given by

        X_2 (k_1, k_2) :    z_1 = e^{2πi k_1 / n} cosh(z)^{2/n}
                            z_2 = e^{2πi k_2 / n} sinh(z)^{2/n}
    
    Thus, the integers k_1 and k_2 select choices of nth-root branches
    for the two complex coordinates z_1 and z_2.

    5. Finally, we project from ℂ^2 = ℝ^4 to ℝ^3 as follows
        X = Re(z_1),
        Y = Re(z_2),
        Z = cos(α) Im(z_1) + sin(α) Im(z_2).
    """
    z = U + 1j * V

    z1 = np.exp(2j * np.pi * k1 / n) * np.power(np.cosh(z), 2.0 / n)
    z2 = np.exp(2j * np.pi * k2 / n) * np.power(np.sinh(z), 2.0 / n)

    X = np.real(z1)
    Y = np.real(z2)
    Z = np.cos(alpha) * np.imag(z1) + np.sin(alpha) * np.imag(z2)

    return X, Y, Z


def generate_all_sheets(n=N, alpha=α, umin=U_MIN, umax=U_MAX, vmin=V_MIN, vmax=V_MAX, nu=NU, nv=NV):
    """
    Generate all n^2 sheets of the projected surface.

    Each sheet corresponds to a pair of branch choices (k_1, k_2).
    Returns a list of dictionaries, one dictionary per sheet.
    """
    
    U, V = make_parameter_grid(umin, umax, vmin, vmax, nu, nv)

    sheets = []

    # Loop over all branch choices for z_1 and z_2.
    for k1 in range(n):
        for k2 in range(n):
            X, Y, Z = calabi_yau_sheet(U, V, k1, k2, n=n, alpha=alpha)
            sheets.append({
                'k1': k1,
                'k2': k2,
                'X': X,
                'Y': Y,
                'Z': Z
            })
            
    return sheets


def structured_triangles(nu=NU, nv=NV):
    """
    Construct a triangle mesh for a structured nu by nv parameter grid.

    Each rectangular grid cell is split into two triangles. The vertex
    ordering is compatible with row-major flattening of arrays of shape
    (nv, nu); e.g. X.ravel(), Y.ravel(), Z.ravel().
    """
    faces = []

    # j indexes rows in the v-direction; i indexes columns in the u-direction.
    for j in range(nv - 1):
        for i in range(nu - 1):
            a = j * nu + i
            b = a + 1
            c = a + nu
            d = c + 1

            # Split the quadrilateral (a, b, d, c) into two triangles.
            faces.append([a, b, d])
            faces.append([a, d, c])
            
    return np.array(faces, dtype=np.int32)


def all_vertices_from_sheets(sheets):
    """
    Collect all vertices from all sheets into a single array.

    Returns an array of shape (total_vertices, 3), where each row is an (X, Y, Z) point.
    """
    pts = []
    
    for s in sheets:
        
        # Stack flattened X, Y, Z arrays columnwise into 3D vertex coordinates.
        pts.append(np.c_[s['X'].ravel(), s['Y'].ravel(), s['Z'].ravel()])
        
    return np.vstack(pts)


def bounding_box_from_sheets(sheets):
    """
    Compute the global axis-aligned bounding box of all sheets.

    Returns:
        mins: array [xmin, ymin, zmin]
        maxs: array [xmax, ymax, zmax]
    """
    pts = all_vertices_from_sheets(sheets)
    
    mins = pts.min(axis=0)
    maxs = pts.max(axis=0)
    
    return mins, maxs
	
	
	
# ============================================================
# Plotting setup
# ============================================================

def set_axes_equal_3d(ax, mins, maxs):
    """
    Force a 3d matplotlib axis to use equal scale in all directions.
    This prevents the surface from appearing artificially stretched.
    """
    
    # Centre of the global bounding box.
    center = 0.5 * (mins + maxs)
    
    # Use the largest side length to define a cubic plotting box.
    radius = 0.5 * np.max(maxs - mins)

    # Set identical plotting ranges in x, y, and z.
    ax.set_xlim(center[0] - radius, center[0] + radius)
    ax.set_ylim(center[1] - radius, center[1] + radius)
    ax.set_zlim(center[2] - radius, center[2] + radius)

    # Ask matplotlib to render the 3d box with equal visual proportions.
    ax.set_box_aspect([1, 1, 1])


def plot_matplotlib(sheets, elev=24, azim=35, figsize=(10, 10), dpi=300):
    """
    Plot all sheets using matplotlib's 3d surface plotting.

    Returns a static rendered view of the projected surface.
    """
    fig = plt.figure(figsize=figsize, dpi=dpi)
    ax = fig.add_subplot(111, projection='3d')

    # Use a continuous colormap to assign a different colour to each sheet.
    cmap = cm.jet
    n_sheets = len(sheets)

    for idx, s in enumerate(sheets):
        
        # Pick a colour according to the sheet index.
        colour = cmap(idx / max(1, n_sheets - 1))

        # Plot one parametrised sheet as a semi-transparent surface.
        ax.plot_surface(
            s['X'], s['Y'], s['Z'],          # Surface coordinates as 2d arrays
            rstride=1,                       # Use every row of the grid
            cstride=1,                       # Use every column of the grid
            linewidth=0.001,                 # Very thin mesh-edge lines
            edgecolor=(0, 0, 0, 0.15),       # Semi-transparent black mesh edges
            color=colour,                    # Solid face colour for this sheet
            alpha=0.58,                      # Surface transparency
            antialiased=True,                # Smooth jagged edges visually
            shade=True                       # Apply lighting/shading for 3d depth
        )

    # Compute a global bounding box and use it to enforce equal 3d scaling.
    mins, maxs = bounding_box_from_sheets(sheets)
    set_axes_equal_3d(ax, mins, maxs)

    # Set the viewing angle.
    ax.view_init(elev=elev, azim=azim)

    # Remove axes, ticks, and frame for a cleaner visual.
    ax.set_axis_off()

    # Save plot as pdf image locally.
    plt.savefig("CY.pdf", format='pdf')

    # Display the plot with tight layout.
    plt.tight_layout()
    plt.show()



def plot_plotly(sheets, nu=NU, nv=NV):
    """
    Plot all sheets as an interactive plotly mesh3d figure.

    Each sheet is converted from structured grid data into a triangle mesh.
    Returns an HTML file with an interactive plot.
    """

    # Triangulation shared by every sheet, since each sheet uses the same grid.
    faces = structured_triangles(nu, nv)

    fig = go.Figure()

    # Use the same colormap idea as in the matplotlib version.
    cmap = cm.jet
    n_sheets = len(sheets)

    for idx, s in enumerate(sheets):

        # Flatten the sheet arrays into a list of 3d vertices.
        verts = np.c_[s['X'].ravel(), s['Y'].ravel(), s['Z'].ravel()]

        # Convert the matplotlib RGBA colour to a plotly-compatible hex string.
        colour = colors.to_hex(cmap(idx / max(1, n_sheets - 1)))

        # Add one triangular mesh for this sheet.
        fig.add_trace(go.Mesh3d(
            x=verts[:, 0],
            y=verts[:, 1],
            z=verts[:, 2],

            # Triangle vertex indices.
            i=faces[:, 0],
            j=faces[:, 1],
            k=faces[:, 2],
            
            color=colour,
            opacity=0.58,
            flatshading=False,

            # Lighting parameters controlling the rendered 3d appearance.
            lighting=dict(
                ambient=0.35,        # Base light level; higher values make shadows less dark
                diffuse=0.75,        # Matte surface lighting; higher values emphasise 3D shape
                specular=0.30,       # Shiny highlight strength; higher values make surfaces glossier
                roughness=0.45,      # Surface roughness; higher values spread/soften highlights
                fresnel=0.10         # Edge glow effect; higher values brighten grazing-angle edges
            ),

            # Position of the virtual light source.
            lightposition=dict(x=60, y=40, z=80),

            # Disable hover labels to keep the interaction visually clean.
            hoverinfo='skip',

            # No scalar colourbar is needed because sheets are coloured manually.
            showscale=False,

            # Name the sheet by its branch indices.
            name=f"({s['k1']},{s['k2']})"
        ))

    fig.update_layout(
        width=950,
        height=900,
        showlegend=False,
        
        scene=dict(
            
            # Hide coordinate axes for a clean geometric rendering.
            xaxis=dict(visible=False),
            yaxis=dict(visible=False),
            zaxis=dict(visible=False),

            # Preserve the actual geometric aspect ratio of the data.
            aspectmode='data',

            # Initial camera position.
            camera=dict(
                eye=dict(x=1.6, y=1.4, z=1.0)
            ),
            
            bgcolor='white'
        ),

        # Remove exterior whitespace around the plot.
        margin=dict(l=0, r=0, b=0, t=0)
    )

    # Save an interactive standalone HTML version.
    fig.write_html("CY.html")

    # Display the interactive figure in the current notebook/session.
    fig.show()
	
	

# ============================================================
# Execute matplot
# ============================================================

sheets = generate_all_sheets()
plot_matplotlib(sheets)


# ============================================================
# Execute plotly
# ============================================================

plot_plotly(sheets)