// // TankCatalog.swift // GasTankAR // // Created by Dennis Nemec on 14.09.26. // import Foundation /// Zugriff auf den Katalog verfügbarer Tanks. Die abgeleiteten Abfragen (welche /// Größen/Farben je Aufstellungsart verfügbar sind, Auflösung einer Auswahl auf /// eine konkrete Variante) leiten sich allein aus `definitions` ab und sind /// daher in einer Protokoll-Extension implementiert. nonisolated protocol TankCatalogProviding { var definitions: [TankDefinition] { get } /// Anzeigenamen je Größe (`TankSize.rawValue` → Anzeigename). var sizeDisplayNames: [String: String] { get } /// Anzeigenamen je Farbe (`TankColor.rawValue` → Anzeigename). var colorDisplayNames: [String: String] { get } } nonisolated extension TankCatalogProviding { /// Verfügbare Aufstellungsarten (in Reihenfolge des ersten Auftretens). var placements: [TankPlacement] { orderedUnique(definitions.map(\.placement)) } /// Für eine Aufstellungsart verfügbare Größen. func sizes(for placement: TankPlacement) -> [TankSize] { orderedUnique(definitions.filter { $0.placement == placement }.map(\.size)) } /// Für eine Größe & Aufstellungsart verfügbare Farben. func colors(for size: TankSize, placement: TankPlacement) -> [TankColor] { orderedUnique( definitions .filter { $0.size == size && $0.placement == placement } .map(\.color) ) } /// Exakte Definition zur getroffenen Auswahl (oder `nil`, falls die /// Kombination nicht existiert). func definition(for tank: Tank) -> TankDefinition? { definitions.first { $0.matches(tank) } } /// Definition zur Auswahl – korrigiert inkonsistente Kombinationen (z. B. aus /// Altdaten) zuvor auf eine gültige Variante. Gibt `nil` zurück, wenn der /// Katalog leer ist. func resolvedDefinition(for tank: Tank) -> TankDefinition? { definition(for: tank) ?? definition(for: normalized(tank)) } /// Korrigiert eine Auswahl so, dass sie einer existierenden Definition /// entspricht: passt zuerst die Farbe an, danach – falls nötig – die Größe. /// Existiert bereits eine passende Definition, bleibt die Auswahl unverändert. func normalized(_ tank: Tank) -> Tank { guard definition(for: tank) == nil else { return tank } var result = tank // Farbe an Größe & Aufstellungsart anpassen. let colors = colors(for: result.size, placement: result.placement) if let color = colors.first, !colors.contains(result.color) { result.color = color } if definition(for: result) != nil { return result } // Größe (und daraus folgend Farbe) an die Aufstellungsart anpassen. if let size = sizes(for: result.placement).first { result.size = size if let color = self.colors(for: result.size, placement: result.placement).first { result.color = color } } return result } /// Anzeigename einer Größe (aus dem Mapping; Fallback: `rawValue`). func displayName(for size: TankSize) -> String { sizeDisplayNames[size.rawValue] ?? size.rawValue } /// Anzeigename einer Farbe (aus dem Mapping; Fallback: `rawValue`). func displayName(for color: TankColor) -> String { colorDisplayNames[color.rawValue] ?? color.rawValue } /// Reihenfolge-erhaltende Deduplizierung. private func orderedUnique(_ values: [T]) -> [T] { var seen = Set() return values.filter { seen.insert($0).inserted } } } /// Konkreter, unveränderlicher Katalog verfügbarer Tanks. nonisolated struct TankCatalog: TankCatalogProviding { let definitions: [TankDefinition] let sizeDisplayNames: [String: String] let colorDisplayNames: [String: String] init( definitions: [TankDefinition], sizeDisplayNames: [String: String] = [:], colorDisplayNames: [String: String] = [:] ) { self.definitions = definitions self.sizeDisplayNames = sizeDisplayNames self.colorDisplayNames = colorDisplayNames } /// Erstellt einen Katalog aus geladenen Rohdaten. init(file: TankCatalogFile) { self.init( definitions: file.tanks, sizeDisplayNames: file.sizeDisplayNames, colorDisplayNames: file.colorDisplayNames ) } /// Erstellt einen Katalog aus einem Loader. Schlägt das Laden fehl, entsteht /// ein leerer Katalog (die App bleibt bedienbar, zeigt aber keine Tanks an). init(loader: TankCatalogLoading) { if let file = try? loader.load() { self.init(file: file) } else { self.init(definitions: []) } } } nonisolated extension TankCatalog { /// Gemeinsamer, aus dem App-Bundle geladener Katalog für den Produktivbetrieb. static let shared = TankCatalog(loader: BundleTankCatalogLoader()) }