124 lines
3.7 KiB
Swift
124 lines
3.7 KiB
Swift
// ── Pre-Warning Cascade System ─────────────────────────────────
|
|
// Cascade presets aligned with PRD: Aggressive, Standard, Light, Minimal, None, Custom
|
|
// Ported from web/src/lib/cascade.ts
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Cascade Preset
|
|
|
|
enum CascadePreset: String, Codable, CaseIterable, Identifiable {
|
|
case aggressive
|
|
case standard
|
|
case light
|
|
case minimal
|
|
case none
|
|
case custom
|
|
|
|
var id: String { rawValue }
|
|
|
|
var label: String {
|
|
switch self {
|
|
case .aggressive: return "Aggressive"
|
|
case .standard: return "Standard"
|
|
case .light: return "Light"
|
|
case .minimal: return "Minimal"
|
|
case .none: return "None (fire only)"
|
|
case .custom: return "Custom"
|
|
}
|
|
}
|
|
|
|
/// Default intervals in minutes before target time
|
|
var defaultIntervals: [Int] {
|
|
switch self {
|
|
case .aggressive: return [240, 180, 120, 90, 60, 30, 15, 5, 1]
|
|
case .standard: return [120, 60, 30, 15, 5]
|
|
case .light: return [60, 15, 5]
|
|
case .minimal: return [15]
|
|
case .none: return []
|
|
case .custom: return []
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Cascade Warning
|
|
|
|
struct CascadeWarning: Codable, Identifiable, Equatable {
|
|
let id: String
|
|
let minutesBefore: Int
|
|
var fired: Bool
|
|
var firedAt: Date?
|
|
let scheduledTime: Date
|
|
|
|
static func == (lhs: CascadeWarning, rhs: CascadeWarning) -> Bool {
|
|
lhs.id == rhs.id
|
|
}
|
|
}
|
|
|
|
// MARK: - Cascade Config
|
|
|
|
struct CascadeConfig: Codable {
|
|
let preset: CascadePreset
|
|
let intervals: [Int] // minutes before target time (for custom preset)
|
|
}
|
|
|
|
// MARK: - Cascade Functions
|
|
|
|
/// Calculate all warning timestamps from target time and cascade intervals.
|
|
/// Filters out warnings that would be in the past relative to `now`.
|
|
func calculateCascadeWarnings(
|
|
targetTime: Date,
|
|
intervals: [Int],
|
|
now: Date = Date()
|
|
) -> [CascadeWarning] {
|
|
let sorted = intervals.sorted(by: >) // largest first (earliest warning)
|
|
return sorted.enumerated().map { idx, minutesBefore in
|
|
let scheduledTime = targetTime.addingTimeInterval(-Double(minutesBefore) * 60.0)
|
|
return CascadeWarning(
|
|
id: "w-\(idx)-\(minutesBefore)m",
|
|
minutesBefore: minutesBefore,
|
|
fired: scheduledTime <= now,
|
|
firedAt: scheduledTime <= now ? scheduledTime : nil,
|
|
scheduledTime: scheduledTime
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Get the next unfired warning from a cascade.
|
|
func getNextWarning(_ warnings: [CascadeWarning]) -> CascadeWarning? {
|
|
warnings.first(where: { !$0.fired })
|
|
}
|
|
|
|
/// Check which warnings should fire given the current time.
|
|
/// Returns newly-fired warning IDs and mutates the warnings array.
|
|
@discardableResult
|
|
func checkWarnings(_ warnings: inout [CascadeWarning], now: Date = Date()) -> [String] {
|
|
var newlyFired: [String] = []
|
|
for i in warnings.indices {
|
|
if !warnings[i].fired && warnings[i].scheduledTime <= now {
|
|
warnings[i].fired = true
|
|
warnings[i].firedAt = now
|
|
newlyFired.append(warnings[i].id)
|
|
}
|
|
}
|
|
return newlyFired
|
|
}
|
|
|
|
/// Get intervals for a preset, or custom intervals if preset is 'custom'.
|
|
func getCascadeIntervals(_ config: CascadeConfig) -> [Int] {
|
|
if config.preset == .custom {
|
|
return config.intervals.sorted(by: >)
|
|
}
|
|
return config.preset.defaultIntervals
|
|
}
|
|
|
|
/// Format minutes into human-readable string.
|
|
func formatMinutesBefore(_ minutes: Int) -> String {
|
|
if minutes >= 60 {
|
|
let hours = minutes / 60
|
|
let remaining = minutes % 60
|
|
if remaining == 0 { return "\(hours)h" }
|
|
return "\(hours)h \(remaining)m"
|
|
}
|
|
return "\(minutes)m"
|
|
}
|