# Copyright (C) 2026 Antoine ANCEAU
#
# This file is part of navaltoolbox.
#
# navaltoolbox is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

"""
Visualization module for navaltoolbox using Plotly.
"""

from typing import Optional, List, Tuple, Any
import numpy as np
import plotly.graph_objects as go  # type: ignore

from . import Vessel, Hull


def plot_vessel_3d(
    vessel: Vessel,
    show_hulls: bool = True,
    show_tanks: bool = True,
    show_silhouettes: bool = True,
    show_openings: bool = True,
    show_appendages: bool = True,
    show_deck_edges: bool = True,
    opacity_hull: float = 0.5,
    opacity_tank: float = 0.3,
    opacity_appendage: float = 0.5,
    title: str = "Vessel Visualization",
    enable_opacity_slider: bool = True,
    show_axes: bool = True,
) -> go.Figure:
    """
    Create an interactive 3D plot of the vessel.

    Args:
        vessel: The vessel to visualize.
        show_hulls: Whether to show hull meshes.
        show_tanks: Whether to show tank meshes.
        show_silhouettes: Whether to show silhouette profiles.
        show_openings: Whether to show downflooding openings.
        show_appendages: Whether to show appendages.
        show_deck_edges: Whether to show deck edges.
        opacity_hull: Opacity for hull meshes (0.0 to 1.0).
        opacity_tank: Opacity for tank meshes (0.0 to 1.0).
        opacity_appendage: Opacity for appendage meshes (0.0 to 1.0).
        title: Title of the plot.
        enable_opacity_slider: Whether to add a slider to control hull opacity.
        show_axes: Whether to show axes, grid, and background.

    Returns:
        A Plotly Figure object.
    """
    fig = go.Figure()

    hull_indices = []

    # 1. Hulls
    if show_hulls:
        for i, hull in enumerate(vessel.get_hulls()):
            _add_hull_trace(fig, hull, f"Hull {i+1}", opacity_hull)
            # Track the index of the added trace
            hull_indices.append(len(fig.data) - 1)

    # 2. Tanks
    if show_tanks:
        for tank in vessel.get_tanks():
            # Tank Container (Wireframe or transparent mesh)
            if hasattr(tank, "get_vertices") and hasattr(tank, "get_faces"):
                verts = tank.get_vertices()
                faces = tank.get_faces()

                if verts and faces:
                    x, y, z = zip(*verts)
                    i_idx, j_idx, k_idx = zip(*faces)

                    fig.add_trace(
                        go.Mesh3d(
                            x=x,
                            y=y,
                            z=z,
                            i=i_idx,
                            j=j_idx,
                            k=k_idx,
                            color="lightgray",
                            opacity=opacity_tank,
                            name=f"{tank.name} (Tank)",
                            showscale=False,
                            alphahull=0,  # Use exact mesh
                        )
                    )

            # Fluid Volume (Higher opacity)
            if hasattr(tank, "get_fluid_vertices") and hasattr(
                tank, "get_fluid_faces"
            ):
                f_verts = tank.get_fluid_vertices()
                f_faces = tank.get_fluid_faces()

                if f_verts and f_faces:
                    fx, fy, fz = zip(*f_verts)
                    fi, fj, fk = zip(*f_faces)

                    fig.add_trace(
                        go.Mesh3d(
                            x=fx,
                            y=fy,
                            z=fz,
                            i=fi,
                            j=fj,
                            k=fk,
                            color="blue",
                            opacity=min(opacity_tank * 2.0, 0.9),
                            name=f"{tank.name} (Fluid)",
                            showscale=False,
                        )
                    )

            # Keep COG marker for reference
            cog = tank.center_of_gravity
            fig.add_trace(
                go.Scatter3d(
                    x=[cog[0]],
                    y=[cog[1]],
                    z=[cog[2]],
                    mode="markers",
                    marker=dict(size=3, color="black"),
                    name=f"{tank.name} COG",
                    showlegend=False,
                )
            )

    # 3. Silhouettes (Lines)
    if show_silhouettes:
        for sil in vessel.get_silhouettes():
            points = sil.get_points()
            if not points:
                continue

            x, y, z = zip(*points)
            # Close the loop if needed
            if sil.is_closed() and (x[0] != x[-1] or z[0] != z[-1]):
                x = x + (x[0],)
                y = y + (y[0],)
                z = z + (z[0],)

            fig.add_trace(
                go.Scatter3d(
                    x=x,
                    y=y,
                    z=z,
                    mode="lines",
                    line=dict(color="green", width=2),
                    name=f"Silhouette: {sil.name}",
                )
            )

    # 4. Openings (Markers)
    if show_openings:
        for opening in vessel.get_openings():
            points = opening.get_points()
            if not points:
                continue

            x, y, z = zip(*points)

            # If contour, draw lines
            if len(points) > 1:
                # Close loop
                x = x + (x[0],)
                y = y + (y[0],)
                z = z + (z[0],)
                mode = "lines+markers"
            else:
                mode = "markers"

            fig.add_trace(
                go.Scatter3d(
                    x=x,
                    y=y,
                    z=z,
                    mode=mode,
                    marker=dict(size=4, color="red"),
                    line=dict(color="red", width=2),
                    name=f"Opening: {opening.name}",
                )
            )

    # 5. Appendages
    if show_appendages:
        for app in vessel.get_appendages():
            _add_appendage_trace(fig, app, opacity_appendage)

    # 6. Deck Edges
    if show_deck_edges:
        for edge in vessel.get_deck_edges():
            points = edge.get_points()
            if not points:
                continue

            x, y, z = zip(*points)
            side = edge.get_side()

            # Determine color based on side
            color = "orange"  # Default/Both
            if "Port" in side:
                color = "red"
            elif "Starboard" in side:
                color = "green"

            fig.add_trace(
                go.Scatter3d(
                    x=x,
                    y=y,
                    z=z,
                    mode="lines+markers",
                    line=dict(color=color, width=4),
                    marker=dict(size=3, color=color),
                    name=f"Deck Edge: {edge.name} ({side})",
                )
            )

    # 5. Reference Points (AP/FP)
    # Using Y=0, Z=0 for reference
    ap_x = vessel.ap
    fp_x = vessel.fp

    fig.add_trace(
        go.Scatter3d(
            x=[ap_x, fp_x],
            y=[0, 0],
            z=[0, 0],
            mode="markers+text",
            marker=dict(size=5, color="black", symbol="diamond"),
            text=["AP", "FP"],
            textposition="top center",
            name="Perpendiculars",
        )
    )

    # Set axis labels and equal aspect ratio
    bounds = vessel.get_bounds()
    max_range = max(
        bounds[1] - bounds[0], bounds[3] - bounds[2], bounds[5] - bounds[4]
    )
    center_x = (bounds[0] + bounds[1]) / 2
    center_y = (bounds[2] + bounds[3]) / 2
    center_z = (bounds[4] + bounds[5]) / 2

    # Hack to force aspect ratio in Plotly
    # We add invisible points to set the range
    fig.add_trace(
        go.Scatter3d(
            x=[center_x - max_range / 2, center_x + max_range / 2],
            y=[center_y - max_range / 2, center_y + max_range / 2],
            z=[center_z - max_range / 2, center_z + max_range / 2],
            mode="markers",
            marker=dict(size=0, opacity=0),
            showlegend=False,
        )
    )

    # Sliders
    sliders = []
    if enable_opacity_slider and hull_indices:
        steps = []
        for op in np.arange(0, 1.1, 0.1):
            step = dict(
                method="restyle",
                args=[{"opacity": op}, hull_indices],
                label=f"{op:.1f}",
            )
            steps.append(step)

        sliders.append(
            dict(
                active=int(opacity_hull * 10),
                currentvalue={"prefix": "Hull Opacity: "},
                pad={"t": 50},
                steps=steps,
            )
        )

    scene_args: dict[str, Any] = dict(aspectmode="cube")

    if show_axes:
        scene_args.update(
            dict(
                xaxis_title="X (Longitudinal)",
                yaxis_title="Y (Transverse)",
                zaxis_title="Z (Vertical)",
            )
        )
    else:
        scene_args.update(
            dict(
                xaxis=dict(
                    visible=False, showgrid=False, showbackground=False
                ),
                yaxis=dict(
                    visible=False, showgrid=False, showbackground=False
                ),
                zaxis=dict(
                    visible=False, showgrid=False, showbackground=False
                ),
            )
        )

    fig.update_layout(
        title=title,
        scene=scene_args,
        sliders=sliders,
        updatemenus=[
            # 1. Projection (Left)
            dict(
                type="buttons",
                direction="right",
                buttons=[
                    dict(
                        label="Persp 👁️",
                        method="relayout",
                        args=["scene.camera.projection.type", "perspective"],
                    ),
                    dict(
                        label="Ortho 📐",
                        method="relayout",
                        args=["scene.camera.projection.type", "orthographic"],
                    ),
                ],
                pad={"r": 5, "t": 5},
                showactive=True,
                x=0.0,
                xanchor="left",
                y=1.1,
                yanchor="top",
                bgcolor="rgba(255, 255, 255, 0.7)",
            ),
            # 2. Views (Right, with gap)
            dict(
                type="buttons",
                direction="right",
                buttons=_get_camera_buttons(),
                pad={"r": 5, "t": 5},
                showactive=True,
                x=0.25,  # Gap from projection buttons
                xanchor="left",
                y=1.1,
                yanchor="top",
                bgcolor="rgba(255, 255, 255, 0.7)",
            ),
        ],
    )

    return fig


