# ---------------------------------------------------------------
# Dynamo Python Script: Wood floor from Level 1 walls
#
# What it does:
#   1. Collects every wall on the chosen level.
#   2. Chains the walls' location lines into one closed loop.
#   3. Offsets that loop outward by half the wall thickness so the
#      floor edge lands on the OUTER face of the walls.
#   4. Creates a floor of the chosen type on that level.
#
# MODIFIES THE MODEL (creates one floor). Undo with Ctrl+Z if needed.
#
# Inputs (all optional - leave unconnected to use the defaults):
#   IN[0]  Level name  (string)  default: "Level 1"
#   IN[1]  Floor type  (string)  default: "Wood Joist 10\" - Wood Finish"
#   IN[2]  Boundary    (string)  "outer" (default) or "center"
#
# Works with IronPython 2.7 and CPython 3 (Dynamo 2.x and 3.x).
# Requires Revit 2022 or newer (uses Floor.Create).
# ---------------------------------------------------------------
import clr
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager

clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

clr.AddReference('System')
from System.Collections.Generic import List

doc = DocumentManager.Instance.CurrentDBDocument


def get_input(index, default):
    """Read IN[index] if it was connected, otherwise use the default."""
    try:
        value = IN[index]
        if value is None or str(value).strip() == "":
            return default
        return str(value).strip()
    except Exception:
        return default


LEVEL_NAME = get_input(0, "Level 1")
FLOOR_TYPE_NAME = get_input(1, 'Wood Joist 10" - Wood Finish')
BOUNDARY_MODE = get_input(2, "outer").lower()

TOL = 0.01  # feet (~1/8") - how close wall ends must be to count as connected


def flatten(curve, z):
    """Move a curve so both ends sit at elevation z (floor sketch must be flat)."""
    p0 = curve.GetEndPoint(0)
    p1 = curve.GetEndPoint(1)
    if isinstance(curve, Line):
        return Line.CreateBound(XYZ(p0.X, p0.Y, z), XYZ(p1.X, p1.Y, z))
    # Arcs and other curves: shift the whole curve vertically
    return curve.CreateTransformed(Transform.CreateTranslation(XYZ(0, 0, z - p0.Z)))


def chain_curves(curves):
    """Order the curves end-to-start so they form one continuous closed loop."""
    remaining = list(curves)
    ordered = [remaining.pop(0)]
    while remaining:
        end_pt = ordered[-1].GetEndPoint(1)
        found = False
        for i, c in enumerate(remaining):
            if c.GetEndPoint(0).IsAlmostEqualTo(end_pt, TOL):
                ordered.append(remaining.pop(i))
                found = True
                break
            if c.GetEndPoint(1).IsAlmostEqualTo(end_pt, TOL):
                ordered.append(remaining.pop(i).CreateReversed())
                found = True
                break
        if not found:
            raise Exception(
                "The walls don't form a closed loop - a wall end doesn't meet another wall "
                "near point ({0:.2f}, {1:.2f}). Check that the walls are joined at the corners."
                .format(end_pt.X, end_pt.Y))
    if not ordered[-1].GetEndPoint(1).IsAlmostEqualTo(ordered[0].GetEndPoint(0), TOL):
        raise Exception("The walls form an open chain, not a closed loop.")
    return ordered


def loop_length(loop):
    return sum(c.Length for c in loop)


try:
    # --- 1. Find the level -----------------------------------------
    level = None
    for lv in FilteredElementCollector(doc).OfClass(Level).ToElements():
        if lv.Name == LEVEL_NAME:
            level = lv
            break
    if level is None:
        raise Exception("Level '{0}' not found (names are case-sensitive).".format(LEVEL_NAME))

    # --- 2. Find the floor type ------------------------------------
    floor_type = None
    for ft in FilteredElementCollector(doc).OfClass(FloorType).ToElements():
        # Read the type name from its parameter - works in both IronPython and CPython
        # (Element.Name.GetValue() is IronPython-only and fails in Dynamo 3.x)
        p = ft.get_Parameter(BuiltInParameter.SYMBOL_NAME_PARAM)
        name = p.AsString() if p else None
        if name == FLOOR_TYPE_NAME:
            floor_type = ft
            break
    if floor_type is None:
        raise Exception("Floor type '{0}' not found (names are case-sensitive).".format(FLOOR_TYPE_NAME))

    # --- 3. Collect the walls on that level ------------------------
    walls = [w for w in FilteredElementCollector(doc)
             .OfCategory(BuiltInCategory.OST_Walls)
             .WhereElementIsNotElementType()
             .ToElements()
             if isinstance(w, Wall) and w.LevelId == level.Id
             and isinstance(w.Location, LocationCurve)]
    if len(walls) < 3:
        raise Exception("Found {0} wall(s) on {1}; need at least 3 to enclose a floor."
                        .format(len(walls), LEVEL_NAME))

    # --- 4. Build a closed loop from the wall location lines -------
    z = level.Elevation
    curves = [flatten(w.Location.Curve, z) for w in walls]
    ordered = chain_curves(curves)

    center_loop = CurveLoop()
    for c in ordered:
        center_loop.Append(c)

    # --- 5. Offset outward to the outer face of the walls ----------
    # Assumes the walls' Location Line is "Wall Centerline" (Revit's default)
    # and that all walls share the same thickness.
    if BOUNDARY_MODE == "center":
        boundary = center_loop
    else:
        half_width = walls[0].Width / 2.0
        a = CurveLoop.CreateViaOffset(center_loop, half_width, XYZ.BasisZ)
        b = CurveLoop.CreateViaOffset(center_loop, -half_width, XYZ.BasisZ)
        # The outward offset is the one with the longer perimeter
        boundary = a if loop_length(a) > loop_length(b) else b

    loops = List[CurveLoop]()
    loops.Add(boundary)

    # --- 6. Create the floor ---------------------------------------
    TransactionManager.Instance.EnsureInTransaction(doc)
    try:
        floor = Floor.Create(doc, loops, floor_type.Id, level.Id)
        TransactionManager.Instance.TransactionTaskDone()
    except Exception as e:
        TransactionManager.Instance.ForceCloseTransaction()
        raise e

    OUT = "Created floor (Id {0}) of type '{1}' on {2}, bounded by {3} walls ({4} boundary)." \
        .format(floor.Id, FLOOR_TYPE_NAME, LEVEL_NAME, len(walls), BOUNDARY_MODE)

except Exception as e:
    OUT = "ERROR: " + str(e)
