// Keeps the local display awake for the duration of a streaming session.
//
// A stream is not "user activity" to the OS: the pixels arrive over the network and the input that
// drives them is often a game controller, which does NOT feed the HID idle timer on any Apple
// platform. So a controller-only session reliably idles the panel out from under the user — the
// same reason the Android client holds FLAG_KEEP_SCREEN_ON while streaming (StreamScreen.kt).
//
// Held by SessionModel from `beginStreaming` to `disconnect`, so it is scoped to the session and
// never leaks past it (including a host-ended or timed-out background session, which both land in
// `disconnect`).

import Foundation

#if os(macOS)
    import IOKit.pwr_mgt
#else
    import UIKit
#endif

@MainActor
final class DisplaySleepGuard {
    #if os(macOS)
        /// The `beginActivity` token; non-nil exactly while held.
        private var activity: NSObjectProtocol?
        /// Re-used across heartbeats so the whole session shares one assertion instead of
        /// accumulating one per tick.
        private var userActivityAssertion: IOPMAssertionID = IOPMAssertionID(0)
        private var heartbeat: Timer?

        /// The power assertion defers DISPLAY SLEEP but not the screen saver — that runs off the
        /// HID idle timer, which a controller-only session never touches. Declaring user activity
        /// on an interval well under the shortest selectable screen-saver delay (1 minute) keeps
        /// that timer from ever reaching it. Side effect, and the intended one: an idle-lock
        /// configured to follow the screen saver is deferred too, for the session only.
        private static let heartbeatInterval: TimeInterval = 30
    #endif

    private(set) var isHeld = false

    /// Idempotent — a second acquire while held is a no-op.
    func acquire() {
        guard !isHeld else { return }
        isHeld = true
        #if os(macOS)
            // The high-level Foundation API over IOKit power assertions: `.idleDisplaySleepDisabled`
            // is the panel, `.userInitiated` also holds off idle SYSTEM sleep and sudden termination
            // for a session the user is watching in real time.
            activity = ProcessInfo.processInfo.beginActivity(
                options: [.userInitiated, .idleDisplaySleepDisabled],
                reason: "Punktfunk streaming session")
            declareUserActivity()
            let timer = Timer.scheduledTimer(withTimeInterval: Self.heartbeatInterval, repeats: true) {
                [weak self] _ in
                MainActor.assumeIsolated { self?.declareUserActivity() }
            }
            // The stream runs under a tracking run-loop mode while a menu or a window resize is up;
            // .common keeps the heartbeat ticking through those.
            RunLoop.main.add(timer, forMode: .common)
            heartbeat = timer
        #else
            // iOS/iPadOS/tvOS: app-wide, and ignored while backgrounded — the background keep-alive
            // (audio-only, video dropped) correctly lets the device sleep without touching this.
            UIApplication.shared.isIdleTimerDisabled = true
        #endif
    }

    /// Idempotent — safe to call when not held (`disconnect` runs on paths that never streamed).
    func release() {
        guard isHeld else { return }
        isHeld = false
        #if os(macOS)
            heartbeat?.invalidate()
            heartbeat = nil
            if let activity {
                ProcessInfo.processInfo.endActivity(activity)
                self.activity = nil
            }
            if userActivityAssertion != IOPMAssertionID(0) {
                IOPMAssertionRelease(userActivityAssertion)
                userActivityAssertion = IOPMAssertionID(0)
            }
        #else
            UIApplication.shared.isIdleTimerDisabled = false
        #endif
    }

    #if os(macOS)
        /// Resets the HID idle timer (see `heartbeatInterval`). `kIOPMUserActiveLocal` = activity at
        /// this Mac's own display, which is what a stream being watched here is.
        private func declareUserActivity() {
            IOPMAssertionDeclareUserActivity(
                "Punktfunk streaming session" as CFString,
                kIOPMUserActiveLocal,
                &userActivityAssertion)
        }
    #endif
}
