"""Build a schematic, parametric solar-electric spacecraft layout in CadQuery.

All dimensions are arbitrary visualization units represented in millimetres
for neutral CAD exchange only. No mass properties or engineering performance
are represented. Rebuild with Python 3.12, CadQuery 2.7, and matplotlib.
"""
from __future__ import annotations

import argparse
import csv
from pathlib import Path

import cadquery as cq
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from matplotlib.patches import Patch


# Millimetres for CAD exchange only; these are display proportions, not a
# dimensioned flight vehicle. The source concept specifies no actual sizes.
P = {
    "bus_width_x": 42.0,
    "bus_width_y": 48.0,
    "bus_height_z": 76.0,
    "bus_shell_wall": 3.0,
    "bus_center_z": 0.0,
    "array_panel_dx": 58.0,
    "array_panel_dz": 68.0,
    "array_panel_t": 2.0,
    "array_center_x": 72.0,
    "array_center_z": 5.0,
    "cell_dx": 17.5,
    "cell_dz": 30.0,
    "cell_t": 1.2,
    "tank_radius": 13.0,
    "tank_height": 30.0,
    "thruster_ring_outer_radius": 13.0,
    "thruster_ring_inner_radius": 8.5,
    "radiator_dx": 28.0,
    "radiator_dy": 2.0,
    "radiator_dz": 40.0,
}

COLORS = {
    "bus": (0.35, 0.51, 0.70),
    "array": (0.39, 0.27, 0.72),
    "cell": (0.52, 0.86, 1.00),
    "tank": (0.83, 0.32, 0.65),
    "thruster": (0.66, 0.49, 0.88),
    "radiator": (0.25, 0.69, 0.82),
    "boom": (0.76, 0.83, 0.94),
    "harness": (0.96, 0.40, 0.78),
}


def box(dx: float, dy: float, dz: float,
        center: tuple[float, float, float]) -> cq.Shape:
    return cq.Workplane("XY").box(dx, dy, dz).translate(center).val()


def rod(start: tuple[float, float, float], end: tuple[float, float, float],
        radius: float) -> cq.Shape:
    a, b = cq.Vector(*start), cq.Vector(*end)
    delta = b - a
    return cq.Solid.makeCylinder(radius, delta.Length, a, delta.normalized())


