import AVFoundation
import Foundation

public protocol CameraDelegate: class {
    func didCaptureBuffer(_ sampleBuffer: CMSampleBuffer)
}

public enum PhysicalCameraLocation {
    case backFacing
    case frontFacing

    // Documentation: "The front-facing camera would always deliver buffers in AVCaptureVideoOrientationLandscapeLeft and the back-facing camera would always deliver buffers in AVCaptureVideoOrientationLandscapeRight."
    func imageOrientation() -> ImageOrientation {
        switch self {
        case .backFacing: return .portrait
        case .frontFacing: return .portrait
        }
    }

    func captureDevicePosition() -> AVCaptureDevice.Position {
        switch self {
        case .backFacing: return .back
        case .frontFacing: return .front
        }
    }

    public func device() -> AVCaptureDevice? {
        let devices = AVCaptureDevice.devices(for: AVMediaType.video)
        for case let device in devices {
            if device.position == captureDevicePosition() {
                return device
            }
        }

        return AVCaptureDevice.default(for: AVMediaType.video)
    }
}

public enum CameraError: Error, CustomStringConvertible {
    case avCaptureDeviceNotFound

    public var errorDescription: String {
        switch self {
        case .avCaptureDeviceNotFound:
            return "The AVCaptureDevice could not be found. Camera is not supported on Simulator."
        }
    }

    public var description: String {
        return "<\(type(of: self)): errorDescription = \(errorDescription)>"
    }
}

let initialBenchmarkFramesToIgnore = 5

