import Foundation
import AVKit
import CoreVideo
import MobileCoreServices

protocol VideoCaptureCoordinatorDelegate: AnyObject {
    func didUpdateVideoConfiguration(_ result: VideoCaptureCoordinator.SessionSetupResult)
}

class VideoCaptureCoordinator: NSObject, ObservableObject, AVCaptureVideoDataOutputSampleBufferDelegate {
    
    enum SessionSetupResult {
        case notStarted
        case success
        case notAuthorized
        case configurationFailed
    }
    
    weak var previewCoordinator: PreviewMetalViewCoordinator?
    weak var delegate: VideoCaptureCoordinatorDelegate?
    
    private var setupResult: SessionSetupResult = .notStarted
    private let session = AVCaptureSession()
    private var isSessionRunning = false
    private var renderingEnabled = false
    
    // Communicate with the session and other session objects on this queue.
    private let sessionQueue = DispatchQueue(
        label: "SessionQueue", 
        attributes: [],
        autoreleaseFrequency: .workItem)
    
    private var videoInput: AVCaptureDeviceInput?
    
    private let dataOutputQueue = DispatchQueue(
        label: "VideoDataQueue", 
        qos: .userInitiated,
        attributes: [],
        autoreleaseFrequency: .workItem)
    
    private let videoDataOutput = AVCaptureVideoDataOutput()
    private let videoDeviceDiscoverySession = AVCaptureDevice
        .DiscoverySession(deviceTypes: [
            .builtInDualCamera,
            .builtInWideAngleCamera
        ],
        mediaType: .video,
        position: .unspecified)
    
    private var defaultVideoDevice: AVCaptureDevice? {
        return videoDeviceDiscoverySession.devices.first
    }
    
    private let photoOutput = AVCapturePhotoOutput()
    
    func configure() {
        checkAuthorization()
        sessionQueue.async { [weak self] in
            guard let self else { return }
            self.configureSession()
        }
    }
    
    func start() {
        sessionQueue.async { [weak self] in
            guard let self else { return }
            switch self.setupResult {
            case .notStarted, .success:
                setupVideoInput()
                self.session.startRunning()
                self.isSessionRunning = self.session.isRunning
                delegate?.didUpdateVideoConfiguration(.success)
                
            case .notAuthorized:
                delegate?.didUpdateVideoConfiguration(.notAuthorized)
                print("AVCamFilter doesn't have permission to use the camera, please change privacy settings")
                break
                
            case .configurationFailed:
                delegate?.didUpdateVideoConfiguration(.configurationFailed)
                print("Configuration failed")
                break
            }
        }
    }
    
    func flipCamera() {
        // print("Adamantium: Flipping Camera")
        changeCamera()
    }
    
    func tappedFocus(_ location: CGPoint) {
        guard let texturePoint = previewCoordinator?.texturePointForView(point: location) else { return }
        let textureRect = CGRect(origin: texturePoint, size: .zero)
        let deviceRect = videoDataOutput.metadataOutputRectConverted(fromOutputRect: textureRect)
        focus(
            with: .autoFocus,
            exposureMode: .autoExpose,
            at: deviceRect.origin,
            monitorSubjectAreaChange: true)
    }
    
    func end() {
        dataOutputQueue.async { [weak self] in
            guard let self else { return }
            self.renderingEnabled = false
        }
        
        sessionQueue.async { [weak self] in
            guard let self else { return }
            if self.setupResult == .success {
                self.session.stopRunning()
                self.isSessionRunning = self.session.isRunning
            }
        }
    }
    
    func captureOutput(
        _ output: AVCaptureOutput,
        didOutput sampleBuffer: CMSampleBuffer,
        from connection: AVCaptureConnection) {
            
        processVideo(sampleBuffer: sampleBuffer)
    }
    
    func toggleFlash(_ isOn: Bool) {
        DispatchQueue.main.async { [weak self] in
            guard let self, let device = self.videoInput?.device else { return }
            
            if device.hasTorch { // Check if the device has a torch (flash)
                do {
                    try device.lockForConfiguration() // Lock the device for configuration
                    device.torchMode = isOn ? .on : .off
                    device.unlockForConfiguration() // Unlock configuration when done
                } catch {
                    print("Error setting flash: \(error)")
                }
            }
        }
    }
}

extension VideoCaptureCoordinator: SegmentationCoordinatorDelegate {
    func didUpdateSegmentationBuffer(_ buffer: CVPixelBuffer) {
//        let metalSegmentationTexture = metalTextureUtility
//            .texture(from: buffer, pixelFormat: .bgra8Unorm)
    }
}

private extension VideoCaptureCoordinator {
    
    func processVideo(sampleBuffer: CMSampleBuffer) {
        guard
            renderingEnabled,
            let videoPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
        else { return }
        
        previewCoordinator?.setPassthroughPixelBuffer(videoPixelBuffer)
    }
    
