# Place W12x26 beams connecting the structural columns on level B01-L00.
# Beams run along each grid line between consecutive columns, at the top of the
# columns (reference level B01-L01). Safe to re-run: existing beams are skipped.
# Works in IronPython2, CPython3 and PythonNet3.
import clr, os, math
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *
from Autodesk.Revit.DB.Structure import StructuralType

doc = DocumentManager.Instance.CurrentDBDocument

COLUMN_LEVEL = 'B01-L00'
BEAM_LEVEL = 'B01-L01'
TYPE_NAME = 'W12x26'
ON_GRID_TOL = 0.25   # ft - column must sit within 3" of a grid line
DUP_TOL = 0.05       # ft

def find_level(name):
    for l in FilteredElementCollector(doc).OfClass(Level):
        if l.Name == name:
            return l
    return None

def type_name(el):
    for bip in (BuiltInParameter.SYMBOL_NAME_PARAM, BuiltInParameter.ALL_MODEL_TYPE_NAME):
        p = el.get_Parameter(bip)
        if p is not None and p.AsString():
            return p.AsString()
    return ''

def find_symbol():
    for s in FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_StructuralFraming).OfClass(FamilySymbol):
        if type_name(s).replace(' ', '').upper() == TYPE_NAME.upper():
            return s
    return None

def try_load_symbol():
    root = 'C:\\ProgramData\\Autodesk\\RVT ' + doc.Application.VersionNumber + '\\Libraries'
    tried = 0
    if not os.path.isdir(root):
        return tried
    for dirpath, dirs, files in os.walk(root):
        if 'Structural Framing' not in dirpath:
            continue
        for f in files:
            fl = f.lower()
            if fl.endswith('.rfa') and fl.startswith('w') and ('wide flange' in fl or 'w shapes' in fl or fl.startswith('w-')):
                tried += 1
                try:
                    doc.LoadFamilySymbol(os.path.join(dirpath, f), TYPE_NAME)
                except Exception:
                    pass
                if find_symbol() is not None:
                    return tried
    return tried

def column_level_id(c):
    p = c.get_Parameter(BuiltInParameter.FAMILY_BASE_LEVEL_PARAM)
    if p is not None and p.AsElementId() != ElementId.InvalidElementId:
        return p.AsElementId()
    return c.LevelId

try:
    base = find_level(COLUMN_LEVEL)
    top = find_level(BEAM_LEVEL)
    if base is None or top is None:
        raise Exception('Level %s or %s not found' % (COLUMN_LEVEL, BEAM_LEVEL))

    # Structural columns on the level
    cols = []
    for c in FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_StructuralColumns).WhereElementIsNotElementType():
        loc = c.Location
        if isinstance(loc, LocationPoint) and column_level_id(c) == base.Id:
            cols.append((loc.Point.X, loc.Point.Y))
    if len(cols) < 2:
        raise Exception('Found %d structural columns on %s - place the columns first.' % (len(cols), COLUMN_LEVEL))

    # For each grid, the columns lying on it, ordered along the grid
    segments = []
    for g in FilteredElementCollector(doc).OfClass(Grid):
        ln = g.Curve
        if not isinstance(ln, Line):
            continue
        p0 = ln.GetEndPoint(0)
        d = ln.Direction
        dx, dy = d.X, d.Y
        L = math.sqrt(dx * dx + dy * dy)
        dx, dy = dx / L, dy / L
        on = []
        for (x, y) in cols:
            vx, vy = x - p0.X, y - p0.Y
            along = vx * dx + vy * dy
            perp = abs(vx * dy - vy * dx)
            if perp <= ON_GRID_TOL:
                on.append((along, x, y))
        on.sort()
        for i in range(len(on) - 1):
            a, b = on[i], on[i + 1]
            if math.hypot(b[1] - a[1], b[2] - a[2]) > DUP_TOL:
                segments.append((g.Name, a[1], a[2], b[1], b[2]))

    sym = find_symbol()
    tried = 0
    if sym is None:
        TransactionManager.Instance.EnsureInTransaction(doc)
        tried = try_load_symbol()
        TransactionManager.Instance.TransactionTaskDone()
        sym = find_symbol()
    if sym is None:
        raise Exception('Type %s is not loaded and was not found in the Revit library (%d files tried). '
                        'Load it via Insert > Load Family > Structural Framing > Steel > AISC > W-Wide Flange '
                        '(pick W12x26), then run again.' % (TYPE_NAME, tried))

    # Existing beams (skip duplicates in either direction)
    existing = []
    for b in FilteredElementCollector(doc).OfCategory(BuiltInCategory.OST_StructuralFraming).WhereElementIsNotElementType():
        loc = b.Location
        if isinstance(loc, LocationCurve):
            e0, e1 = loc.Curve.GetEndPoint(0), loc.Curve.GetEndPoint(1)
            existing.append((e0.X, e0.Y, e1.X, e1.Y))

    def is_dup(x1, y1, x2, y2):
        for (a, b, c, d) in existing:
            if (abs(a - x1) < DUP_TOL and abs(b - y1) < DUP_TOL and abs(c - x2) < DUP_TOL and abs(d - y2) < DUP_TOL) or \
               (abs(a - x2) < DUP_TOL and abs(b - y2) < DUP_TOL and abs(c - x1) < DUP_TOL and abs(d - y1) < DUP_TOL):
                return True
        return False

    z = top.Elevation
    placed, skipped = [], 0
    TransactionManager.Instance.EnsureInTransaction(doc)
    if not sym.IsActive:
        sym.Activate()
        doc.Regenerate()
    for (gname, x1, y1, x2, y2) in segments:
        if is_dup(x1, y1, x2, y2):
            skipped += 1
            continue
        crv = Line.CreateBound(XYZ(x1, y1, z), XYZ(x2, y2, z))
        beam = doc.Create.NewFamilyInstance(crv, sym, top, StructuralType.Beam)
        placed.append('%s: %.1f ft  (id %s)' % (gname, crv.Length, str(beam.Id)))
        existing.append((x1, y1, x2, y2))
    TransactionManager.Instance.TransactionTaskDone()

    OUT = ['Columns found: %d. Placed %d W12x26 beams, skipped %d existing.' % (len(cols), len(placed), skipped)] + placed
except Exception as e:
    try:
        TransactionManager.Instance.ForceCloseTransaction()
    except Exception:
        pass
    OUT = 'ERROR: ' + str(e)
