// // TrefleMapper.swift // PlantGuide // // Created on 2026-01-21. // import Foundation // MARK: - TrefleMapper /// Maps Trefle API DTOs to domain entities. /// /// This mapper provides conversion functions for transforming Trefle API /// responses into PlantCareInfo domain entities and related care requirement types. /// All mapping functions handle nil/missing data gracefully with sensible defaults. struct TrefleMapper { // MARK: - Primary Mapping /// Maps a Trefle species DTO to a PlantCareInfo domain entity. /// /// This function extracts all available care information from the Trefle species data, /// including growth requirements, environmental preferences, and blooming seasons. /// /// - Parameter species: The detailed species information from the Trefle API. /// - Returns: A `PlantCareInfo` domain entity populated with care requirements. static func mapToPlantCareInfo(from species: TrefleSpeciesDTO) -> PlantCareInfo { let growth = species.growth let specifications = species.specifications return PlantCareInfo( id: UUID(), scientificName: species.scientificName, commonName: species.commonName, lightRequirement: mapToLightRequirement(from: growth?.light), wateringSchedule: mapToWateringSchedule(from: growth), temperatureRange: mapToTemperatureRange(from: growth), fertilizerSchedule: mapToFertilizerSchedule(from: growth), humidity: mapToHumidityLevel(from: growth?.atmosphericHumidity), growthRate: mapToGrowthRate(from: specifications?.growthRate), bloomingSeason: mapToBloomingSeason(from: growth?.bloomMonths), additionalNotes: buildAdditionalNotes(from: species), sourceURL: URL(string: "https://trefle.io/api/v1/species/\(species.id)"), trefleID: species.id ) } // MARK: - Light Requirement Mapping /// Maps a Trefle light scale value to a LightRequirement enum. /// /// Trefle uses a 0-10 scale where 0 is full shade and 10 is full sun. /// This function maps that scale to the app's LightRequirement categories. /// /// - Parameter light: The Trefle light value (0-10 scale), or nil if not available. /// - Returns: The corresponding `LightRequirement` enum value. /// Defaults to `.partialShade` if the input is nil. /// /// Mapping: /// - 0-2: `.fullShade` - Very low light conditions /// - 3-4: `.lowLight` - Low light but not complete shade /// - 5-6: `.partialShade` - Moderate, indirect light /// - 7-10: `.fullSun` - Direct sunlight for most of the day static func mapToLightRequirement(from light: Int?) -> LightRequirement { guard let light = light else { return .partialShade } switch light { case 0...2: return .fullShade case 3...4: return .lowLight case 5...6: return .partialShade case 7...10: return .fullSun default: return .partialShade } } // MARK: - Watering Schedule Mapping /// Maps Trefle growth data to a WateringSchedule. /// /// This function determines watering frequency and amount based on the plant's /// atmospheric and soil humidity requirements. Plants with higher humidity needs /// typically require more frequent but lighter watering, while those with lower /// needs benefit from less frequent but thorough watering. /// /// - Parameter growth: The Trefle growth data containing humidity requirements. /// - Returns: A `WateringSchedule` with appropriate frequency and amount. /// Defaults to weekly/moderate if growth data is nil. /// /// Mapping based on humidity levels (0-10 scale): /// - High humidity (7-10): Weekly frequency with light watering /// - Medium humidity (4-6): Twice weekly with moderate watering /// - Low humidity (0-3): Weekly with thorough watering static func mapToWateringSchedule(from growth: TrefleGrowthDTO?) -> WateringSchedule { guard let growth = growth else { return WateringSchedule(frequency: .weekly, amount: .moderate) } // Use atmospheric humidity as primary indicator, fall back to soil humidity let humidityLevel = growth.atmosphericHumidity ?? growth.soilHumidity guard let humidity = humidityLevel else { return WateringSchedule(frequency: .weekly, amount: .moderate) } switch humidity { case 7...10: // High humidity plants need frequent, light watering return WateringSchedule(frequency: .weekly, amount: .light) case 4...6: // Medium humidity plants need regular, moderate watering return WateringSchedule(frequency: .twiceWeekly, amount: .moderate) case 0...3: // Low humidity plants (often drought-tolerant) need less frequent but thorough watering return WateringSchedule(frequency: .weekly, amount: .thorough) default: return WateringSchedule(frequency: .weekly, amount: .moderate) } } // MARK: - Temperature Range Mapping /// Maps Trefle growth data to a TemperatureRange. /// /// This function extracts minimum and maximum temperature tolerances from the /// Trefle growth data and determines frost tolerance based on whether the plant /// can survive temperatures below 0 degrees Celsius. /// /// - Parameter growth: The Trefle growth data containing temperature information. /// - Returns: A `TemperatureRange` with min/max values and frost tolerance. /// Defaults to 15-30 degrees Celsius if growth data is nil. static func mapToTemperatureRange(from growth: TrefleGrowthDTO?) -> TemperatureRange { guard let growth = growth else { return TemperatureRange( minimumCelsius: 15.0, maximumCelsius: 30.0, optimalCelsius: nil, frostTolerant: false ) } let minTemp = growth.minimumTemperature?.degC ?? 15.0 let maxTemp = growth.maximumTemperature?.degC ?? 30.0 // Calculate optimal as midpoint between min and max if both are available let optimalTemp: Double? if growth.minimumTemperature?.degC != nil && growth.maximumTemperature?.degC != nil { optimalTemp = (minTemp + maxTemp) / 2.0 } else { optimalTemp = nil } // Plant is frost tolerant if it can survive temperatures below 0 degrees Celsius let frostTolerant = minTemp < 0.0 return TemperatureRange( minimumCelsius: minTemp, maximumCelsius: maxTemp, optimalCelsius: optimalTemp, frostTolerant: frostTolerant ) } // MARK: - Fertilizer Schedule Mapping /// Maps Trefle growth data to a FertilizerSchedule. /// /// This function determines fertilizer frequency and type based on the plant's /// soil nutrient requirements. Plants with higher nutrient needs require more /// frequent fertilization, while those with lower needs can use organic fertilizers /// applied less frequently. /// /// - Parameter growth: The Trefle growth data containing soil nutrient requirements. /// - Returns: A `FertilizerSchedule` with appropriate frequency and type, /// or nil if soil nutrient data is not available. /// /// Mapping based on soil nutriment levels (0-10 scale): /// - High needs (7-10): Biweekly with balanced fertilizer /// - Medium needs (4-6): Monthly with balanced fertilizer /// - Low needs (0-3): Quarterly with organic fertilizer static func mapToFertilizerSchedule(from growth: TrefleGrowthDTO?) -> FertilizerSchedule? { guard let soilNutriments = growth?.soilNutriments else { return nil } switch soilNutriments { case 7...10: // High nutrient needs - frequent balanced fertilization return FertilizerSchedule(frequency: .biweekly, type: .balanced) case 4...6: // Medium nutrient needs - monthly balanced fertilization return FertilizerSchedule(frequency: .monthly, type: .balanced) case 0...3: // Low nutrient needs - occasional organic fertilization return FertilizerSchedule(frequency: .quarterly, type: .organic) default: return FertilizerSchedule(frequency: .monthly, type: .balanced) } } // MARK: - Humidity Level Mapping /// Maps a Trefle atmospheric humidity value to a HumidityLevel enum. /// /// Trefle uses a 0-10 scale for atmospheric humidity requirements. /// This function maps that scale to the app's HumidityLevel categories. /// /// - Parameter humidity: The Trefle atmospheric humidity value (0-10 scale), /// or nil if not available. /// - Returns: The corresponding `HumidityLevel` enum value, or nil if input is nil. /// /// Mapping: /// - 0-2: `.low` - Below 30% humidity /// - 3-5: `.moderate` - 30-50% humidity /// - 6-8: `.high` - 50-70% humidity /// - 9-10: `.veryHigh` - Above 70% humidity static func mapToHumidityLevel(from humidity: Int?) -> HumidityLevel? { guard let humidity = humidity else { return nil } switch humidity { case 0...2: return .low case 3...5: return .moderate case 6...8: return .high case 9...10: return .veryHigh default: return .moderate } } // MARK: - Growth Rate Mapping /// Maps a Trefle growth rate string to a GrowthRate enum. /// /// Trefle provides growth rate as a string (e.g., "slow", "moderate", "rapid"). /// This function maps those values to the app's GrowthRate enum. /// /// - Parameter growthRate: The Trefle growth rate string, or nil if not available. /// - Returns: The corresponding `GrowthRate` enum value, or nil if input is nil /// or doesn't match a known value. static func mapToGrowthRate(from growthRate: String?) -> GrowthRate? { guard let growthRate = growthRate?.lowercased() else { return nil } switch growthRate { case "slow": return .slow case "moderate", "medium": return .moderate case "rapid", "fast": return .fast default: return nil } } // MARK: - Blooming Season Mapping /// Maps Trefle bloom months to an array of Season values. /// /// Trefle provides bloom months as an array of three-letter month abbreviations /// (e.g., ["mar", "apr", "may"]). This function converts those months to the /// corresponding seasons based on Northern Hemisphere conventions. /// /// - Parameter bloomMonths: An array of month abbreviations, or nil if not available. /// - Returns: An array of unique `Season` values when the plant blooms, /// or nil if bloom month data is not available. /// /// Month to Season Mapping (Northern Hemisphere): /// - December, January, February: `.winter` /// - March, April, May: `.spring` /// - June, July, August: `.summer` /// - September, October, November: `.fall` static func mapToBloomingSeason(from bloomMonths: [String]?) -> [Season]? { guard let bloomMonths = bloomMonths, !bloomMonths.isEmpty else { return nil } var seasons = Set() for month in bloomMonths { if let season = monthToSeason(month.lowercased()) { seasons.insert(season) } } guard !seasons.isEmpty else { return nil } // Return seasons in natural order: spring, summer, fall, winter let orderedSeasons: [Season] = [.spring, .summer, .fall, .winter] return orderedSeasons.filter { seasons.contains($0) } } // MARK: - Private Helpers /// Converts a month abbreviation to its corresponding season. /// /// - Parameter month: A three-letter month abbreviation (lowercase). /// - Returns: The corresponding `Season`, or nil if the abbreviation is not recognized. private static func monthToSeason(_ month: String) -> Season? { switch month { case "dec", "jan", "feb": return .winter case "mar", "apr", "may": return .spring case "jun", "jul", "aug": return .summer case "sep", "oct", "nov": return .fall default: return nil } } /// Builds additional care notes from species data. /// /// This function compiles relevant information that doesn't fit into other /// structured fields, such as pH requirements and toxicity warnings. /// /// - Parameter species: The Trefle species DTO. /// - Returns: A string containing additional care notes, or nil if no relevant data. private static func buildAdditionalNotes(from species: TrefleSpeciesDTO) -> String? { var notes: [String] = [] // Add pH range information if available if let phMin = species.growth?.phMinimum, let phMax = species.growth?.phMaximum { notes.append("Soil pH: \(String(format: "%.1f", phMin)) - \(String(format: "%.1f", phMax))") } else if let phMin = species.growth?.phMinimum { notes.append("Minimum soil pH: \(String(format: "%.1f", phMin))") } else if let phMax = species.growth?.phMaximum { notes.append("Maximum soil pH: \(String(format: "%.1f", phMax))") } // Add toxicity warning if available if let toxicity = species.specifications?.toxicity?.lowercased(), toxicity != "none" { notes.append("Toxicity: \(toxicity.capitalized)") } // Add family information for reference if let family = species.family { notes.append("Family: \(family)") } return notes.isEmpty ? nil : notes.joined(separator: ". ") } }