    func configureSession() {
        guard setupResult == .success else { return }
        delegate?.didUpdateVideoConfiguration(setupResult)
        configurationStepCaptureDevice()
        
        session.beginConfiguration()
        session.sessionPreset = AVCaptureSession.Preset.photo
        
        configurationStepAddVideoInput()
        configurationStepAddVideoOutput()
        configurationStepAddPhotoOutput()
        
        session.commitConfiguration()
    }
    
    func setupVideoInput() {
        let interfaceOrientation = UIInterfaceOrientation.portrait
        guard let videoInput = self.videoInput else { return }
        let videoDevicePosition = videoInput.device.position
        let isMirroring = (videoDevicePosition == .front)
        
        let rotation = PassthroughRenderPass.Rotation(
            with: interfaceOrientation,
            videoOrientation: .landscapeRight,
            cameraPosition: videoDevicePosition)
        
        self.previewCoordinator?.setPassthroughMirroring(isMirroring)
        
        if let rotation = rotation {
            self.previewCoordinator?.setPassthroughRotation(rotation)
        }
        
        self.dataOutputQueue.async {
            self.renderingEnabled = true
        }
    }
    
    func configurationStepCaptureDevice() {
        
        guard let videoDevice = defaultVideoDevice else {
            print("Could not find any video device")
            setupResult = .configurationFailed
            delegate?.didUpdateVideoConfiguration(.configurationFailed)
            return
        }
        
        do {
            videoInput = try AVCaptureDeviceInput(device: videoDevice)
        } catch {
            print("Could not create video device input: \(error)")
            setupResult = .configurationFailed
            delegate?.didUpdateVideoConfiguration(.configurationFailed)
            return
        }
    }
    
    func configurationStepAddVideoInput() {
        guard let videoInput, session.canAddInput(videoInput) else {
            print("Could not add video device input to the session")
            setupResult = .configurationFailed
            delegate?.didUpdateVideoConfiguration(.configurationFailed)
            session.commitConfiguration()
            return
        }
        session.addInput(videoInput)
    }
    
    func configurationStepAddVideoOutput() {
        if session.canAddOutput(videoDataOutput) {
            session.addOutput(videoDataOutput)
            videoDataOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)]
            videoDataOutput.setSampleBufferDelegate(self, queue: dataOutputQueue)
        } else {
            print("Could not add video data output to the session")
            setupResult = .configurationFailed
            delegate?.didUpdateVideoConfiguration(.configurationFailed)
            session.commitConfiguration()
            return
        }
    }
    
    func configurationStepAddPhotoOutput() {
        if session.canAddOutput(photoOutput) {
            session.addOutput(photoOutput)
            // photoOutput.isHighResolutionCaptureEnabled = true
            
        } else {
            print("Could not add photo output to the session")
            setupResult = .configurationFailed
            session.commitConfiguration()
            return
        }
    }
    
    func checkAuthorization() {
        // Check video authorization status, video access is required
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .authorized:
            // The user has previously granted access to the camera
            setupResult = .success
            delegate?.didUpdateVideoConfiguration(.success)
            break
            
        case .notDetermined:
            /*
             The user has not yet been presented with the option to grant video access
             Suspend the SessionQueue to delay session setup until the access request has completed
             */
            sessionQueue.suspend()
            AVCaptureDevice.requestAccess(
                for: .video,
                completionHandler: { [weak self] granted in
                    
                    guard let self else { return }
                    if granted {
                        self.setupResult = .success
                        self.delegate?.didUpdateVideoConfiguration(.success)
                    } else {
                        self.setupResult = .notAuthorized
                        self.delegate?.didUpdateVideoConfiguration(.notAuthorized)
                    }
                    self.sessionQueue.resume()
                })
            
        default:
            // The user has previously denied access
            setupResult = .notAuthorized
            delegate?.didUpdateVideoConfiguration(.notAuthorized)
        }
    }
    
    func focus(
        with focusMode: AVCaptureDevice.FocusMode,
        exposureMode: AVCaptureDevice.ExposureMode,
        at devicePoint: CGPoint,
        monitorSubjectAreaChange: Bool) {
            
        sessionQueue.async { [weak self] in
            guard let self, let videoDevice = self.videoInput?.device else { return }
            
            do {
                try videoDevice.lockForConfiguration()
                if videoDevice.isFocusPointOfInterestSupported && videoDevice.isFocusModeSupported(focusMode) {
                    videoDevice.focusPointOfInterest = devicePoint
                    videoDevice.focusMode = focusMode
                }
                
                if videoDevice.isExposurePointOfInterestSupported && videoDevice.isExposureModeSupported(exposureMode) {
                    videoDevice.exposurePointOfInterest = devicePoint
                    videoDevice.exposureMode = exposureMode
                }
                
                videoDevice.isSubjectAreaChangeMonitoringEnabled = monitorSubjectAreaChange
                videoDevice.unlockForConfiguration()
            } catch {
                print("Could not lock device for configuration: \(error)")
            }
        }
    }
    
    
    func changeCamera() {
        dataOutputQueue.sync {
            renderingEnabled = false
            
            // If you want black between flipping the camera you can use this
            previewCoordinator?.setPassthroughPixelBuffer(nil)
                        
            sessionQueue.async { [weak self] in
                guard let self, let videoInput = self.videoInput else { return }
                
                var preferredPosition = AVCaptureDevice.Position.unspecified
                let currentVideoDevice = videoInput.device
                switch currentVideoDevice.position {
                case .unspecified, .front:
                    preferredPosition = .back
                case .back:
                    preferredPosition = .front
                @unknown default:
                    fatalError("Unknown video device position.")
                }
                
                let devices = self.videoDeviceDiscoverySession.devices
                if let videoDevice = devices.first(where: { $0.position == preferredPosition }) {
                    do {
                        let newVideoInput = try AVCaptureDeviceInput(device: videoDevice)
                        
                        self.session.beginConfiguration()
                        
                        // Remove the existing device input first, since
                        // using the front and back camera simultaneously
                        // is not supported.
                        self.session.removeInput(videoInput)
                        
                        if self.session.canAddInput(newVideoInput) {
                            self.session.addInput(newVideoInput)
                            self.videoInput = newVideoInput
                        } else {
                            print("Could not add video device input to the session")
                            self.session.addInput(videoInput)
                        }
                        
                        self.session.commitConfiguration()
                        setupVideoInput()
                        
                    } catch {
                        print("Could not create video device input: \(error)")
                        self.dataOutputQueue.async {
                            self.renderingEnabled = false
                        }
                        return
                    }
                }
                
                self.dataOutputQueue.async {
                    self.renderingEnabled = true
                }
            }
        }
    }
}

