//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import AVFoundation
import SignalServiceKit
import SignalUI

/// Represents a voice note that's actively being recorded.
///
/// In most cases you'll immediately send (or discard) voice notes you
/// record. When this happens, an instance of this type will be passed to
/// the voice note sending logic.
///
/// In some cases, an external event may interrupt an active recording. When
/// that happens, we convert this object into a durable
/// ``VoiceMessageInterruptedDraft``. That draft is visible in the compose
/// box for the user to return to later.
final class VoiceMessageInProgressDraft: VoiceMessageSendableDraft {
    private let threadUniqueId: String
    private let audioFileUrl: URL
    private let audioActivity: AudioActivity
    private let audioSession: AudioSession
    private let sleepManager: DeviceSleepManager

    init(thread: TSThread, audioSession: AudioSession, sleepManager: DeviceSleepManager) {
        self.threadUniqueId = thread.uniqueId
        self.audioFileUrl = OWSFileSystem.temporaryFileUrl(fileExtension: "m4a")
        self.audioActivity = AudioActivity(audioDescription: "Voice Message Recording", behavior: .playAndRecord)
        self.audioSession = audioSession
        self.sleepManager = sleepManager
    }

    private var audioRecorder: AVAudioRecorder? {
        didSet {
            guard oldValue !== audioRecorder else { return }
            if let oldValue {
                sleepManager.removeBlock(blockObject: oldValue)
            }
            if let audioRecorder {
                sleepManager.addBlock(blockObject: audioRecorder)
            }
        }
    }

    var isRecording: Bool { audioRecorder?.isRecording ?? false }

    func startRecording() throws {
        AssertIsOnMainThread()

        guard !isRecording else {
            throw OWSAssertionError("Attempted to start recording while recording is in progress")
        }

        guard audioSession.startAudioActivity(audioActivity) else {
            throw OWSAssertionError("Couldn't configure audio session")
        }

        let audioRecorder: AVAudioRecorder
        do {
            audioRecorder = try AVAudioRecorder(
                url: audioFileUrl,
                settings: [
                    AVFormatIDKey: kAudioFormatMPEG4AAC,
                    AVSampleRateKey: 44100,
                    AVNumberOfChannelsKey: 1,
                    AVEncoderBitRateKey: 32000
                ]
            )
            self.audioRecorder = audioRecorder
        } catch {
            throw OWSAssertionError("Couldn't create audioRecorder: \(error)")
        }

        audioRecorder.isMeteringEnabled = true

        guard audioRecorder.prepareToRecord() else {
            throw OWSAssertionError("audioRecorder couldn't prepareToRecord.")
        }

        guard audioRecorder.record() else {
            throw OWSAssertionError("audioRecorder couldn't record.")
        }
    }

    func stopRecording() {
        AssertIsOnMainThread()

        guard let audioRecorder = audioRecorder else { return }
        self.audioRecorder = nil

        self.duration = audioRecorder.currentTime

        audioRecorder.stop()

        // This is expensive. We can safely do it in the background.
        DispatchQueue.sharedUserInteractive.async {
            self.audioSession.endAudioActivity(self.audioActivity)
        }
    }

    func stopRecordingAsync() {
        AssertIsOnMainThread()

        guard let audioRecorder = audioRecorder else { return }
        self.audioRecorder = nil

        self.duration = audioRecorder.currentTime

        // This is expensive. We can safely do it in the background
        // if we're not relying on the recorded audio (e.g. we canceled)
        DispatchQueue.sharedUserInteractive.async {
            audioRecorder.stop()
            self.audioSession.endAudioActivity(self.audioActivity)
        }
    }

    private(set) var duration: TimeInterval?

    func convertToDraft(transaction: SDSAnyWriteTransaction) -> VoiceMessageInterruptedDraft {
        let directoryUrl = VoiceMessageInterruptedDraftStore.saveDraft(
            audioFileUrl: audioFileUrl,
            threadUniqueId: threadUniqueId,
            transaction: transaction
        )
        return VoiceMessageInterruptedDraft(threadUniqueId: threadUniqueId, directoryUrl: directoryUrl)
    }

    func prepareForSending() throws -> URL {
        return audioFileUrl
    }
}