def plot_hydrostatic_condition(
    vessel: Vessel,
    draft: float,
    trim: float = 0.0,
    heel: float = 0.0,
    show_hulls: bool = True,
    show_tanks: bool = True,
    show_silhouettes: bool = True,
    show_openings: bool = True,
    show_appendages: bool = True,
    show_deck_edges: bool = True,
    opacity_hull: float = 0.5,
    opacity_tank: float = 0.3,
    opacity_appendage: float = 0.5,
    title: str = "Hydrostatic Condition",
    enable_opacity_slider: bool = True,
    cog: Optional[Tuple[float, float, float]] = None,
    show_axes: bool = True,
) -> go.Figure:
    """
    Visualize the vessel at a specific hydrostatic condition (floating in
    water).
    The waterplane is fixed at Z=0.
    Vessel is transformed to match drafts.

    Args:
        vessel: The vessel to visualize.
        draft: Draft at midship (or reference point) in meters.
        trim: Trim angle in degrees (positive = stern down usually).
        heel: Heel angle in degrees (positive = starboard down usually).
        show_hulls: Whether to show hull meshes.
        show_tanks: Whether to show tank meshes.
        show_silhouettes: Whether to show silhouette profiles.
        show_openings: Whether to show downflooding openings.
        show_appendages: Whether to show appendages.
        show_deck_edges: Whether to show deck edges.
        opacity_hull: Opacity for hull meshes (0.0 to 1.0).
        opacity_tank: Opacity for tank meshes (0.0 to 1.0).
        opacity_appendage: Opacity for appendage meshes (0.0 to 1.0).
        title: Title of the plot.
        enable_opacity_slider: Whether to add a slider to control hull opacity.
        cog: Optional Center of Gravity (x, y, z) to display.
        show_axes: Whether to show axes, grid, and background.
    """
    fig = go.Figure()
    hull_indices = []

    # Calculate Transformation Matrix
    # We want to move Ship -> World such that water is at Z=0.
    # Rotation Order: Pitch (Trim) then Roll (Heel).
    # R = Ry(trim) * Rx(heel)

    h_rad = np.deg2rad(heel)
    t_rad = np.deg2rad(trim)

    ch, sh = np.cos(h_rad), np.sin(h_rad)
    ct, st = np.cos(t_rad), np.sin(t_rad)

    # Rx (Heel around X)
    Rx = np.array([[1, 0, 0], [0, ch, -sh], [0, sh, ch]])

    # Ry (Trim around Y)
    Ry = np.array([[ct, 0, st], [0, 1, 0], [-st, 0, ct]])

    # Combined Rotation R = Rx @ Ry
    R_total = Rx @ Ry

    # Transform function
    def transform_points(points):
        if len(points) == 0:
            return []
        pts = np.array(points)
        # Apply rotation (pts is Nx3, R_total is 3x3)
        # (R @ v)^T = v^T @ R^T
        rotated = pts @ R_total.T

        # Apply translation
        # Z translation: -draft.
        # Ideally, we should pivot around the flotation center,
        # but here we just translate vertically.
        translated = rotated + [0, 0, -draft]
        return translated

    # 0. Waterplane
    bounds = vessel.get_bounds()
    # Big plane
    L = bounds[1] - bounds[0]
    B = bounds[3] - bounds[2]
    plane_size = max(L, B) * 1.5

    # Grid of points for water
    wx = np.linspace(-plane_size / 2, plane_size / 2, 2) + (
        bounds[0] + bounds[1]
    ) / 2
    wy = np.linspace(-plane_size / 2, plane_size / 2, 2)
    WX, WY = np.meshgrid(wx, wy)
    WZ = np.zeros_like(WX)

    fig.add_trace(
        go.Surface(
            x=WX,
            y=WY,
            z=WZ,
            opacity=0.3,
            colorscale=[[0, "blue"], [1, "blue"]],
            showscale=False,
            name="Waterplane",
        )
    )

    # Add waterplane outline (so it's visible in orthographic side views)
    cx = (bounds[0] + bounds[1]) / 2
    cy = 0
    half_size = plane_size / 2

    # Square aligned with axes
    outline_x = [
        cx - half_size,
        cx + half_size,
        cx + half_size,
        cx - half_size,
        cx - half_size,
    ]
    outline_y = [
        cy - half_size,
        cy - half_size,
        cy + half_size,
        cy + half_size,
        cy - half_size,
    ]
    outline_z = [0, 0, 0, 0, 0]

    fig.add_trace(
        go.Scatter3d(
            x=outline_x,
            y=outline_y,
            z=outline_z,
            mode="lines",
            line=dict(color="blue", width=4),
            name="Waterline Level",
        )
    )

    # 1. Hulls
    if show_hulls:
        for i, hull in enumerate(vessel.get_hulls()):
            if hasattr(hull, "get_vertices") and hasattr(hull, "get_faces"):
                verts = hull.get_vertices()
                faces = hull.get_faces()
                if verts and faces:
                    t_verts = transform_points(verts)
                    x, y, z = t_verts[:, 0], t_verts[:, 1], t_verts[:, 2]
                    i_idx, j_idx, k_idx = zip(*faces)

                    fig.add_trace(
                        go.Mesh3d(
                            x=x,
                            y=y,
                            z=z,
                            i=i_idx,
                            j=j_idx,
                            k=k_idx,
                            color="gray",
                            opacity=opacity_hull,
                            name=f"Hull {i+1}",
                            showscale=False,
                            flatshading=True,
                        )
                    )
                    hull_indices.append(len(fig.data) - 1)

    # 2. Tanks
    if show_tanks:
        for tank in vessel.get_tanks():
            # Container
            if hasattr(tank, "get_vertices"):
                verts = tank.get_vertices()
                faces = tank.get_faces()
                if verts:
                    t_verts = transform_points(verts)
                    x, y, z = t_verts[:, 0], t_verts[:, 1], t_verts[:, 2]
                    i_idx, j_idx, k_idx = zip(*faces)
                    fig.add_trace(
                        go.Mesh3d(
                            x=x,
                            y=y,
                            z=z,
                            i=i_idx,
                            j=j_idx,
                            k=k_idx,
                            color="lightgray",
                            opacity=opacity_tank,
                            name=f"{tank.name} (Tank)",
                            showscale=False,
                        )
                    )

            # Fluid (Use heel/trim-aware getter)
            if hasattr(tank, "get_fluid_vertices"):
                # Pass vessel heel/trim to get the fluid mesh that is
                # 'horizontal' in world but 'tilted' in ship frame.
                verts = tank.get_fluid_vertices(heel=heel, trim=trim)
                faces = tank.get_fluid_faces(heel=heel, trim=trim)
                if verts:
                    # Apply SHIP transformation to place it in world
                    t_verts = transform_points(verts)
                    x, y, z = t_verts[:, 0], t_verts[:, 1], t_verts[:, 2]
                    i_idx, j_idx, k_idx = zip(*faces)
                    fig.add_trace(
                        go.Mesh3d(
                            x=x,
                            y=y,
                            z=z,
                            i=i_idx,
                            j=j_idx,
                            k=k_idx,
                            color="blue",
                            opacity=0.8,
                            name=f"{tank.name} (Fluid)",
                            showscale=False,
                        )
                    )

    # 3. Silhouettes (Lines)
    if show_silhouettes:
        for sil in vessel.get_silhouettes():
            points = sil.get_points()
            if not points:
                continue

            # Close the loop if needed (in ship frame)
            pts_nx3 = np.array(points)
            if sil.is_closed():
                # Check distance between first and last point
                if np.linalg.norm(pts_nx3[0] - pts_nx3[-1]) > 1e-6:
                    pts_nx3 = np.vstack([pts_nx3, pts_nx3[0]])

            # Transform to world
            t_points = transform_points(pts_nx3)
            x, y, z = t_points[:, 0], t_points[:, 1], t_points[:, 2]

            fig.add_trace(
                go.Scatter3d(
                    x=x,
                    y=y,
                    z=z,
                    mode="lines",
                    line=dict(color="green", width=2),
                    name=f"Silhouette: {sil.name}",
                )
            )

    # 4. Openings (Markers)
    if show_openings:
        for opening in vessel.get_openings():
            points = opening.get_points()
            if not points:
                continue

            pts_nx3 = np.array(points)

            # If contour, draw lines
            if len(points) > 1:
                # Close loop
                if np.linalg.norm(pts_nx3[0] - pts_nx3[-1]) > 1e-6:
                    pts_nx3 = np.vstack([pts_nx3, pts_nx3[0]])

                t_points = transform_points(pts_nx3)
                x, y, z = t_points[:, 0], t_points[:, 1], t_points[:, 2]

                mode = "lines+markers"
            else:
                t_points = transform_points(pts_nx3)
                x, y, z = t_points[:, 0], t_points[:, 1], t_points[:, 2]
                mode = "markers"

            fig.add_trace(
                go.Scatter3d(
                    x=x,
                    y=y,
                    z=z,
                    mode=mode,
                    marker=dict(size=4, color="red"),
                    line=dict(color="red", width=2),
                    name=f"Opening: {opening.name}",
                )
            )

    # 5. Appendages
    if show_appendages:
        for app in vessel.get_appendages():
            geo_type = app.geometry_type()

            if geo_type == "Mesh":
                mesh_data = app.get_mesh_data()
                if mesh_data:
                    verts, faces = mesh_data
                    if verts and faces:
                        t_verts = transform_points(verts)
                        x, y, z = t_verts[:, 0], t_verts[:, 1], t_verts[:, 2]
                        i_idx, j_idx, k_idx = zip(*faces)

                        fig.add_trace(
                            go.Mesh3d(
                                x=x, y=y, z=z, i=i_idx, j=j_idx, k=k_idx,
                                color="darkkhaki",
                                opacity=opacity_appendage,
                                name=f"{app.name} (Appendage)",
                                showscale=False,
                                flatshading=True,
                            )
                        )
            elif geo_type == "Box" or geo_type == "Cube":
                bounds = app.bounds
                if bounds:
                    xmin, xmax, ymin, ymax, zmin, zmax = bounds
                    # Create a box mesh
                    box_verts = [
                        (xmin, ymin, zmin), (xmax, ymin, zmin),
                        (xmax, ymax, zmin), (xmin, ymax, zmin),
                        (xmin, ymin, zmax), (xmax, ymin, zmax),
                        (xmax, ymax, zmax), (xmin, ymax, zmax)
                    ]
                    t_verts = transform_points(box_verts)
                    x, y, z = t_verts[:, 0], t_verts[:, 1], t_verts[:, 2]

                    i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
                    j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
                    k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]

                    fig.add_trace(
                        go.Mesh3d(
                            x=x, y=y, z=z, i=i, j=j, k=k,
                            color="goldenrod",
                            opacity=opacity_appendage,
                            name=f"{app.name} (Appendage)",
                            showscale=False,
                        )
                    )
            elif geo_type == "Sphere" or geo_type == "Point":
                center = app.center
                volume = app.volume
                t_center = transform_points([center])[0]

                fig.add_trace(
                    go.Scatter3d(
                        x=[t_center[0]],
                        y=[t_center[1]],
                        z=[t_center[2]],
                        mode="markers",
                        marker=dict(
                            size=10, color="goldenrod", symbol="circle"),
                        name=f"{app.name} (Appendage)",
                        text=f"Vol: {volume:.3f}m³",
                    )
                )

    # 6. Deck Edges
    if show_deck_edges:
        for edge in vessel.get_deck_edges():
            points = edge.get_points()
            if not points:
                continue

            t_points = transform_points(points)
            x, y, z = t_points[:, 0], t_points[:, 1], t_points[:, 2]
            side = edge.get_side()

            color = "orange"
            if "Port" in side:
                color = "red"
            elif "Starboard" in side:
                color = "green"

            fig.add_trace(
                go.Scatter3d(
                    x=x, y=y, z=z,
                    mode="lines+markers",
                    line=dict(color=color, width=4),
                    marker=dict(size=3, color=color),
                    name=f"Deck Edge: {edge.name} ({side})",
                )
            )

    # 7. Reference Points
    ap_pt = transform_points([(vessel.ap, 0, 0)])[0]
    fp_pt = transform_points([(vessel.fp, 0, 0)])[0]

    fig.add_trace(
        go.Scatter3d(
            x=[ap_pt[0], fp_pt[0]],
            y=[ap_pt[1], fp_pt[1]],
            z=[ap_pt[2], fp_pt[2]],
            mode="markers+text",
            marker=dict(size=5, color="black", symbol="diamond"),
            text=["AP", "FP"],
            name="Perpendiculars",
        )
    )

    # 8. Center of Gravity
    if cog:
        cog_pt = transform_points([cog])[0]
        fig.add_trace(
            go.Scatter3d(
                x=[cog_pt[0]],
                y=[cog_pt[1]],
                z=[cog_pt[2]],
                mode="markers+text",
                marker=dict(size=8, color="orange", symbol="diamond"),
                text=["COG"],
                textposition="top center",
                name="Center of Gravity",
            )
        )

    # Opacity Slider
    sliders = []
    if enable_opacity_slider and hull_indices:
        steps = []
        for op in np.arange(0, 1.1, 0.1):
            step = dict(
                method="restyle",
                args=[{"opacity": op}, hull_indices],
                label=f"{op:.1f}",
            )
            steps.append(step)

        sliders.append(
            dict(
                active=int(opacity_hull * 10),
                currentvalue={"prefix": "Hull Opacity: "},
                pad={"t": 50},
                steps=steps,
            )
        )

    scene_args: dict[str, Any] = dict(aspectmode="data")

    if show_axes:
        scene_args.update(
            dict(
                xaxis_title="X",
                yaxis_title="Y",
                zaxis_title="Z",
            )
        )
    else:
        scene_args.update(
            dict(
                xaxis=dict(
                    visible=False, showgrid=False, showbackground=False
                ),
                yaxis=dict(
                    visible=False, showgrid=False, showbackground=False
                ),
                zaxis=dict(
                    visible=False, showgrid=False, showbackground=False
                ),
            )
        )

    fig.update_layout(
        title=title,
        scene=scene_args,
        sliders=sliders,
        updatemenus=[
            # 1. Projection (Left)
            dict(
                type="buttons",
                direction="right",
                buttons=[
                    dict(
                        label="Persp 👁️",
                        method="relayout",
                        args=["scene.camera.projection.type", "perspective"],
                    ),
                    dict(
                        label="Ortho 📐",
                        method="relayout",
                        args=["scene.camera.projection.type", "orthographic"],
                    ),
                ],
                pad={"r": 5, "t": 5},
                showactive=True,
                x=0.0,
                xanchor="left",
                y=1.1,
                yanchor="top",
                bgcolor="rgba(255, 255, 255, 0.7)",
            ),
            # 2. Views (Right, with gap)
            dict(
                type="buttons",
                direction="right",
                buttons=_get_camera_buttons(),
                pad={"r": 5, "t": 5},
                showactive=True,
                x=0.25,  # Gap from projection buttons
                xanchor="left",
                y=1.1,
                yanchor="top",
                bgcolor="rgba(255, 255, 255, 0.7)",
            ),
        ],
    )
    return fig


