# -----------------------------------------------------------------------------
# Curtain Wall Checkerboard  -  Dynamo 3.x (CPython3 / PythonNet3)
#
# Updates the perimeter curtain walls (from create_perimeter_curtain_walls.py):
#   * Vertical grids: Fixed Distance 2'-6", justified to the CENTER of each wall
#   * Horizontal grids: one row per floor - grid lines at Level 2 and Level 3
#   * Panels alternate Glazed / Solid in a checkerboard (by column and floor)
#
#   Elevation of one wall (G = glazed, S = solid), each cell 2'-6" x one floor:
#
#     Level 4  ---------------------------------------
#              | S | G | S | G | S | G | S | G | ...
#     Level 3  ---------------------------------------
#              | G | S | G | S | G | S | G | S | ...
#     Level 2  ---------------------------------------
#              | S | G | S | G | S | G | S | G | ...
#     Level 1  ---------------------------------------
#                            ^ center of wall
#
# The wall type is renamed "Curtain Wall - 2.5ft Vertical". Panel types used:
# "Glazed" and "Solid" (the standard System Panel types). Panels are unpinned
# so their type can be changed.
#
# Safe to re-run: existing level grid lines are reused and panels are simply
# re-assigned.
#
# 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, Level, Wall, WallType, WallKind, PanelType, LocationCurve,
    FilteredElementCollector, BuiltInParameter,
)
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

doc = DocumentManager.Instance.CurrentDBDocument

# ---------------------------------------------------------------- settings ---
WALL_TYPE_NAMES = ["Curtain Wall - 5ft Vertical", "Curtain Wall - 2.5ft Vertical"]
RENAME_TYPE_TO = "Curtain Wall - 2.5ft Vertical"
VERTICAL_SPACING = 2.5
ROW_LEVELS = ["Level 2", "Level 3"]   # horizontal grid line at each of these levels
GLAZED_PANEL = "Glazed"
SOLID_PANEL = "Solid"
START_WITH_SOLID = True               # bottom-left panel of each wall

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


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


def set_named(elem, bip_name, value, report):
    bip = getattr(BuiltInParameter, bip_name, None)
    p = elem.get_Parameter(bip) if bip is not None else None
    if p is None or p.IsReadOnly:
        report.append("Note: couldn't set {} on {}".format(bip_name, elem.Id))
        return False
    p.Set(value)
    return True


# ------------------------------------------------------------- lookups ------
levels = {l.Name: l for l in FilteredElementCollector(doc).OfClass(Level)}
missing = [n for n in ROW_LEVELS if n not in levels]
if missing:
    raise Exception("Level(s) not found: {}".format(", ".join(missing)))

cw_types = [wt for wt in FilteredElementCollector(doc).OfClass(WallType)
            if wt.Kind == WallKind.Curtain and type_name(wt) in WALL_TYPE_NAMES]
if not cw_types:
    raise Exception("Curtain wall type '{}' not found. Run create_perimeter_curtain_walls.py first."
                    .format(WALL_TYPE_NAMES[0]))
cw_type = cw_types[0]

walls = [w for w in FilteredElementCollector(doc).OfClass(Wall)
         if w.GetTypeId() == cw_type.Id and isinstance(w.Location, LocationCurve)]
if not walls:
    raise Exception("No walls of type '{}' found.".format(type_name(cw_type)))

panel_types = {type_name(pt): pt for pt in FilteredElementCollector(doc).OfClass(PanelType)}
glazed = next((pt for n, pt in panel_types.items() if n.lower() == GLAZED_PANEL.lower()), None)
solid = next((pt for n, pt in panel_types.items() if n.lower() == SOLID_PANEL.lower()), None)
if glazed is None or solid is None:
    raise Exception("Panel types needed: '{}' and '{}'. Found: {}. Load System Panel types and re-run."
                    .format(GLAZED_PANEL, SOLID_PANEL, ", ".join(sorted(panel_types)) or "none"))


def cluster(values):
    """Sorted unique values, merging ones within TOL."""
    out = []
    for v in sorted(values):
        if not out or abs(v - out[-1]) > TOL:
            out.append(v)
    return out


def index_of(v, keys):
    return min(range(len(keys)), key=lambda i: abs(keys[i] - v))


# ---------------------------------------------------------------- update ----
report = []
counts = {"glazed": 0, "solid": 0, "failed": 0}

TransactionManager.Instance.EnsureInTransaction(doc)
try:
    # 1. Wall type: 2.5' vertical grid, no automatic horizontal grid
    set_named(cw_type, "SPACING_LAYOUT_VERT", LAYOUT_FIXED_DISTANCE, report)
    set_named(cw_type, "SPACING_LENGTH_VERT", VERTICAL_SPACING, report)
    set_named(cw_type, "SPACING_LAYOUT_HORIZ", LAYOUT_NONE, report)
    if type_name(cw_type) != RENAME_TYPE_TO:
        try:
            cw_type.Name = RENAME_TYPE_TO
            report.append("Wall type renamed to '{}'".format(RENAME_TYPE_TO))
        except Exception:
            pass

    # 2. Center-justify vertical grids on each wall
    for w in walls:
        set_named(w, "CURTAINGRID_ALIGN_VERT", JUSTIFY_CENTER, report)
    doc.Regenerate()

    # 3. Horizontal grid lines at each floor level
    for w in walls:
        grid = w.CurtainGrid
        c = w.Location.Curve
        mid = c.Evaluate(0.5, True)
        existing_z = []
        for gid in grid.GetUGridLineIds():
            gl = doc.GetElement(gid)
            existing_z.append(gl.FullCurve.GetEndPoint(0).Z)
        for lv in ROW_LEVELS:
            z = levels[lv].ProjectElevation
            if any(abs(z - ez) < TOL for ez in existing_z):
                continue
            grid.AddGridLine(True, XYZ(mid.X, mid.Y, z), False)
    doc.Regenerate()

    # 4. Checkerboard panels
    for w in walls:
        c = w.Location.Curve
        start = c.GetEndPoint(0)
        direction = (c.GetEndPoint(1) - start).Normalize()

        panels = []
        for pid in w.CurtainGrid.GetPanelIds():
            p = doc.GetElement(pid)
            bb = p.get_BoundingBox(None) if p else None
            if bb is None:
                continue
            ctr = (bb.Min + bb.Max).Multiply(0.5)
            along = (XYZ(ctr.X, ctr.Y, start.Z) - start).DotProduct(direction)
            panels.append((p, along, ctr.Z))

        cols = cluster([a for _, a, _ in panels])
        rows = cluster([z for _, _, z in panels])

        for p, along, zc in panels:
            ci, ri = index_of(along, cols), index_of(zc, rows)
            use_solid = ((ci + ri) % 2 == 0) == START_WITH_SOLID
            target = solid if use_solid else glazed
            try:
                if p.Pinned:
                    p.Pinned = False
                if p.GetTypeId() != target.Id:
                    p.ChangeTypeId(target.Id)
                counts["solid" if use_solid else "glazed"] += 1
            except Exception:
                counts["failed"] += 1

        report.append("Wall {}: {} columns x {} rows".format(w.Id, len(cols), len(rows)))
finally:
    TransactionManager.Instance.TransactionTaskDone()

report.insert(0, "{} walls updated: {} glazed, {} solid panels".format(
    len(walls), counts["glazed"], counts["solid"]))
if counts["failed"]:
    report.append("{} panels could not be changed".format(counts["failed"]))

OUT = report