extension AVCaptureConnection {
    var videoOrientationFromAngle: UIInterfaceOrientation {
        let angle = Int(videoRotationAngle)
        switch angle {
        case 0:
            return .portrait
        case 90:
            return .landscapeRight
        case 180:
            return .portraitUpsideDown
        case 270:
            return .landscapeLeft
        default:
            // Think about this a little bit more
            return .portrait
        }
    }
}

extension PassthroughRenderPass.Rotation {
    init?(
        with interfaceOrientation: UIInterfaceOrientation,
        videoOrientation: UIInterfaceOrientation,
        cameraPosition: AVCaptureDevice.Position) {
            
        /*
            Calculate the rotation between the videoOrientation and the interfaceOrientation.
            The direction of the rotation depends upon the camera position.
         */
        switch videoOrientation {
        case .portrait:
            switch interfaceOrientation {
            case .landscapeRight:
                if cameraPosition == .front {
                    self = .rotate90Degrees
                } else {
                    self = .rotate270Degrees
                }
                
            case .landscapeLeft:
                if cameraPosition == .front {
                    self = .rotate270Degrees
                } else {
                    self = .rotate90Degrees
                }
                
            case .portrait:
                self = .rotate0Degrees
                
            case .portraitUpsideDown:
                self = .rotate180Degrees
                
            default: return nil
            }
        case .portraitUpsideDown:
            switch interfaceOrientation {
            case .landscapeRight:
                if cameraPosition == .front {
                    self = .rotate270Degrees
                } else {
                    self = .rotate90Degrees
                }
                
            case .landscapeLeft:
                if cameraPosition == .front {
                    self = .rotate90Degrees
                } else {
                    self = .rotate270Degrees
                }
                
            case .portrait:
                self = .rotate180Degrees
                
            case .portraitUpsideDown:
                self = .rotate0Degrees
                
            default: return nil
            }
            
        case .landscapeRight:
            switch interfaceOrientation {
            case .landscapeRight:
                self = .rotate0Degrees
                
            case .landscapeLeft:
                self = .rotate180Degrees
                
            case .portrait:
                if cameraPosition == .front {
                    self = .rotate270Degrees
                } else {
                    self = .rotate90Degrees
                }
                
            case .portraitUpsideDown:
                if cameraPosition == .front {
                    self = .rotate90Degrees
                } else {
                    self = .rotate270Degrees
                }
                
            default: return nil
            }
            
        case .landscapeLeft:
            switch interfaceOrientation {
            case .landscapeLeft:
                self = .rotate0Degrees
                
            case .landscapeRight:
                self = .rotate180Degrees
                
            case .portrait:
                if cameraPosition == .front {
                    self = .rotate90Degrees
                } else {
                    self = .rotate270Degrees
                }
                
            case .portraitUpsideDown:
                if cameraPosition == .front {
                    self = .rotate270Degrees
                } else {
                    self = .rotate90Degrees
                }
                
            default: return nil
            }
        case .unknown:
            self = .rotate0Degrees
        @unknown default:
            self = .rotate0Degrees
        }
    }
}
