import Foundation

public struct CaptureFrameMetaData: Equatable {
    public enum CapturePermissionState {
        case requesting
        case error
        case notPermitted
        case permitted
    }

    public let deltaTime: TimeInterval
    public let captureRecordingState: CaptureRecordingState
    public let audioPermissionsState: CaptureCoordinator.AudioPermissionsState
    public let videoPermissionsState: CaptureCoordinator.VideoPermissionsState

    public var debugDescription: String {
        let formattedDeltaTime = String(format: "%.5f", deltaTime)
        return """
        Frame MetaData:
        Delta Time: \(formattedDeltaTime)s"
        Recording State: \(captureRecordingState)"
        """
    }

    public init(
        deltaTime: TimeInterval = .zero,
        captureRecordingState: CaptureRecordingState = .needsSetup,
        audioPermissionsState: CaptureCoordinator.AudioPermissionsState = .requesting,
        videoPermissionsState: CaptureCoordinator.VideoPermissionsState = .requesting
    ) {
        self.deltaTime = deltaTime
        self.captureRecordingState = captureRecordingState
        self.audioPermissionsState = audioPermissionsState
        self.videoPermissionsState = videoPermissionsState
    }

    public var combinedPermissionState: CapturePermissionState {
        var foundError = false
        var foundLackOfAccess = false
        var isStillRequestingAccess = false

        switch audioPermissionsState {
        case .requesting:
            isStillRequestingAccess = true
        case .error:
            foundError = true
        case .notPermitted:
            foundLackOfAccess = true
        case .permitted:
            break
        }

        switch videoPermissionsState {
        case .requesting:
            isStillRequestingAccess = true
        case .error:
            foundError = true
        case .notPermitted:
            foundLackOfAccess = true
        case .permitted:
            break
        }

        if foundError {
            return .error
        } else if foundLackOfAccess {
            return .notPermitted
        } else if isStillRequestingAccess {
            return .requesting
        } else {
            return .permitted
        }
    }
}

public enum CaptureOption: Equatable {
    case shouldUseFlash
    case shouldComposeOnCapture
}

public class CaptureCoordinator: ObservableObject {
    enum CaptureCompositionState {
        case notComposing
        case composing
    }

    public enum VideoPermissionsState: Equatable {
        case requesting
        case notPermitted
        case permitted
        case error
    }

    public enum AudioPermissionsState: Equatable {
        case requesting
        case notPermitted
        case permitted
        case error
    }

    private let syncQueue = DispatchQueue(
        label: "Coordinator Sync Queue",
        qos: .userInitiated,
        attributes: [],
        autoreleaseFrequency: .workItem
    )

    @Published public var zoom: CGFloat = 1.0
    @Published public private(set) var recordingFPS: Float = 60.0

    // Originals
    @Published private(set) var capturedVideoURL: URL?
    @Published private(set) var capturedPhotoURL: URL?
    @Published private(set) var capturedAudioURL: URL?

    // Composed
    @Published private(set) var composedVideoWithAudio: URL?

    @Published public var captureOptions: Set<CaptureOption> = []
    @Published private var videoPermissionsState: VideoPermissionsState = .requesting
    @Published private var audioPermissionsState: AudioPermissionsState = .requesting

    public var frameUpdateAction: (CaptureFrameMetaData, CaptureViewSignalToEndAction) -> Void = { _, _ in }

    private let videoComposer = VideoCompositionUtility()
    var previewCoordinator = PreviewMetalViewCoordinator()
    var videoCaptureCoordinator = VideoCaptureCoordinator()
    var audioCaptureCoordinator = AudioCaptureCoordinator()
    var hasTriggeredEndRecording = false
    var recentFramerateHistory: [Float] = []

    private var captureCompositionState: CaptureCompositionState = .notComposing

    #if targetEnvironment(simulator)

        public init() {}

        public func resetAfterActiveSession() {}

        public func flipCamera() {}

        public func capturePhoto() {}

        public func enabledFlashIfNeeded() {}

        public func startVideoRecording() {}

        public func endVideoRecording() {}

    #else

        public init() {
            previewCoordinator.delegate = self
            previewCoordinator.audioCaptureCoordinator = audioCaptureCoordinator
            previewCoordinator.videoCaptureCoordinator = videoCaptureCoordinator

            // Video Capture Coordinator
            videoCaptureCoordinator.previewCoordinator = previewCoordinator
            videoCaptureCoordinator.delegate = self
            videoCaptureCoordinator.configure()

            // Audio Capture
            audioCaptureCoordinator.delegate = self
            audioCaptureCoordinator.configure()
        }

        public func resetAfterActiveSession() {
            previewCoordinator.resetAfterActiveSession()
        }

        public func flipCamera() {
            videoCaptureCoordinator.flipCamera()
        }

        public func capturePhoto() {
            guard case .readyToRecord = previewCoordinator.recordingState else { return }
            if shouldUseFlash {
                Task {
                    videoCaptureCoordinator.toggleFlash(true)
                    try await Task.sleep(for: .seconds(0.5))
                    videoCaptureCoordinator.toggleFlash(false)
                    previewCoordinator.capturePhoto()
                    try await Task.sleep(for: .seconds(0.05))
                    videoCaptureCoordinator.toggleFlash(true)
                    try await Task.sleep(for: .seconds(0.1))
                    videoCaptureCoordinator.toggleFlash(false)
                    audioCaptureCoordinator.resetAudioSessionForPlayback()
                }
            } else {
                previewCoordinator.capturePhoto()
                audioCaptureCoordinator.resetAudioSessionForPlayback()
            }
        }