def build_parts() -> list[tuple[str, str, cq.Shape, tuple[float, float, float]]]:
    p = P
    parts: list[tuple[str, str, cq.Shape, tuple[float, float, float]]] = []
    bus_z = p["bus_center_z"]
    bx, by, bz = p["bus_width_x"], p["bus_width_y"], p["bus_height_z"]

    # A hollow rectangular enclosure assembled from six editable plates. The
    # mobile/section render removes the front access cover to show the interior.
    wall = p["bus_shell_wall"]
    parts.extend([
        ("Bus left side wall", "Central spacecraft bus", box(wall, by, bz, (-bx / 2 + wall / 2, 0, bus_z)), COLORS["bus"]),
        ("Bus right side wall", "Central spacecraft bus", box(wall, by, bz, (bx / 2 - wall / 2, 0, bus_z)), COLORS["bus"]),
        ("Bus rear wall", "Central spacecraft bus", box(bx - 2 * wall, wall, bz, (0, by / 2 - wall / 2, bus_z)), COLORS["bus"]),
        ("Bus top wall", "Central spacecraft bus", box(bx, by, wall, (0, 0, bus_z + bz / 2 - wall / 2)), COLORS["bus"]),
        ("Bus bottom wall", "Central spacecraft bus", box(bx, by, wall, (0, 0, bus_z - bz / 2 + wall / 2)), COLORS["bus"]),
        ("Bus front access cover", "Central spacecraft bus", box(bx - 2 * wall, wall, bz - 2 * wall,
              (0, -by / 2 + wall / 2, bus_z)), COLORS["bus"]),
    ])
    # Generic interior blocks give the cutaway a concrete scale reference;
    # their arrangement is illustrative and does not encode a real spacecraft.
    interior = [
        ("Generic power-electronics placeholder", (0.0, -3.0, 17.0), (19.0, 15.0, 14.0)),
        ("Generic control/payload placeholder", (0.0, 3.0, -3.0), (20.0, 16.0, 20.0)),
        ("Generic support-equipment placeholder", (0.0, -2.0, -22.0), (17.0, 14.0, 10.0)),
    ]
    for name, center, dims in interior:
        part = box(*dims, center)
        parts.append((name, "Generic bus-interior placeholder", part, (0.46, 0.61, 0.79)))

    # A single generic carried-reaction-mass tank; its contents are not modeled.
    tank_bottom = bus_z + bz / 2 + 5.0
    tank = (cq.Workplane("XY").circle(p["tank_radius"]).extrude(p["tank_height"])
            .edges().fillet(2.0).translate((0, 0, tank_bottom)).val())
    parts.append(("Propellant tank (generic)", "Stored reaction mass", tank, COLORS["tank"]))

    # Two flat array wings with 3 x 2 raised cell tiles on each wing.
    for side, sign in (("Port", -1), ("Starboard", 1)):
        cx = sign * p["array_center_x"]
        plate = box(p["array_panel_dx"], p["array_panel_t"], p["array_panel_dz"],
                    (cx, 0, p["array_center_z"]))
        parts.append((f"{side} solar-array substrate", "Solar arrays", plate, COLORS["array"]))
        x_offsets = (-19.0, 0.0, 19.0)
        z_offsets = (-17.0, 17.0)
        for face, face_sign in (("front", -1), ("back", 1)):
            for row, dz in enumerate(z_offsets, start=1):
                for col, dx in enumerate(x_offsets, start=1):
                    tile = box(p["cell_dx"], p["cell_t"], p["cell_dz"],
                               (cx + dx, face_sign * (p["array_panel_t"] / 2 + p["cell_t"] / 2),
                                p["array_center_z"] + dz))
                    parts.append((f"{side} solar-cell layout marker {face} {row}-{col}",
                                  "Solar arrays", tile, COLORS["cell"]))
        # Simple structural booms join the bus to each array root.
        parts.append((f"{side} array support boom", "Structure placeholder",
                      rod((sign * bx * 0.49, 0, p["array_center_z"]),
                          (sign * (p["array_center_x"] - p["array_panel_dx"] / 2), 0,
                           p["array_center_z"]), 1.8), COLORS["boom"]))
        # A route marker is a solid CAD cylinder only to make the layout legible.
        parts.append((f"{side} illustrative electrical harness", "Routing placeholder",
                      rod((sign * (p["array_center_x"] - p["array_panel_dx"] / 2), 0,
                           p["array_center_z"] - 2.4),
                          (sign * bx * 0.49, 0, p["array_center_z"] - 2.4), 0.8), COLORS["harness"]))

    # Generic electric-thruster head below the bus, with no specific internal
    # Hall, ion-grid, cathode, or magnetic topology implied.
    thruster_z_top = bus_z - bz / 2 - 3.0
    thruster_body = cq.Solid.makeCylinder(11.0, 17.0,
        cq.Vector(0, 0, thruster_z_top - 17.0), cq.Vector(0, 0, 1))
    parts.append(("Generic electric-thruster housing", "Electric thruster", thruster_body, COLORS["thruster"]))
    ring_z = thruster_z_top - 20.0
    ring = (cq.Workplane("XY").circle(p["thruster_ring_outer_radius"])
            .circle(p["thruster_ring_inner_radius"]).extrude(3.0)
            .translate((0, 0, ring_z)).val())
    parts.append(("Generic thruster exit ring", "Electric thruster", ring, COLORS["thruster"]))

    # One distinct thermal-radiator panel and four raised fins; no heat load or
    # required radiator area is calculated.
    rx = bx / 2 + p["radiator_dx"] / 2 + 5.0
    rz = -55.0
    radiator = box(p["radiator_dx"], p["radiator_dy"], p["radiator_dz"],
                   (rx, 0, rz))
    parts.append(("Separate radiator panel", "Thermal-management placeholder", radiator, COLORS["radiator"]))
    for index, dz in enumerate((-14.0, -5.0, 5.0, 14.0), start=1):
        fin = box(p["radiator_dx"] + 3.0, p["radiator_dy"] * 2.0, 1.3,
                  (rx, 0, rz + dz))
        parts.append((f"Radiator fin {index}", "Thermal-management placeholder", fin, COLORS["radiator"]))

    return parts


