// ── Timer Engine ─────────────────────────────────────────────── // Core timer types, state machine, and lifecycle management // Ported from web/src/lib/timer-engine.ts import Foundation // MARK: - Timer Types enum CMTimerType: String, Codable, CaseIterable, Identifiable { case alarm case countdown case pomodoro case event var id: String { rawValue } var label: String { switch self { case .alarm: return "Alarm" case .countdown: return "Countdown" case .pomodoro: return "Pomodoro" case .event: return "Event" } } } // MARK: - Timer State enum CMTimerState: String, Codable { case idle case active case warning case firing case snoozed case dismissed case completed case paused } // MARK: - Pomodoro Config struct PomodoroConfig: Codable, Equatable { var workMinutes: Int var breakMinutes: Int var longBreakMinutes: Int var rounds: Int static let `default` = PomodoroConfig( workMinutes: 25, breakMinutes: 5, longBreakMinutes: 15, rounds: 4 ) } // MARK: - Pomodoro State struct PomodoroState: Codable { var currentRound: Int var isBreak: Bool var isLongBreak: Bool var completedRounds: Int } // MARK: - Timer Model struct CMTimer: Codable, Identifiable, Equatable { let id: String let type: CMTimerType var label: String var description: String? var urgency: UrgencyLevel var state: CMTimerState // Time fields var targetTime: Date // when the timer fires var duration: TimeInterval? // seconds — for countdowns/pomodoro let createdAt: Date var startedAt: Date? var pausedAt: Date? var firedAt: Date? var dismissedAt: Date? var completedAt: Date? // Elapsed tracking for pause/resume var elapsedBeforePause: TimeInterval // seconds accumulated before last pause // Cascade var cascade: CascadeConfig var warnings: [CascadeWarning] // Pomodoro-specific var pomodoroConfig: PomodoroConfig? var pomodoroState: PomodoroState? // Snooze var snoozeCount: Int var snoozedUntil: Date? // Metadata var category: String? var tags: [String]? var linkedTimerId: String? static func == (lhs: CMTimer, rhs: CMTimer) -> Bool { lhs.id == rhs.id } } // MARK: - Factory Functions struct CreateAlarmParams { let label: String let targetTime: Date var urgency: UrgencyLevel = .standard var cascade: CascadeConfig? = nil var category: String? = nil var description: String? = nil } func createAlarm(_ params: CreateAlarmParams) -> CMTimer { let cascade = params.cascade ?? CascadeConfig(preset: .standard, intervals: []) let intervals = getCascadeIntervals(cascade) let now = Date() return CMTimer( id: UUID().uuidString, type: .alarm, label: params.label, description: params.description, urgency: params.urgency, state: .active, targetTime: params.targetTime, duration: params.targetTime.timeIntervalSince(now), createdAt: now, startedAt: now, pausedAt: nil, firedAt: nil, dismissedAt: nil, completedAt: nil, elapsedBeforePause: 0, cascade: cascade, warnings: calculateCascadeWarnings(targetTime: params.targetTime, intervals: intervals, now: now), pomodoroConfig: nil, pomodoroState: nil, snoozeCount: 0, snoozedUntil: nil, category: params.category, tags: nil, linkedTimerId: nil ) } struct CreateCountdownParams { let label: String let durationSeconds: TimeInterval var urgency: UrgencyLevel = .standard var cascade: CascadeConfig? = nil var category: String? = nil var description: String? = nil } func createCountdown(_ params: CreateCountdownParams) -> CMTimer { let now = Date() let targetTime = now.addingTimeInterval(params.durationSeconds) let cascade = params.cascade ?? CascadeConfig(preset: .standard, intervals: []) let intervals = getCascadeIntervals(cascade) return CMTimer( id: UUID().uuidString, type: .countdown, label: params.label, description: params.description, urgency: params.urgency, state: .active, targetTime: targetTime, duration: params.durationSeconds, createdAt: now, startedAt: now, pausedAt: nil, firedAt: nil, dismissedAt: nil, completedAt: nil, elapsedBeforePause: 0, cascade: cascade, warnings: calculateCascadeWarnings(targetTime: targetTime, intervals: intervals, now: now), pomodoroConfig: nil, pomodoroState: nil, snoozeCount: 0, snoozedUntil: nil, category: params.category, tags: nil, linkedTimerId: nil ) } struct CreatePomodoroParams { var label: String = "Focus Session" var config: PomodoroConfig = .default var urgency: UrgencyLevel = .standard } func createPomodoro(_ params: CreatePomodoroParams = CreatePomodoroParams()) -> CMTimer { let now = Date() let durationSeconds = TimeInterval(params.config.workMinutes * 60) let targetTime = now.addingTimeInterval(durationSeconds) return CMTimer( id: UUID().uuidString, type: .pomodoro, label: params.label, description: nil, urgency: params.urgency, state: .active, targetTime: targetTime, duration: durationSeconds, createdAt: now, startedAt: now, pausedAt: nil, firedAt: nil, dismissedAt: nil, completedAt: nil, elapsedBeforePause: 0, cascade: CascadeConfig(preset: .minimal, intervals: []), warnings: calculateCascadeWarnings(targetTime: targetTime, intervals: [1], now: now), pomodoroConfig: params.config, pomodoroState: PomodoroState( currentRound: 1, isBreak: false, isLongBreak: false, completedRounds: 0 ), snoozeCount: 0, snoozedUntil: nil, category: nil, tags: nil, linkedTimerId: nil ) } // MARK: - State Transitions func pauseTimer(_ timer: CMTimer) -> CMTimer { guard timer.state == .active || timer.state == .warning else { return timer } var t = timer let now = Date() let elapsed = t.elapsedBeforePause + now.timeIntervalSince(t.startedAt ?? now) t.state = .paused t.pausedAt = now t.elapsedBeforePause = elapsed return t } func resumeTimer(_ timer: CMTimer) -> CMTimer { guard timer.state == .paused else { return timer } var t = timer let now = Date() let remainingSeconds = (t.duration ?? 0) - t.elapsedBeforePause let newTargetTime = now.addingTimeInterval(remainingSeconds) let intervals = getCascadeIntervals(t.cascade) t.state = .active t.startedAt = now t.pausedAt = nil t.targetTime = newTargetTime t.warnings = calculateCascadeWarnings(targetTime: newTargetTime, intervals: intervals, now: now) return t } func fireTimer(_ timer: CMTimer) -> CMTimer { guard timer.state != .dismissed && timer.state != .completed else { return timer } var t = timer t.state = .firing t.firedAt = Date() return t } func snoozeTimer(_ timer: CMTimer, snoozeMinutes: Int) -> CMTimer { guard timer.state == .firing else { return timer } var t = timer let now = Date() let snoozeUntil = now.addingTimeInterval(TimeInterval(snoozeMinutes * 60)) let intervals = getCascadeIntervals(t.cascade).filter { $0 <= snoozeMinutes } t.state = .snoozed t.targetTime = snoozeUntil t.snoozedUntil = snoozeUntil t.snoozeCount += 1 t.warnings = calculateCascadeWarnings(targetTime: snoozeUntil, intervals: intervals, now: now) return t } func dismissTimer(_ timer: CMTimer) -> CMTimer { var t = timer t.state = .dismissed t.dismissedAt = Date() return t } func completeTimer(_ timer: CMTimer) -> CMTimer { var t = timer t.state = .completed t.completedAt = Date() return t } // MARK: - Pomodoro Transitions func advancePomodoro(_ timer: CMTimer) -> CMTimer? { guard timer.type == .pomodoro, let config = timer.pomodoroConfig, let state = timer.pomodoroState else { return nil } var t = timer let now = Date() if state.isBreak || state.isLongBreak { // Long break finished → all done if state.isLongBreak { return completeTimer(t) } // Short break finished → start next work round let nextRound = state.currentRound + 1 if nextRound > config.rounds { return completeTimer(t) } let durationSeconds = TimeInterval(config.workMinutes * 60) t.state = .active t.targetTime = now.addingTimeInterval(durationSeconds) t.duration = durationSeconds t.startedAt = now t.firedAt = nil t.elapsedBeforePause = 0 t.warnings = calculateCascadeWarnings(targetTime: t.targetTime, intervals: [1], now: now) t.pomodoroState = PomodoroState( currentRound: nextRound, isBreak: false, isLongBreak: false, completedRounds: state.completedRounds ) return t } else { // Work finished → start break let completedRounds = state.completedRounds + 1 let isLongBreak = completedRounds >= config.rounds if isLongBreak { // All rounds done, long break let durationSeconds = TimeInterval(config.longBreakMinutes * 60) t.state = .active t.targetTime = now.addingTimeInterval(durationSeconds) t.duration = durationSeconds t.startedAt = now t.firedAt = nil t.elapsedBeforePause = 0 t.warnings = [] t.pomodoroState = PomodoroState( currentRound: state.currentRound, isBreak: false, isLongBreak: true, completedRounds: completedRounds ) return t } let durationSeconds = TimeInterval(config.breakMinutes * 60) t.state = .active t.targetTime = now.addingTimeInterval(durationSeconds) t.duration = durationSeconds t.startedAt = now t.firedAt = nil t.elapsedBeforePause = 0 t.warnings = [] t.pomodoroState = PomodoroState( currentRound: state.currentRound, isBreak: true, isLongBreak: false, completedRounds: completedRounds ) return t } } // MARK: - Utility func getRemainingSeconds(_ timer: CMTimer, now: Date = Date()) -> TimeInterval { if timer.state == .paused { return (timer.duration ?? 0) - timer.elapsedBeforePause } return max(0, timer.targetTime.timeIntervalSince(now)) } func isTimerActive(_ timer: CMTimer) -> Bool { [.active, .warning, .snoozed].contains(timer.state) } func shouldTimerFire(_ timer: CMTimer, now: Date = Date()) -> Bool { if timer.state == .snoozed, let snoozedUntil = timer.snoozedUntil, now >= snoozedUntil { return true } if (timer.state == .active || timer.state == .warning) && now >= timer.targetTime { return true } return false }