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)
                        }
                    }
            }
        }
    }
}