def render(parts, output_dir: Path, view: str) -> Path:
    is_mobile = view == "mobile-section"
    fig = plt.figure(figsize=(6.0, 8.2) if is_mobile else (12.0, 8.0), facecolor="#07152e")
    ax = fig.add_subplot(111, projection="3d", facecolor="#07152e")
    ax.set_proj_type("ortho")
    for name, _group, shape, color in parts:
        if is_mobile and name == "Bus front access cover":
            # A genuine section view: temporarily remove the CAD cover component
            # so the separately modeled generic interior is visible.
            continue
        vertices, triangles = shape.tessellate(0.5, 0.2)
        xyz = [(v.x, v.y, v.z) for v in vertices]
        faces = [[xyz[i] for i in face] for face in triangles]
        mesh = Poly3DCollection(faces, facecolor=color, edgecolor="none", linewidth=0,
                                alpha=0.97)
        ax.add_collection3d(mesh)
    if is_mobile:
        # Orthographic side/section view aligned with the original conceptual
        # presentation. Looking along negative Y exposes the axis arrangement.
        ax.view_init(elev=0, azim=-90)
        ax.set_xlim(-130, 130)
        ax.set_ylim(-48, 48)
        ax.set_zlim(-82, 110)
        ax.set_box_aspect((260, 96, 192))
        ax.set_position([0.04, 0.17, 0.92, 0.68])
        fig.text(0.055, 0.955, "SOLAR-ELECTRIC SPACECRAFT · SECTION", color="#f1f5ff",
                 fontsize=17, weight="bold", family="DejaVu Sans")
        fig.text(0.058, 0.918, "MODEL-DERIVED BUS CUTAWAY · SCHEMATIC · NOT TO SCALE",
                 color="#69d9f7", fontsize=10, family="DejaVu Sans")
        fig.text(0.06, 0.105, "Bus cover omitted; internal blocks are generic placeholders.",
                 color="#d8e5f8", fontsize=9.3, family="DejaVu Sans")
        fig.text(0.06, 0.072, "Arrays supply electricity; the thruster accelerates carried propellant.",
                 color="#a9bbd8", fontsize=9.1, family="DejaVu Sans")
        fig.text(0.06, 0.038, "No mass, thrust, power, thermal or mission performance is modeled.",
                 color="#a9bbd8", fontsize=9.1, family="DejaVu Sans")
    else:
        ax.view_init(elev=34, azim=-112)
        ax.set_xlim(-120, 120)
        ax.set_ylim(-68, 68)
        ax.set_zlim(-82, 112)
        ax.set_box_aspect((240, 136, 194))
        ax.set_position([0.01, 0.21, 0.69, 0.72])
        fig.text(0.055, 0.955, "SOLAR-ELECTRIC SPACECRAFT", color="#f1f5ff",
                 fontsize=21, weight="bold", family="DejaVu Sans")
        fig.text(0.058, 0.915, "PARAMETRIC OPEN CASCADE MODEL  ·  SCHEMATIC ONLY",
                 color="#69d9f7", fontsize=11, family="DejaVu Sans")
        legend = [
            Patch(facecolor=COLORS["array"], label="Solar-array substrate"),
            Patch(facecolor=COLORS["cell"], label="Raised cell tiles"),
            Patch(facecolor=COLORS["bus"], label="Central spacecraft bus"),
            Patch(facecolor=COLORS["tank"], label="Generic propellant tank"),
            Patch(facecolor=COLORS["thruster"], label="Generic electric thruster"),
            Patch(facecolor=COLORS["radiator"], label="Separate radiator"),
            Patch(facecolor=COLORS["harness"], label="Illustrative harness route"),
        ]
        fig.legend(handles=legend, loc="center left", bbox_to_anchor=(0.72, 0.53),
                   frameon=False, fontsize=10, labelcolor="#dce8ff", handlelength=1.3)
        fig.text(0.73, 0.29, "Thrust axis ↑", color="#69d9f7", fontsize=12, weight="bold")
        fig.text(0.73, 0.245, "Exhaust direction ↓", color="#c4d2ec", fontsize=10)
        fig.text(0.055, 0.105, "Arbitrary visualization dimensions; not a scaled or buildable spacecraft.",
                 color="#c4d2ec", fontsize=10, family="DejaVu Sans")
        fig.text(0.055, 0.068, "No mass properties, thruster design, power, heat rejection or mission result is calculated.",
                 color="#93a9c9", fontsize=9.3, family="DejaVu Sans")
    ax.set_axis_off()
    path = output_dir / ("solar-electric-spacecraft-mobile-section.png" if is_mobile
                         else "solar-electric-spacecraft-isometric.png")
    fig.savefig(path, dpi=190, facecolor=fig.get_facecolor(), bbox_inches="tight", pad_inches=0.24)
    plt.close(fig)
    return path


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output-dir", type=Path, default=Path(__file__).resolve().parent)
    args = parser.parse_args()
    out = args.output_dir.resolve()
    out.mkdir(parents=True, exist_ok=True)
    parts = build_parts()

    assembly = cq.Assembly(name="Solar-electric spacecraft schematic layout")
    solids: list[cq.Shape] = []
    for name, group, shape, rgb in parts:
        if not shape.isValid() or shape.Volume() <= 0:
            raise RuntimeError(f"Invalid or zero-volume model solid: {name}")
        solids.append(shape)
        assembly.add(shape, name=name.replace(" ", "_"), color=cq.Color(*rgb), loc=cq.Location())

    step_path = out / "solar-electric-spacecraft-parametric-layout.step"
    stl_path = out / "solar-electric-spacecraft-parametric-layout.stl"
    assembly.export(str(step_path))
    cq.Compound.makeCompound(solids).exportStl(str(stl_path), tolerance=0.3,
                                                 angularTolerance=0.18)

    with (out / "solar-electric-spacecraft-components.csv").open("w", newline="", encoding="utf-8") as stream:
        writer = csv.writer(stream)
        writer.writerow(["component", "group", "bounding span X (visual mm)",
                         "bounding span Y (visual mm)", "bounding span Z (visual mm)",
                         "interpretation / limitation"])
        for name, group, shape, _color in parts:
            bounds = shape.BoundingBox()
            writer.writerow([name, group, f"{bounds.xlen:.2f}", f"{bounds.ylen:.2f}",
                             f"{bounds.zlen:.2f}",
                             "arbitrary visualization geometry; not engineering dimensions"])
        writer.writerow(["All model dimensions", "Units", "mm for CAD exchange only", "—", "—",
                         "proportions are illustrative, not scale or engineering dimensions"])

    render(parts, out, "isometric")
    render(parts, out, "mobile-section")
    print(f"Built {len(solids)} valid parametric solids. STEP: {step_path.name}; STL: {stl_path.name}.")


if __name__ == "__main__":
    main()
