using System.Text.Json;
using Autodesk.Revit.DB;
namespace RevitBridge.Tools
{
///
/// Resolves a user-facing category string (friendly alias, category display name,
/// or BuiltInCategory enum name like OST_Walls) to a category ElementId usable
/// with FilteredElementCollector.OfCategoryId. Shared by the query tools.
///
internal static class CategoryResolver
{
private static readonly IReadOnlyDictionary Aliases = new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["wall"] = BuiltInCategory.OST_Walls,
["walls"] = BuiltInCategory.OST_Walls,
["door"] = BuiltInCategory.OST_Doors,
["doors"] = BuiltInCategory.OST_Doors,
["window"] = BuiltInCategory.OST_Windows,
["windows"] = BuiltInCategory.OST_Windows,
["floor"] = BuiltInCategory.OST_Floors,
["floors"] = BuiltInCategory.OST_Floors,
["room"] = BuiltInCategory.OST_Rooms,
["rooms"] = BuiltInCategory.OST_Rooms,
["area"] = BuiltInCategory.OST_Areas,
["areas"] = BuiltInCategory.OST_Areas,
["column"] = BuiltInCategory.OST_Columns,
["columns"] = BuiltInCategory.OST_Columns,
["roof"] = BuiltInCategory.OST_Roofs,
["roofs"] = BuiltInCategory.OST_Roofs,
["stair"] = BuiltInCategory.OST_Stairs,
["stairs"] = BuiltInCategory.OST_Stairs,
["ceiling"] = BuiltInCategory.OST_Ceilings,
["ceilings"] = BuiltInCategory.OST_Ceilings,
["furniture"] = BuiltInCategory.OST_Furniture,
["pipe"] = BuiltInCategory.OST_PipeCurves,
["pipes"] = BuiltInCategory.OST_PipeCurves,
["duct"] = BuiltInCategory.OST_DuctCurves,
["ducts"] = BuiltInCategory.OST_DuctCurves,
["beam"] = BuiltInCategory.OST_StructuralFraming,
["beams"] = BuiltInCategory.OST_StructuralFraming,
["structural framing"] = BuiltInCategory.OST_StructuralFraming,
["generic model"] = BuiltInCategory.OST_GenericModel,
["generic models"] = BuiltInCategory.OST_GenericModel,
["level"] = BuiltInCategory.OST_Levels,
["levels"] = BuiltInCategory.OST_Levels,
["grid"] = BuiltInCategory.OST_Grids,
["grids"] = BuiltInCategory.OST_Grids,
["sheet"] = BuiltInCategory.OST_Sheets,
["sheets"] = BuiltInCategory.OST_Sheets,
["view"] = BuiltInCategory.OST_Views,
["views"] = BuiltInCategory.OST_Views,
};
public static ElementId Resolve(Document doc, string input)
{
string trimmed = input.Trim();
if (trimmed.Length == 0)
throw new ArgumentException("category must be a non-empty string.");
if (Aliases.TryGetValue(trimmed, out var alias))
return new ElementId(alias);
if (TryParseBuiltInCategory(trimmed, out var builtIn))
return new ElementId(builtIn);
if (TryParseBuiltInCategory("OST_" + trimmed.Replace(" ", string.Empty), out builtIn))
return new ElementId(builtIn);
foreach (Category category in doc.Settings.Categories)
{
if (string.Equals(category.Name, trimmed, StringComparison.OrdinalIgnoreCase))
return category.Id;
}
throw new ArgumentException($"Unknown category: {input}. Use a BuiltInCategory enum name (e.g. OST_Walls) or a category display name as shown in Revit (e.g. Walls, Doors, Sheets, Views).");
}
private static bool TryParseBuiltInCategory(string text, out BuiltInCategory category)
{
category = BuiltInCategory.INVALID;
return text.Length > 0
&& char.IsLetter(text[0])
&& Enum.TryParse(text, true, out category)
&& category != BuiltInCategory.INVALID;
}
}
///
/// Resolves a Revit API class name ("Wall", "ViewSheet", "WallType", or a full
/// name like "Autodesk.Revit.DB.Mechanical.Duct") to its Type for OfClass filters.
///
internal static class ElementClassResolver
{
private static readonly Lazy> BySimpleName = new(BuildSimpleNameMap);
public static Type Resolve(string input)
{
string trimmed = input.Trim();
var assembly = typeof(Element).Assembly;
Type? match = trimmed.Contains('.') ? assembly.GetType(trimmed, false, true) : null;
match ??= assembly.GetType("Autodesk.Revit.DB." + trimmed, false, true);
if (match is null)
BySimpleName.Value.TryGetValue(trimmed, out match);
if (match is null || !typeof(Element).IsAssignableFrom(match))
throw new ArgumentException($"Unknown Revit element class: {input}. Use an Element subclass from the Revit API (e.g. Wall, FamilyInstance, ViewSheet, Level, WallType) or a full name like Autodesk.Revit.DB.Mechanical.Duct.");
if (match == typeof(Element))
throw new ArgumentException("of_class must be a concrete Element subclass, not Element itself.");
return match;
}
private static IReadOnlyDictionary BuildSimpleNameMap()
{
Type[] types;
try
{
types = typeof(Element).Assembly.GetTypes();
}
catch (System.Reflection.ReflectionTypeLoadException ex)
{
types = ex.Types.OfType().ToArray();
}
var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var type in types)
{
if (!type.IsPublic || !typeof(Element).IsAssignableFrom(type))
continue;
// Prefer the root Autodesk.Revit.DB namespace when simple names collide.
if (!map.TryGetValue(type.Name, out var existing) || (type.Namespace == "Autodesk.Revit.DB" && existing.Namespace != "Autodesk.Revit.DB"))
map[type.Name] = type;
}
return map;
}
}
///
/// Resolves friendly forge unit names ("millimeters", "squareMeters") or full forge
/// ids ("autodesk.unit.unit:millimeters-1.0.1") to UnitTypeId values, and back.
///
internal static class UnitResolver
{
private static readonly Lazy> ByName = new(Build);
public static ForgeTypeId Resolve(string unit)
{
string trimmed = unit.Trim();
if (ByName.Value.TryGetValue(trimmed, out var match))
return match;
throw new ArgumentException($"Unknown unit: {unit}. Use a forge unit name like millimeters, meters, feet, inches, squareMeters, cubicFeet, degrees.");
}
/// "autodesk.unit.unit:millimeters-1.0.1" -> "millimeters".
public static string ShortName(ForgeTypeId unit)
{
string id = unit.TypeId ?? string.Empty;
int colon = id.IndexOf(':');
string tail = colon >= 0 ? id[(colon + 1)..] : id;
int dash = tail.IndexOf('-');
return dash >= 0 ? tail[..dash] : tail;
}
private static IReadOnlyDictionary Build()
{
var map = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (ForgeTypeId unit in UnitUtils.GetAllUnits())
{
map[ShortName(unit)] = unit;
map[unit.TypeId] = unit;
}
return map;
}
}
/// Identity-only element row shared by the query tools.
internal static class ElementIdentity
{
public static readonly IReadOnlyList Fields = new[] { "id", "name", "category", "typeName", "levelId" };
public static Dictionary Build(Document doc, Element element, IReadOnlyList fields)
{
var row = new Dictionary(fields.Count);
foreach (string field in fields)
{
row[field] = field switch
{
"id" => (object?)element.Id.Value,
"name" => element.Name,
"category" => element.Category?.Name,
"typeName" => doc.GetElement(element.GetTypeId())?.Name,
"levelId" => element.LevelId is { } levelId && levelId != ElementId.InvalidElementId ? levelId.Value : (long?)null,
_ => null,
};
}
return row;
}
}
/// Small JsonElement argument readers shared by the tools.
internal static class JsonArgs
{
public static string? GetString(JsonElement args, string name)
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: null;
public static bool GetBool(JsonElement args, string name, bool fallback)
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind is JsonValueKind.True or JsonValueKind.False
? value.GetBoolean()
: fallback;
public static int GetInt(JsonElement args, string name, int fallback)
=> args.ValueKind == JsonValueKind.Object && args.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out int parsed)
? parsed
: fallback;
public static long? GetLong(JsonElement args, string name)
{
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value))
return null;
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out long fromNumber))
return fromNumber;
if (value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out long fromString))
return fromString;
return null;
}
public static List GetLongArray(JsonElement args, string name)
{
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
throw new ArgumentException($"Missing or invalid required field: {name} must be an array of element ids.");
var result = new List(value.GetArrayLength());
foreach (var item in value.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.Number && item.TryGetInt64(out long fromNumber))
result.Add(fromNumber);
else if (item.ValueKind == JsonValueKind.String && long.TryParse(item.GetString(), out long fromString))
result.Add(fromString);
else
throw new ArgumentException($"{name} must contain only integer element ids.");
}
return result;
}
public static List? GetStringArray(JsonElement args, string name)
{
if (args.ValueKind != JsonValueKind.Object || !args.TryGetProperty(name, out var value) || value.ValueKind != JsonValueKind.Array)
return null;
var result = new List(value.GetArrayLength());
foreach (var item in value.EnumerateArray())
{
string? text = item.ValueKind == JsonValueKind.String ? item.GetString() : null;
if (string.IsNullOrWhiteSpace(text))
throw new ArgumentException($"{name} must contain only non-empty strings.");
result.Add(text.Trim());
}
return result;
}
}
///
/// Optional wrong-document protection for write tools. A write queued while the
/// user switches models would otherwise land in whichever document is active when
/// the queued call runs -- possibly silently, since low element ids resolve in
/// most documents. Tools pass the caller's optional expected_document through
/// here before touching the model.
///
internal static class DocumentGuard
{
/// Throws when expected_document is provided and does not match the
/// active document's title (case-insensitive; the .rvt extension and a
/// detached suffix are tolerated). No-op when the argument is absent.
public static void CheckExpectedDocument(JsonElement args, Document doc)
{
string? expected = JsonArgs.GetString(args, "expected_document");
if (string.IsNullOrWhiteSpace(expected))
return;
string actual = doc.Title;
if (Matches(expected, actual))
return;
throw new ArgumentException(
$"Active document is '{actual}' but this call expected '{expected}'. Nothing was changed. "
+ "The user switched models (or several are open); re-read the target model (get_model_overview) and retry against the right one.");
}
private static bool Matches(string expected, string actual)
{
static string Normalize(string value)
{
string v = value.Trim();
if (v.EndsWith(".rvt", StringComparison.OrdinalIgnoreCase))
v = v[..^4];
if (v.EndsWith("_detached", StringComparison.OrdinalIgnoreCase))
v = v[..^"_detached".Length];
return v;
}
return string.Equals(Normalize(expected), Normalize(actual), StringComparison.OrdinalIgnoreCase);
}
}
}