export type BimLevel = { id: string; label: string; elevation: number }
export type BimSite = { id: string; label: string; latitude?: number; longitude?: number }
export type BimBuilding = { id: string; label: string; siteId: string }
export type BimSpace = { id: string; label: string; levelId: string; area?: number }
export type BimMaterial = { id: string; name: string; category?: string }
export type BimElement = { id: string; label: string; type: 'wall' | 'structure' | 'window' | 'door' | 'space' | 'generic'; shapeId: string; levelId: string; spaceId?: string; materialId?: string; classification?: { system: string; code: string }; properties: Record<string, string | number | boolean>; quantity: { length?: number; area?: number; volume?: number } }
export type BimSnapshot = { id: string; label: string; sites: BimSite[]; buildings: BimBuilding[]; levels: BimLevel[]; spaces: BimSpace[]; materials: BimMaterial[]; elements: BimElement[]; version: number }
export type BimIfcSchema = 'IFC2X3' | 'IFC4'
export type BimIfcImport = { schema: BimIfcSchema; entities: number; hierarchy: number; propertySets: number; classifications: number }
export type BimApi = { snapshot(): BimSnapshot; addSite(input: BimSite): BimSite; addBuilding(input: BimBuilding): BimBuilding; addLevel(input: BimLevel & { buildingId?: string }): BimLevel; addSpace(input: BimSpace): BimSpace; addMaterial(input: BimMaterial): BimMaterial; addElement(input: BimElement): BimElement; setElementProperty(elementId: string, name: string, value: string | number | boolean): BimElement; assignMaterial(elementId: string, materialId: string): BimElement; setClassification(elementId: string, classification: { system: string; code: string }): BimElement; schedule(): Array<{ elementId: string; type: string; quantity: BimElement['quantity'] }>; exportIfc(schema?: BimIfcSchema): string; importIfc(serialized: string): BimIfcImport }
const finite = (value: number, label: string) => { if (!Number.isFinite(value)) throw new RangeError(`${label} must be finite.`) }
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
const ifcEscape = (value: string) => value.replace(/'/g, "''")

export const createBimModel = (id = 'bim', label = 'BIM model'): BimApi => {
  const sites = new Map<string, BimSite>(); const buildings = new Map<string, BimBuilding>(); const levels = new Map<string, BimLevel>(); const spaces = new Map<string, BimSpace>(); const materials = new Map<string, BimMaterial>(); const elements = new Map<string, BimElement>(); const levelBuildings = new Map<string, string>(); let version = 0
  const snapshot = (): BimSnapshot => ({ id, label, sites: [...sites.values()].map(clone), buildings: [...buildings.values()].map(clone), levels: [...levels.values()].map(clone), spaces: [...spaces.values()].map(clone), materials: [...materials.values()].map(clone), elements: [...elements.values()].map(clone), version })
  const site = (siteId: string) => { const value = sites.get(siteId); if (!value) throw new RangeError(`BIM site does not exist: ${siteId}`); return value }
  const building = (buildingId: string) => { const value = buildings.get(buildingId); if (!value) throw new RangeError(`BIM building does not exist: ${buildingId}`); return value }
  const level = (id: string) => { const value = levels.get(id); if (!value) throw new RangeError(`BIM level does not exist: ${id}`); return value }
  const space = (id: string) => { const value = spaces.get(id); if (!value) throw new RangeError(`BIM space does not exist: ${id}`); return value }
  const material = (id: string) => { const value = materials.get(id); if (!value) throw new RangeError(`BIM material does not exist: ${id}`); return value }
  const element = (id: string) => { const value = elements.get(id); if (!value) throw new RangeError(`BIM element does not exist: ${id}`); return value }
  const addSite = (input: BimSite) => { if (!input.id.trim() || sites.has(input.id)) throw new RangeError(`BIM site already exists: ${input.id}`); for (const coordinate of [input.latitude, input.longitude]) if (coordinate !== undefined) finite(coordinate, 'BIM site coordinate'); sites.set(input.id, clone(input)); version += 1; return clone(input) }
  const addBuilding = (input: BimBuilding) => { if (!input.id.trim() || buildings.has(input.id)) throw new RangeError(`BIM building already exists: ${input.id}`); site(input.siteId); buildings.set(input.id, clone(input)); version += 1; return clone(input) }
  const addLevel = (input: BimLevel & { buildingId?: string }) => { if (!input.id.trim() || levels.has(input.id)) throw new RangeError(`BIM level already exists: ${input.id}`); finite(input.elevation, 'BIM level elevation'); if (input.buildingId) { building(input.buildingId); levelBuildings.set(input.id, input.buildingId) } levels.set(input.id, { id: input.id, label: input.label, elevation: input.elevation }); version += 1; return clone(levels.get(input.id)!) }
  const addSpace = (input: BimSpace) => { if (!input.id.trim() || spaces.has(input.id)) throw new RangeError(`BIM space already exists: ${input.id}`); level(input.levelId); if (input.area !== undefined) { finite(input.area, 'BIM space area'); if (input.area < 0) throw new RangeError('BIM space area cannot be negative.') } spaces.set(input.id, clone(input)); version += 1; return clone(input) }
  const addMaterial = (input: BimMaterial) => { if (!input.id.trim() || materials.has(input.id)) throw new RangeError(`BIM material already exists: ${input.id}`); if (!input.name.trim()) throw new RangeError('BIM material name is required.'); materials.set(input.id, clone(input)); version += 1; return clone(input) }
  const addElement = (input: BimElement) => { if (!input.id.trim() || elements.has(input.id)) throw new RangeError(`BIM element already exists: ${input.id}`); if (!input.shapeId.trim()) throw new RangeError('BIM element shapeId is required.'); level(input.levelId); if (input.spaceId) space(input.spaceId); if (input.materialId) material(input.materialId); for (const value of Object.values(input.quantity)) if (value !== undefined) { finite(value, 'BIM element quantity'); if (value < 0) throw new RangeError('BIM element quantities cannot be negative.') } elements.set(input.id, clone(input)); version += 1; return clone(input) }
  const setElementProperty = (elementId: string, name: string, value: string | number | boolean) => { if (!name.trim()) throw new RangeError('BIM property name is required.'); if (typeof value === 'number') finite(value, 'BIM property value'); const target = element(elementId); target.properties[name] = value; version += 1; return clone(target) }
  const assignMaterial = (elementId: string, materialId: string) => { material(materialId); const target = element(elementId); target.materialId = materialId; version += 1; return clone(target) }
  const setClassification = (elementId: string, classification: { system: string; code: string }) => { if (!classification.system.trim() || !classification.code.trim()) throw new RangeError('BIM classification system and code are required.'); const target = element(elementId); target.classification = { ...classification }; version += 1; return clone(target) }
  const schedule = () => [...elements.values()].map((entry) => ({ elementId: entry.id, type: entry.type, quantity: clone(entry.quantity) }))
  const exportIfc = (schema: BimIfcSchema = 'IFC4') => { if (schema !== 'IFC2X3' && schema !== 'IFC4') throw new RangeError(`Unsupported IFC schema: ${schema}`); const lines = ['ISO-10303-21;', 'HEADER;', `FILE_DESCRIPTION(('Bitbybit BIM'),'2;1');`, "FILE_NAME('model.ifc','2026-08-05',('Bitbybit'),('Bitbybit'),'','','');", `FILE_SCHEMA(('${schema}'));`, 'ENDSEC;', 'DATA;']; let entity = 1; for (const entry of sites.values()) lines.push(`#${entity++}=IFCSITE('${ifcEscape(entry.id)}','${ifcEscape(entry.label)}',$);`); for (const entry of buildings.values()) lines.push(`#${entity++}=IFCBUILDING('${ifcEscape(entry.id)}','${ifcEscape(entry.label)}',$);`); for (const entry of levels.values()) lines.push(`#${entity++}=IFCBUILDINGSTOREY('${ifcEscape(entry.id)}','${ifcEscape(entry.label)}',${entry.elevation});`); for (const entry of spaces.values()) lines.push(`#${entity++}=IFCSPACE('${ifcEscape(entry.id)}','${ifcEscape(entry.label)}',$);`); for (const entry of elements.values()) { lines.push(`#${entity++}=IFCBUILDINGELEMENTPROXY('${ifcEscape(entry.id)}','${ifcEscape(entry.label)}','${ifcEscape(entry.type)}');`); for (const [name, value] of Object.entries(entry.properties)) lines.push(`#${entity++}=IFCPROPERTYSET('${ifcEscape(entry.id)}.${ifcEscape(name)}','${ifcEscape(String(value))}');`); if (entry.classification) lines.push(`#${entity++}=IFCCLASSIFICATIONREFERENCE('${ifcEscape(entry.classification.system)}','${ifcEscape(entry.classification.code)}');`) } lines.push('ENDSEC;', 'END-ISO-10303-21;'); return `${lines.join('\n')}\n` }
  const importIfc = (serialized: string): BimIfcImport => { if (!serialized.startsWith('ISO-10303-21;') || !serialized.includes('END-ISO-10303-21;')) throw new RangeError('IFC archive boundaries are invalid.'); const schemaMatch = serialized.match(/FILE_SCHEMA\(\('\s*(IFC2X3|IFC4)\s*'\)\)/); if (!schemaMatch) throw new RangeError('IFC schema is missing or unsupported.'); return { schema: schemaMatch[1] as BimIfcSchema, entities: (serialized.match(/^#\d+=/gm) ?? []).length, hierarchy: (serialized.match(/=IFC(?:SITE|BUILDING|BUILDINGSTOREY|SPACE)\(/g) ?? []).length, propertySets: (serialized.match(/=IFCPROPERTYSET\(/g) ?? []).length, classifications: (serialized.match(/=IFCCLASSIFICATIONREFERENCE\(/g) ?? []).length } }
  return { snapshot, addSite, addBuilding, addLevel, addSpace, addMaterial, addElement, setElementProperty, assignMaterial, setClassification, schedule, exportIfc, importIfc }
}
