# -----------------------------------------------------------------------------
# Perimeter Curtain Walls, 5' Vertical Grid  -  Dynamo 3.x (CPython3 / PythonNet3)
#
# Creates four curtain walls around the building perimeter:
#   * Perimeter = outermost grid lines (with the current grid: 1, 5, A, F)
#   * Walls sit WALL_OFFSET outside the grid lines, clear of the columns
#   * Base Level 1, top Level 4
#   * Curtain wall type "Curtain Wall - 5ft Vertical" (duplicated from the
#     project's basic curtain wall type) with vertical grids at Fixed Distance
#     5'-0"; vertical grids centered on each wall so end panels match.
#   * No horizontal grids (change HORIZONTAL_SPACING to add them).
#
#   Plan (current grid, 1' offset):
#
#       +==================================+   y = 81'  (north, Grid A)
#       ||                                ||
#       ||   1      2          3      5   ||
#       ||                                ||
#       +==================================+   y = -1'  (south, Grid F, under cantilever)
#     x = -1'                          x = 81'
#
#   Walls are drawn clockwise so their exterior faces point outward.
#
# Safe to re-run: reuses the wall type if it exists, and skips any wall
# whose location line already exists.
#
# HOW TO USE
#   Dynamo 3.x > Python Script node (CPython3 or PythonNet3) > paste > Run.
#   Ctrl+Z in Revit undoes the whole run.
# -----------------------------------------------------------------------------

import clr
clr.AddReference("RevitAPI")
clr.AddReference("RevitServices")

from Autodesk.Revit.DB import (
    XYZ, Line, Grid, Level, Wall, WallType, WallKind, LocationCurve,
    FilteredElementCollector, BuiltInParameter,
)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument

# ---------------------------------------------------------------- settings ---
BASE_LEVEL = "Level 1"
TOP_LEVEL = "Level 4"
WALL_OFFSET = 1.0            # feet outside the outer grid lines
VERTICAL_SPACING = 5.0       # feet
HORIZONTAL_SPACING = None    # e.g. 5.0 for horizontal grids too; None = no horizontal grids
CENTER_VERTICAL_GRID = True  # center the 5' layout on each wall
NEW_TYPE_NAME = "Curtain Wall - 5ft Vertical"

TOL = 0.01
LAYOUT_NONE, LAYOUT_FIXED_DISTANCE = 0, 1
JUSTIFY_CENTER = 1

# ------------------------------------------------------------- levels -------
levels = {l.Name: l for l in FilteredElementCollector(doc).OfClass(Level)}
for n in (BASE_LEVEL, TOP_LEVEL):
    if n not in levels:
        raise Exception("Level '{}' not found. Levels: {}".format(n, ", ".join(sorted(levels))))
base, top = levels[BASE_LEVEL], levels[TOP_LEVEL]
height = top.Elevation - base.Elevation
if height <= 0:
    raise Exception("{} must be above {}.".format(TOP_LEVEL, BASE_LEVEL))

# ------------------------------------------------- perimeter from grids -----
lines = []
for g in FilteredElementCollector(doc).OfClass(Grid):
    if isinstance(g.Curve, Line):
        a, b = g.Curve.GetEndPoint(0), g.Curve.GetEndPoint(1)
        lines.append((XYZ(a.X, a.Y, 0), XYZ(b.X, b.Y, 0)))


def intersect(a0, a1, b0, b1):
    d1x, d1y = a1.X - a0.X, a1.Y - a0.Y
    d2x, d2y = b1.X - b0.X, b1.Y - b0.Y
    den = d1x * d2y - d1y * d2x
    if abs(den) < 1e-9:
        return None
    t = ((b0.X - a0.X) * d2y - (b0.Y - a0.Y) * d2x) / den
    u = ((b0.X - a0.X) * d1y - (b0.Y - a0.Y) * d1x) / den
    if -1e-6 <= t <= 1 + 1e-6 and -1e-6 <= u <= 1 + 1e-6:
        return XYZ(a0.X + t * d1x, a0.Y + t * d1y, 0)
    return None


pts = []
for i in range(len(lines)):
    for j in range(i + 1, len(lines)):
        p = intersect(lines[i][0], lines[i][1], lines[j][0], lines[j][1])
        if p:
            pts.append(p)
if len(pts) < 4:
    raise Exception("Need a rectangular grid to define the perimeter; found {} intersections.".format(len(pts)))