def _get_camera_buttons() -> List[dict]:
    """Returns a list of camera view buttons for Plotly."""
    return [
        dict(
            label="ISO",
            method="relayout",
            args=[
                {
                    "scene.camera.eye": dict(x=1.25, y=1.25, z=1.25),
                    "scene.camera.up": dict(x=0, y=0, z=1),
                    "scene.camera.projection.type": "perspective",
                }
            ],
        ),
        dict(
            label="STBD",  # Starboard
            method="relayout",
            args=[
                {
                    "scene.camera.eye": dict(x=0, y=-2.5, z=0),
                    "scene.camera.up": dict(x=0, y=0, z=1),
                    "scene.camera.projection.type": "orthographic",
                }
            ],
        ),
        dict(
            label="PORT",  # Port
            method="relayout",
            args=[
                {
                    "scene.camera.eye": dict(x=0, y=2.5, z=0),
                    "scene.camera.up": dict(x=0, y=0, z=1),
                    "scene.camera.projection.type": "orthographic",
                }
            ],
        ),
        dict(
            label="TOP",
            method="relayout",
            args=[
                {
                    "scene.camera.eye": dict(x=0, y=0, z=2.5),
                    "scene.camera.up": dict(x=1, y=0, z=0),
                    "scene.camera.projection.type": "orthographic",
                }
            ],
        ),
        dict(
            label="BOW",
            method="relayout",
            args=[
                {
                    "scene.camera.eye": dict(x=2.5, y=0, z=0),
                    "scene.camera.up": dict(x=0, y=0, z=1),
                    "scene.camera.projection.type": "orthographic",
                }
            ],
        ),
        dict(
            label="AFT",  # Stern
            method="relayout",
            args=[
                {
                    "scene.camera.eye": dict(x=-2.5, y=0, z=0),
                    "scene.camera.up": dict(x=0, y=0, z=1),
                    "scene.camera.projection.type": "orthographic",
                }
            ],
        ),
    ]