public class Camera: NSObject, ImageSource, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
    public var location: PhysicalCameraLocation {
        didSet {
            // TODO: Swap the camera locations, framebuffers as needed
        }
    }

    public var runBenchmark: Bool = false
    public var logFPS: Bool = false
    public var audioEncodingTarget: AudioEncodingTarget? {
        didSet {
            guard let audioEncodingTarget = audioEncodingTarget else {
                return
            }
            do {
                try addAudioInputsAndOutputs()
                audioEncodingTarget.activateAudioTrack()
            } catch {
                debugPrint("ERROR: Could not connect audio target with error: \(error)")
            }
        }
    }

    public let targets = TargetContainer()
    public weak var delegate: CameraDelegate?
    public let captureSession: AVCaptureSession
    public let inputCamera: AVCaptureDevice!
    public let videoInput: AVCaptureDeviceInput!
    public let videoOutput: AVCaptureVideoDataOutput!
    public var microphone: AVCaptureDevice?
    public var audioInput: AVCaptureDeviceInput?
    public var audioOutput: AVCaptureAudioDataOutput?

    var supportsFullYUVRange: Bool = false
    let captureAsYUV: Bool
    let yuvConversionShader: ShaderProgram?
    let frameRenderingSemaphore = DispatchSemaphore(value: 1)

    let cameraProcessingQueue = DispatchQueue.global()
    let audioProcessingQueue = DispatchQueue.global()

    let framesToIgnore = 5
    var numberOfFramesCaptured = 0
    var totalFrameTimeDuringCapture: Double = 0.0
    var framesSinceLastCheck = 0
    var lastCheckTime = CFAbsoluteTimeGetCurrent()

    public init(sessionPreset: AVCaptureSession.Preset, cameraDevice: AVCaptureDevice? = nil, location: PhysicalCameraLocation = .backFacing, captureAsYUV: Bool = true) throws {
        self.location = location
        self.captureAsYUV = captureAsYUV

        captureSession = AVCaptureSession()
        captureSession.beginConfiguration()

        if let cameraDevice = cameraDevice {
            inputCamera = cameraDevice
        } else {
            if let device = location.device() {
                inputCamera = device
            } else {
                videoInput = nil
                videoOutput = nil
                yuvConversionShader = nil
                inputCamera = nil
                super.init()
                throw CameraError.avCaptureDeviceNotFound
            }
        }

        do {
            videoInput = try AVCaptureDeviceInput(device: inputCamera)
        } catch {
            videoInput = nil
            videoOutput = nil
            yuvConversionShader = nil
            super.init()
            throw error
        }
        if captureSession.canAddInput(videoInput) {
            captureSession.addInput(videoInput)
        }

        // Add the video frame output
        videoOutput = AVCaptureVideoDataOutput()
        videoOutput.alwaysDiscardsLateVideoFrames = false

        if captureAsYUV {
            supportsFullYUVRange = false
            let supportedPixelFormats = videoOutput.availableVideoPixelFormatTypes
            for currentPixelFormat in supportedPixelFormats {
                if (currentPixelFormat as NSNumber).int32Value == Int32(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
                    supportsFullYUVRange = true
                }
            }

            if supportsFullYUVRange {
                yuvConversionShader = crashOnShaderCompileFailure("Camera") { try sharedImageProcessingContext.programForVertexShader(defaultVertexShaderForInputs(2), fragmentShader: YUVConversionFullRangeFragmentShader) }
                videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: Int32(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange))]
            } else {
                yuvConversionShader = crashOnShaderCompileFailure("Camera") { try sharedImageProcessingContext.programForVertexShader(defaultVertexShaderForInputs(2), fragmentShader: YUVConversionVideoRangeFragmentShader) }
                videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: Int32(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange))]
            }
        } else {
            yuvConversionShader = nil
            videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: Int32(kCVPixelFormatType_32BGRA))]
        }

        if captureSession.canAddOutput(videoOutput) {
            captureSession.addOutput(videoOutput)
        }
        captureSession.sessionPreset = sessionPreset

        var captureConnection: AVCaptureConnection!
        for connection in videoOutput.connections {
            for port in connection.inputPorts {
                if port.mediaType == AVMediaType.video {
                    captureConnection = connection
                    captureConnection.isVideoMirrored = location == .frontFacing
                }
            }
        }

        if captureConnection.isVideoOrientationSupported {
            captureConnection.videoOrientation = .portrait
        }

        captureSession.commitConfiguration()

        super.init()

        videoOutput.setSampleBufferDelegate(self, queue: cameraProcessingQueue)
    }

    deinit {
        let captureSession = self.captureSession
        DispatchQueue.global().async {
            if captureSession.isRunning {
                // Don't call this on the sharedImageProcessingContext otherwise you may get a deadlock
                // since this waits for the captureOutput() delegate call to finish.
                captureSession.stopRunning()
            }
        }

        sharedImageProcessingContext.runOperationSynchronously {
            self.videoOutput?.setSampleBufferDelegate(nil, queue: nil)
            self.audioOutput?.setSampleBufferDelegate(nil, queue: nil)
        }
    }

    public func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from _: AVCaptureConnection) {
        guard output != audioOutput else {
            processAudioSampleBuffer(sampleBuffer)
            return
        }

        guard frameRenderingSemaphore.wait(timeout: DispatchTime.now()) == DispatchTimeoutResult.success else { return }

        let startTime = CFAbsoluteTimeGetCurrent()

        let cameraFrame = CMSampleBufferGetImageBuffer(sampleBuffer)!
        let bufferWidth = CVPixelBufferGetWidth(cameraFrame)
        let bufferHeight = CVPixelBufferGetHeight(cameraFrame)
        let currentTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)

        CVPixelBufferLockBaseAddress(cameraFrame, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0)))
        sharedImageProcessingContext.runOperationAsynchronously {
            let cameraFramebuffer: Framebuffer

            self.delegate?.didCaptureBuffer(sampleBuffer)
            if self.captureAsYUV {
                let luminanceFramebuffer: Framebuffer
                let chrominanceFramebuffer: Framebuffer
                if sharedImageProcessingContext.supportsTextureCaches() {
                    var luminanceTextureRef: CVOpenGLESTexture?
                    _ = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, sharedImageProcessingContext.coreVideoTextureCache, cameraFrame, nil, GLenum(GL_TEXTURE_2D), GL_LUMINANCE, GLsizei(bufferWidth), GLsizei(bufferHeight), GLenum(GL_LUMINANCE), GLenum(GL_UNSIGNED_BYTE), 0, &luminanceTextureRef)
                    let luminanceTexture = CVOpenGLESTextureGetName(luminanceTextureRef!)
                    glActiveTexture(GLenum(GL_TEXTURE4))
                    glBindTexture(GLenum(GL_TEXTURE_2D), luminanceTexture)
                    glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
                    glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
                    luminanceFramebuffer = try! Framebuffer(context: sharedImageProcessingContext, orientation: self.location.imageOrientation(), size: GLSize(width: GLint(bufferWidth), height: GLint(bufferHeight)), textureOnly: true, overriddenTexture: luminanceTexture)

                    var chrominanceTextureRef: CVOpenGLESTexture?
                    _ = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault, sharedImageProcessingContext.coreVideoTextureCache, cameraFrame, nil, GLenum(GL_TEXTURE_2D), GL_LUMINANCE_ALPHA, GLsizei(bufferWidth / 2), GLsizei(bufferHeight / 2), GLenum(GL_LUMINANCE_ALPHA), GLenum(GL_UNSIGNED_BYTE), 1, &chrominanceTextureRef)
                    let chrominanceTexture = CVOpenGLESTextureGetName(chrominanceTextureRef!)
                    glActiveTexture(GLenum(GL_TEXTURE5))
                    glBindTexture(GLenum(GL_TEXTURE_2D), chrominanceTexture)
                    glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
                    glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
                    chrominanceFramebuffer = try! Framebuffer(context: sharedImageProcessingContext, orientation: self.location.imageOrientation(), size: GLSize(width: GLint(bufferWidth / 2), height: GLint(bufferHeight / 2)), textureOnly: true, overriddenTexture: chrominanceTexture)
                } else {
                    glActiveTexture(GLenum(GL_TEXTURE4))
                    luminanceFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation: self.location.imageOrientation(), size: GLSize(width: GLint(bufferWidth), height: GLint(bufferHeight)), textureOnly: true)
                    luminanceFramebuffer.lock()

                    glBindTexture(GLenum(GL_TEXTURE_2D), luminanceFramebuffer.texture)
                    glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_LUMINANCE, GLsizei(bufferWidth), GLsizei(bufferHeight), 0, GLenum(GL_LUMINANCE), GLenum(GL_UNSIGNED_BYTE), CVPixelBufferGetBaseAddressOfPlane(cameraFrame, 0))

                    glActiveTexture(GLenum(GL_TEXTURE5))
                    chrominanceFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation: self.location.imageOrientation(), size: GLSize(width: GLint(bufferWidth / 2), height: GLint(bufferHeight / 2)), textureOnly: true)
                    chrominanceFramebuffer.lock()
                    glBindTexture(GLenum(GL_TEXTURE_2D), chrominanceFramebuffer.texture)
                    glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_LUMINANCE_ALPHA, GLsizei(bufferWidth / 2), GLsizei(bufferHeight / 2), 0, GLenum(GL_LUMINANCE_ALPHA), GLenum(GL_UNSIGNED_BYTE), CVPixelBufferGetBaseAddressOfPlane(cameraFrame, 1))
                }

                cameraFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation: .portrait, size: luminanceFramebuffer.sizeForTargetOrientation(.portrait), textureOnly: false)

                let conversionMatrix: Matrix3x3
                if self.supportsFullYUVRange {
                    conversionMatrix = colorConversionMatrix601FullRangeDefault
                } else {
                    conversionMatrix = colorConversionMatrix601Default
                }
                convertYUVToRGB(shader: self.yuvConversionShader!, luminanceFramebuffer: luminanceFramebuffer, chrominanceFramebuffer: chrominanceFramebuffer, resultFramebuffer: cameraFramebuffer, colorConversionMatrix: conversionMatrix)
            } else {
                cameraFramebuffer = sharedImageProcessingContext.framebufferCache.requestFramebufferWithProperties(orientation: self.location.imageOrientation(), size: GLSize(width: GLint(bufferWidth), height: GLint(bufferHeight)), textureOnly: true)
                glBindTexture(GLenum(GL_TEXTURE_2D), cameraFramebuffer.texture)
                glTexImage2D(GLenum(GL_TEXTURE_2D), 0, GL_RGBA, GLsizei(bufferWidth), GLsizei(bufferHeight), 0, GLenum(GL_BGRA), GLenum(GL_UNSIGNED_BYTE), CVPixelBufferGetBaseAddress(cameraFrame))
            }
            CVPixelBufferUnlockBaseAddress(cameraFrame, CVPixelBufferLockFlags(rawValue: CVOptionFlags(0)))

            cameraFramebuffer.timingStyle = .videoFrame(timestamp: Timestamp(currentTime))
            self.updateTargetsWithFramebuffer(cameraFramebuffer)

            if self.runBenchmark {
                self.numberOfFramesCaptured += 1
                if self.numberOfFramesCaptured > initialBenchmarkFramesToIgnore {
                    let currentFrameTime = (CFAbsoluteTimeGetCurrent() - startTime)
                    self.totalFrameTimeDuringCapture += currentFrameTime
                    print("Average frame time : \(1000.0 * self.totalFrameTimeDuringCapture / Double(self.numberOfFramesCaptured - initialBenchmarkFramesToIgnore)) ms")
                    print("Current frame time : \(1000.0 * currentFrameTime) ms")
                }
            }

            if self.logFPS {
                if (CFAbsoluteTimeGetCurrent() - self.lastCheckTime) > 1.0 {
                    self.lastCheckTime = CFAbsoluteTimeGetCurrent()
                    print("FPS: \(self.framesSinceLastCheck)")
                    self.framesSinceLastCheck = 0
                }

                self.framesSinceLastCheck += 1
            }

            self.frameRenderingSemaphore.signal()
        }
    }

    public func startCapture() {
        numberOfFramesCaptured = 0
        totalFrameTimeDuringCapture = 0

        if !captureSession.isRunning {
            captureSession.startRunning()
        }
    }

    public func stopCapture() {
        if captureSession.isRunning {
            captureSession.stopRunning()
        }
    }

    public func transmitPreviousImage(to _: ImageConsumer, atIndex _: UInt) {
        // Not needed for camera inputs
    }

    // MARK: -

    // MARK: Audio processing

    public func addAudioInputsAndOutputs() throws {
        guard self.audioOutput == nil else { return }

        captureSession.beginConfiguration()
        defer {
            captureSession.commitConfiguration()
        }
        guard let microphone = AVCaptureDevice.default(for: .audio) else {
            return
        }
        let audioInput = try AVCaptureDeviceInput(device: microphone)
        if captureSession.canAddInput(audioInput) {
            captureSession.addInput(audioInput)
        }
        let audioOutput = AVCaptureAudioDataOutput()
        if captureSession.canAddOutput(audioOutput) {
            captureSession.addOutput(audioOutput)
        }
        self.microphone = microphone
        self.audioInput = audioInput
        self.audioOutput = audioOutput
        audioOutput.setSampleBufferDelegate(self, queue: audioProcessingQueue)
    }

    public func removeAudioInputsAndOutputs() {
        guard audioOutput != nil else { return }

        captureSession.beginConfiguration()
        captureSession.removeInput(audioInput!)
        captureSession.removeOutput(audioOutput!)
        audioInput = nil
        audioOutput = nil
        microphone = nil
        captureSession.commitConfiguration()
    }

    func processAudioSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
        audioEncodingTarget?.processAudioBuffer(sampleBuffer)
    }
}