x0 = min(p.X for p in pts) - WALL_OFFSET
x1 = max(p.X for p in pts) + WALL_OFFSET
y0 = min(p.Y for p in pts) - WALL_OFFSET
y1 = max(p.Y for p in pts) + WALL_OFFSET
z = base.ProjectElevation

# Clockwise in plan -> exterior faces outward
corners = [XYZ(x0, y0, z), XYZ(x0, y1, z), XYZ(x1, y1, z), XYZ(x1, y0, z)]
sides = ["West", "North", "East", "South"]
segments = [(sides[i], corners[i], corners[(i + 1) % 4]) for i in range(4)]

# ------------------------------------------------- curtain wall type --------
wall_types = [wt for wt in FilteredElementCollector(doc).OfClass(WallType) if wt.Kind == WallKind.Curtain]
if not wall_types:
    raise Exception("No curtain wall types in this project. Load or create one and re-run.")


def type_name(wt):
    p = wt.get_Parameter(BuiltInParameter.SYMBOL_NAME_PARAM)
    return p.AsString() if p else wt.Name


def set_param(elem, bip, value):
    p = elem.get_Parameter(bip)
    if p is None or p.IsReadOnly:
        return False
    p.Set(value)
    return True


report = []
existing_walls = []
for w in FilteredElementCollector(doc).OfClass(Wall):
    if isinstance(w.Location, LocationCurve):
        c = w.Location.Curve
        existing_walls.append((c.GetEndPoint(0), c.GetEndPoint(1)))


def wall_exists(p, q):
    def same(a, b):
        return abs(a.X - b.X) < TOL and abs(a.Y - b.Y) < TOL
    return any((same(a, p) and same(b, q)) or (same(a, q) and same(b, p)) for a, b in existing_walls)


TransactionManager.Instance.EnsureInTransaction(doc)
try:
    cw_type = next((wt for wt in wall_types if type_name(wt) == NEW_TYPE_NAME), None)
    if cw_type is None:
        source = next((wt for wt in wall_types if type_name(wt) == "Curtain Wall"), wall_types[0])
        cw_type = source.Duplicate(NEW_TYPE_NAME)
        report.append("Created wall type '{}' from '{}'".format(NEW_TYPE_NAME, type_name(source)))

    def set_named(elem, bip_name, value):
        bip = getattr(BuiltInParameter, bip_name, None)
        ok = bip is not None and set_param(elem, bip, value)
        if not ok:
            report.append("Note: couldn't set {} - check the type's grid settings".format(bip_name))
        return ok

    set_named(cw_type, "SPACING_LAYOUT_VERT", LAYOUT_FIXED_DISTANCE)
    set_named(cw_type, "SPACING_LENGTH_VERT", VERTICAL_SPACING)
    if HORIZONTAL_SPACING:
        set_named(cw_type, "SPACING_LAYOUT_HORIZ", LAYOUT_FIXED_DISTANCE)
        set_named(cw_type, "SPACING_LENGTH_HORIZ", HORIZONTAL_SPACING)
    else:
        set_named(cw_type, "SPACING_LAYOUT_HORIZ", LAYOUT_NONE)

    for side, p, q in segments:
        if wall_exists(p, q):
            report.append("Skipped {} wall - already exists".format(side))
            continue
        wall = Wall.Create(doc, Line.CreateBound(p, q), cw_type.Id, base.Id, height, 0.0, False, False)
        set_param(wall, BuiltInParameter.WALL_BASE_OFFSET, 0.0)
        set_param(wall, BuiltInParameter.WALL_HEIGHT_TYPE, top.Id)
        set_param(wall, BuiltInParameter.WALL_TOP_OFFSET, 0.0)
        if CENTER_VERTICAL_GRID:
            bip = getattr(BuiltInParameter, "CURTAINGRID_ALIGN_VERT", None)
            try:
                if bip is not None:
                    set_param(wall, bip, JUSTIFY_CENTER)
            except Exception:
                pass  # keep default (Beginning) if not settable in this version
        length = p.DistanceTo(q)
        report.append("{} wall: {:.0f}' long, {} panels @ {:.0f}' + ends".format(
            side, length, int(length // VERTICAL_SPACING), VERTICAL_SPACING))
finally:
    TransactionManager.Instance.TransactionTaskDone()

report.append("Curtain walls {} -> {} ({:.0f}' tall), {:.0f}' outside the outer grids".format(
    BASE_LEVEL, TOP_LEVEL, height, WALL_OFFSET))

OUT = report