def _add_hull_trace(fig: go.Figure, hull: Hull, name: str, opacity: float):
    """Helper to add hull mesh to figure."""
    if hasattr(hull, "get_vertices") and hasattr(hull, "get_faces"):
        verts = hull.get_vertices()
        faces = hull.get_faces()
        if verts and faces:
            x, y, z = zip(*verts)
            i_idx, j_idx, k_idx = zip(*faces)

            fig.add_trace(
                go.Mesh3d(
                    x=x,
                    y=y,
                    z=z,
                    i=i_idx,
                    j=j_idx,
                    k=k_idx,
                    color="gray",
                    opacity=opacity,
                    name=name,
                    showscale=False,
                    flatshading=True,
                )
            )


def _add_appendage_trace(fig: go.Figure, app: Any, opacity: float):
    """Helper to add appendage to figure."""
    geo_type = app.geometry_type()

    if geo_type == "Mesh":
        mesh_data = app.get_mesh_data()
        if mesh_data:
            verts, faces = mesh_data
            if verts and faces:
                x, y, z = zip(*verts)
                i_idx, j_idx, k_idx = zip(*faces)

                fig.add_trace(
                    go.Mesh3d(
                        x=x,
                        y=y,
                        z=z,
                        i=i_idx,
                        j=j_idx,
                        k=k_idx,
                        color="darkkhaki",
                        opacity=opacity,
                        name=f"{app.name} (Appendage)",
                        showscale=False,
                        flatshading=True,
                    )
                )
    elif geo_type == "Box" or geo_type == "Cube":
        bounds = app.bounds
        if bounds:
            xmin, xmax, ymin, ymax, zmin, zmax = bounds

            # Create a box mesh
            x = [xmin, xmin, xmax, xmax, xmin, xmin, xmax, xmax]
            y = [ymin, ymax, ymax, ymin, ymin, ymax, ymax, ymin]
            z = [zmin, zmin, zmin, zmin, zmax, zmax, zmax, zmax]

            i = [7, 0, 0, 0, 4, 4, 6, 6, 4, 0, 3, 2]
            j = [3, 4, 1, 2, 5, 6, 5, 2, 0, 1, 6, 3]
            k = [0, 7, 2, 3, 6, 7, 1, 1, 5, 5, 7, 6]

            fig.add_trace(
                go.Mesh3d(
                    x=x, y=y, z=z, i=i, j=j, k=k,
                    color="goldenrod",
                    opacity=opacity,
                    name=f"{app.name} (Appendage)",
                    showscale=False,
                )
            )
    elif geo_type == "Sphere" or geo_type == "Point":
        # Draw as a marker or simple sphere if we want
        center = app.center
        volume = app.volume

        # Simple marker for now
        fig.add_trace(
            go.Scatter3d(
                x=[center[0]],
                y=[center[1]],
                z=[center[2]],
                mode="markers",
                marker=dict(size=10, color="goldenrod", symbol="circle"),
                name=f"{app.name} (Appendage)",
                text=f"Vol: {volume:.3f}m³",
            )
        )