        public func enabledFlashIfNeeded() {
            guard shouldUseFlash else { return }
            videoCaptureCoordinator.toggleFlash(true)
        }

        public func startVideoRecording() {
            guard case .readyToRecord = previewCoordinator.recordingState else { return }
            previewCoordinator.startRecording()
        }

        public func endVideoRecording() {
            guard case .isRecording = previewCoordinator.recordingState else { return }
            previewCoordinator.endRecording()
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
                self?.disableFlashIfNeeded()
            }
        }

    #endif

    func composeVideoAndAudioIfAvailable() {
        guard
            captureCompositionState == .notComposing,
            let _ = capturedVideoURL,
            let _ = capturedAudioURL
        else { return }
        captureCompositionState = .composing
        composeVideoAndAudio()
    }
}

extension CaptureCoordinator: VideoCaptureCoordinatorDelegate {
    func didUpdateVideoConfiguration(_ result: VideoCaptureCoordinator.SessionSetupResult) {
        DispatchQueue.main.async { [weak self] in
            switch result {
            case .configurationFailed:
                self?.videoPermissionsState = .error
            case .notAuthorized:
                self?.videoPermissionsState = .notPermitted
            case .notStarted:
                self?.videoPermissionsState = .requesting
            case .success:
                self?.videoPermissionsState = .permitted
            }
        }
    }
}

extension CaptureCoordinator: AudioCaptureCoordinatorDelegate {
    func didUpdateAudioConfiguration(_ result: AudioCaptureCoordinator.SessionSetupResult) {
        DispatchQueue.main.async { [weak self] in
            switch result {
            case .configurationFailed:
                self?.audioPermissionsState = .error
            case .notAuthorized:
                self?.audioPermissionsState = .notPermitted
            case .notStarted:
                self?.audioPermissionsState = .requesting
            case .success:
                self?.audioPermissionsState = .permitted
            }
        }
    }

    func didFinishRecordingAudio(_ output: URL?) {
        setAudioOutput(output)
    }
}

extension CaptureCoordinator: PreviewMetalViewCoordinatorDelegate {
    func didUpdateVideoRecordingFrame(_ deltaTime: TimeInterval, recordingState: CaptureRecordingState) {
        DispatchQueue.main.async { [weak self] in
            guard let self else { return }
            frameUpdateAction(
                .init(
                    deltaTime: deltaTime,
                    captureRecordingState: recordingState,
                    audioPermissionsState: audioPermissionsState,
                    videoPermissionsState: videoPermissionsState
                ),
                endVideoRecording
            )
        }
    }

    func setFrameRateUpdate(_ fps: Float) {
        DispatchQueue.main.async { [weak self] in
            guard
                let self,
                case .isRecording = self.previewCoordinator.recordingState,
                fps != .zero
            else { return }

            self.recentFramerateHistory.append(fps)

            if self.recentFramerateHistory.count > 60 {
                self.recentFramerateHistory.removeFirst()
            }

            let fps = self.recentFramerateHistory.reduce(.zero, +) / Float(self.recentFramerateHistory.count)
            self.recordingFPS = fps
        }
    }

    func clearAllOutputURLs() {
        recentFramerateHistory = []
        capturedVideoURL = nil
        capturedPhotoURL = nil
        capturedAudioURL = nil
        composedVideoWithAudio = nil
    }

    func setVideoOutput(_ url: URL?) {
        capturedVideoURL = url
    }

    func setAudioOutput(_ url: URL?) {
        capturedAudioURL = url
    }

    func setImageOutput(_ url: URL?) {
        capturedPhotoURL = url
    }
}

private extension CaptureCoordinator {
    var shouldUseFlash: Bool {
        return captureOptions.contains(.shouldUseFlash)
    }

    func triggerFlashAfterDelay(_ toggleValue: Bool, _ delay: TimeInterval, completion: @escaping () -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
            self?.videoCaptureCoordinator.toggleFlash(toggleValue)
            completion()
        }
    }

    func disableFlashIfNeeded() {
        guard shouldUseFlash else { return }
        videoCaptureCoordinator.toggleFlash(false)
    }

    func composeVideoAndAudio() {
        DispatchQueue.main.async { [weak self] in
            guard
                let self,
                let noAudioVideoURL = self.capturedVideoURL,
                let audioURL = self.capturedAudioURL,
                let randomOutputURL = Adamantium.createRandomFileURLInDocuments("mp4")
            else { return }

            Task {
                await self.videoComposer.mergeVideoAndAudio(
                    videoURL: noAudioVideoURL,
                    audioURL: audioURL,
                    outputURL: randomOutputURL
                ) { result in
                    switch result {
                    case .success(let url):
                        DispatchQueue.main.async { [weak self] in
                            self?.captureCompositionState = .notComposing
                            self?.composedVideoWithAudio = url
                        }

                    case .failure(let error):
                        print(error)
                    }
                }
            }
        }
    }
}
