//
//  VideoSource.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `VideoSource` is an interface for sources which produce video frames, and can send to a `VideoSink`.
/// Implementations can be passed to the `AudioVideoFacade` to be used as the video source sent to remote
/// participlants
@objc public protocol VideoSource {
    /// Content hint for downstream processing.
    var videoContentHint: VideoContentHint { get set }

    /// Add a video sink which will immediately begin to receive new frames.
    ///
    /// Multiple sinks can be added to a single `VideoSource` to allow forking of video frames,
    /// e.g. to send to both local preview and AmazonChimeSDKMedia framework (i.e. for encoding) at the same time.
    ///
    /// - Parameter sink: New video sink
    func addVideoSink(sink: VideoSink)

    /// Remove a video sink which will no longer receive new frames on return.
    ///
    /// - Parameter sink: Video sink to remove
    func removeVideoSink(sink: VideoSink)
}


//
//  VideoSubscriptionConfiguration.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Configuration for a specific video source.
/// The values are intentionally mutable so that a map of all current configurations can be kept and updated as needed.
///
/// `VideoSubscriptionConfiguration` is used to contain the priority and resolution of
/// remote video sources and content share to be received
@objcMembers public class VideoSubscriptionConfiguration: NSObject {
    ///   - priority: Relative priority for the subscription.
    ///   - targetResolution: A target resolution for the subscription. The actual receive resolution may vary.
    public var priority: VideoPriority = VideoPriority.high
    public var targetResolution: VideoResolution = VideoResolution.high
}


//
//  VideoTile.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation
import VideoToolbox

/// `VideoTile` is a tile that binds video render view to diplay the frame into the view.
@objc public protocol VideoTile: VideoSink {
    /// State of VideoTile
    var state: VideoTileState { get }

    /// View which will be used to render the Video Frame
    var videoRenderView: VideoRenderView? { get set }

    /// Binds the view to the tile. The view needs to be create by the application.
    /// Once the binding is done, the view will start displaying the video frame automatically
    ///
    /// - Parameter videoRenderView: the view created by application to render the video frame
    func bind(videoRenderView: VideoRenderView?)

    /// Unbinds the `videoRenderView` from tile.
    func unbind()

    /// Update the pause state of the tile.
    func setPauseState(pauseState: VideoPauseState)
}


//
//  VideoTileController.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import CoreGraphics.CGImage
import Foundation
import VideoToolbox

/// `VideoTileController` handles rendering/creating of new `VideoTile`.
@objc public protocol VideoTileController: VideoTileControllerFacade {
    /// Called whenever there is a new Video frame received for any of the attendee in the meeting
    /// - Parameters:
    ///   - frame: a frame of video
    ///   - videoId: unique id that belongs to video being transmitted
    ///   - attendeeId: a id of user who is transmitting current frame
    ///   - pauseState: current pause state of the video being received
    func onReceiveFrame(
        frame: VideoFrame?,
        videoId: Int,
        attendeeId: String?,
        pauseState: VideoPauseState
    )
}


//
//  VideoTileControllerFacade.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `VideoTileControllerFacade` allows one to control `VideoTile`. The caller is responsible for laying
/// out video render views as desired and binding tile ids received from the observer
/// in the `videoTileDidAdd` and `videoTileDidRemove` callbacks.
@objc public protocol VideoTileControllerFacade {
    /// Binds the video rendering view to Video Tile. The view will start displaying the video frame
    /// after the completion of this API
    ///
    /// - Parameters:
    ///   - videoView: View to render the video. Application needs to create it and pass to SDK.
    ///   - tileId: id of the tile which was passed to the application in `VideoTileObserver.videoTileDidAdd`
    func bindVideoView(videoView: VideoRenderView, tileId: Int)

    /// Unbinds the video rendering view from Video Tile. The view will stop displaying the video frame
    /// after the completion of this API
    ///
    /// - Parameter tileId: id of the tile which was passed to the application in `VideoTileObserver.videoTileDidRemove`
    func unbindVideoView(tileId: Int)

    /// Subscribe to Video Tile events with an `VideoTileObserver`.
    ///
    /// - Parameter observer: The observer to subscribe to events with
    func addVideoTileObserver(observer: VideoTileObserver)

    /// Unsubscribes from Video Tile events by removing specified `VideoTileObserver`.
    ///
    /// - Parameter observer: The observer to unsubscribe from events with
    func removeVideoTileObserver(observer: VideoTileObserver)

    /// Pauses remote video tile, if it exists.
    ///
    /// - Parameter tileId: The tile id to pause
    func pauseRemoteVideoTile(tileId: Int)

    /// Resume remote video tile, if it exists.
    ///
    /// - Parameter tileId: The tile id to resume
    func resumeRemoteVideoTile(tileId: Int)
}


//
//  VideoTileObserver.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `VideoTileObserver` handles events related to `VideoTile`.
@objc public protocol VideoTileObserver {
    /// Called whenever a new attendee starts sharing the video
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameters:
    ///   - tileState: video tile state associated with this attendee
    func videoTileDidAdd(tileState: VideoTileState)

    /// Called whenever any attendee stops sharing the video
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameters:
    ///   - tileState: video tile state associated with this attendee
    func videoTileDidRemove(tileState: VideoTileState)

    /// Called whenever an attendee tile pauseState changes from .unpaused
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameters:
    ///   - tileState: video tile state associated with this attendee
    func videoTileDidPause(tileState: VideoTileState)

    /// Called whenever an attendee tile pauseState changes to .unpaused
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameters:
    ///   - tileState: video tile state associated with this attendee
    func videoTileDidResume(tileState: VideoTileState)

    /// Called whenever a video steam size is changed
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameters:
    ///   - tileState: video tile state whose size was changed
    func videoTileSizeDidChange(tileState: VideoTileState)
}


//
//  VideoTileState.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `VideoTileState` encapsulates the state of a `VideoTile`.
@objcMembers public class VideoTileState: NSObject {
    /// Unique Id associated with this tile
    public let tileId: Int

    /// Id of the user associated with this tile
    public let attendeeId: String

    /// Width of video stream content
    public var videoStreamContentWidth: Int

    /// Height of video stream content
    public var videoStreamContentHeight: Int

    /// Current pause state of this tile
    public var pauseState: VideoPauseState

    /// Whether tile is local or remote tile
    public let isLocalTile: Bool

    /// Whether this is screen share
    public let isContent: Bool

    public init(tileId: Int,
                attendeeId: String,
                videoStreamContentWidth: Int,
                videoStreamContentHeight: Int,
                pauseState: VideoPauseState,
                isLocalTile: Bool) {
        self.tileId = tileId
        self.attendeeId = attendeeId
        self.videoStreamContentWidth = videoStreamContentWidth
        self.videoStreamContentHeight = videoStreamContentHeight
        self.pauseState = pauseState
        self.isLocalTile = isLocalTile
        self.isContent = DefaultModality(id: attendeeId).isOfType(type: .content)
    }
}


//
//  BackgroundFilter.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Enum defining the different background filter options.
@objc public enum BackgroundFilter: Int {
    case none
    case blur
    case replacement

    public var description: String {
        switch self {
        case .none:
            return "none"
        case .blur:
            return "blur"
        case .replacement:
            return "replacement"
        }
    }
}


//
//  BackgroundFilterProcessor.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import CoreImage
import CoreMedia
import Foundation
import UIKit

/// `BackgroundFilterProcessor` is a processor that uses `SegmentationProcessor` to process a frame by
/// creating the alpha mask of the foreground image and blending the mask with the input image which is then rendered on
/// top of a background image.
public class BackgroundFilterProcessor {
    /// Context used for processing and rendering the final output image.
    private let context = CIContext(options: [.cacheIntermediates: false])

    /// `CVPixelBufferPool` used to store the final output image.
    private var bufferPool: CVPixelBufferPool?

    /// Used to track buffer pool width.
    private var bufferPoolWidth: Int = 0

    /// Used to track buffer pool height.
    private var bufferPoolHeight: Int = 0

    /// A segmentation processor used to predict foreground of an image.
    /// See `SegmentationProcessor` for more details.
    private let segmentationProcessor: SegmentationProcessor

    /// Custom logger to log any errors or warnings.
    let logger: Logger

    /// Segmentation processor height.
    private var segmentationProcessorHeight = 256

    /// Segmentation processor width.
    private var segmentationProcessorWidth = 144

    /// Static method to check whether BackgroundFilterProcessor can be used. This verifies that the builder
    /// has linked the necessary runtime framework (i.e. `AmazonChimeSDKMachineLearning`) to
    /// use this class.
    ///
    /// - Returns: true if the class can be used, otherwise false.
    public static func isAvailable() -> Bool {
        return TensorFlowSegmentationProcessor.isAvailable()
    }

    /// Public constructor to initialize the processor.
    ///
    /// - Parameters:
    ///   - logger: Custom logger to log events.
    public init(logger: Logger) {
        self.logger = logger
        if !BackgroundFilterProcessor.isAvailable() {
            self.logger.error(msg: "Unable to load TensorFlowLiteSegmentationProcessor. " +
                              "See `Update Project File` section in README for more information " +
                              "on how to import `AmazonChimeSDKMachineLearning` framework " +
                              "and the `selfie_segmentation_landscape.tflite` as a bundle resource " +
                              "to your project.")
            segmentationProcessor = NoopSegmentationProcessor()
        } else {
            segmentationProcessor = TensorFlowSegmentationProcessor()
        }
    }

    /// Creates the alpha mask [0-255] of the foreground image using `SegmentationProcessor`.
    ///
    /// - Parameters:
    ///   - inputFrameCG: Input CGImage frame to produce the foreground image.
    ///   - inputFrameCI: Input CIImage frame to produce the foreground image.
    ///
    /// - Returns: Alpha mask CGImage of the foreground.
    public func createForegroundAlphaMask(inputFrameCG: CGImage,
                                          inputFrameCI: CIImage) -> CIImage? {
        // Verify that the processor is available.
        if !BackgroundFilterProcessor.isAvailable() {
            return nil
        }

        // Number of the input image color space channels.
        let imageChannels = inputFrameCG.bitsPerPixel / inputFrameCG.bitsPerComponent

        // Update the buffer pool dimensions if the new frame does not match the previous frame dimensions.
        if bufferPool == nil || inputFrameCG.width != bufferPoolWidth || inputFrameCG.height != bufferPoolHeight {
            logger.info(msg: "Updating buffer pool with new sizes: \(inputFrameCG.width) x \(inputFrameCG.height)")
            updateBufferPool(newWidth: inputFrameCG.width, newHeight: inputFrameCG.height)
            // Initialize the segmentationProcessor if it has not been initialized.
            let initializeResult: Bool = segmentationProcessor.initialize(segmentationProcessorHeight,
                                                                          width: segmentationProcessorWidth,
                                                                          channels: imageChannels)
            if !initializeResult {
                logger.error(msg: "Unable to initialize segmentation processor.")
                return nil
            }
        }

        // Check if segmentation model has loaded.
        if segmentationProcessor.getModelState() != CwtModelState.LOADED.rawValue {
            logger.error(msg: "Segmentation processor failed to start. Unable to perform segmentation.")
            return nil
        }

        // Downscale the image.
        let downSize = CGSize(width: segmentationProcessorWidth, height: segmentationProcessorHeight)
        guard let downscaledImageCG: CGImage = resizeImage(image: inputFrameCG, newSize: downSize)
        else {
            logger.error(msg: "Error downscaling input frame")
            return nil
        }

        // Convert the input CGImage to a UInt8 byte array.
        guard var byteArray: [UInt8] = ImageConversionUtils.cgImageToByteArray(cgImage: downscaledImageCG) else {
            logger.error(msg: "Error converting CGImage to byte array when creating the foreground mask.")
            return nil
        }

        // Copy the input buffer to the TensorFlow buffer which will be used during predict.
        let inputBuffer: UnsafeMutablePointer<UInt8> = segmentationProcessor.getInputBuffer()
        inputBuffer.initialize(from: &byteArray, count: byteArray.count)

        // Predict the foreground mask.
        let predictResult: Bool = segmentationProcessor.predict()
        if !predictResult {
            logger.error(msg: "Error predicting the foreground mask.")
            return nil
        }

        // Retrieve the foreground mask.
        let maskOutputBuffer = segmentationProcessor.getOutputBuffer()

        guard let maskImage: CGImage = ImageConversionUtils.byteArrayToCGImage(
            raw: maskOutputBuffer,
            frameWidth: segmentationProcessorWidth,
            frameHeight: segmentationProcessorHeight,
            bytesPerPixel: imageChannels,
            bitsPerComponent: inputFrameCG.bitsPerComponent
        ) else {
            logger.error(msg: "Error creating CGImage of the foreground mask.")
            return nil
        }

        // Upscale the image back to it size.
        let originalSize = CGSize(width: inputFrameCG.width, height: inputFrameCG.height)
        guard let upscaledMaskImage = resizeImage(image: maskImage, newSize: originalSize)
        else {
            logger.error(msg: "Error upscaling segmentation mask")
            return nil
        }

        return CIImage(cgImage: upscaledMaskImage)
    }

    /// Blends foreground alpha mask with input image to produce a foreground image which is rendered on top
    /// of a background image using `CIBlendWithAlphaMask` CIFilter.
    ///
    /// - Parameters:
    ///   - inputFrameCI: Input image which is used to blend the foreground alpha mask to produce the foreground image.
    ///   - maskImage: Foreground alpha mask.
    ///   - backgroundImage: Background image which can be a blurred or a custom background image.
    public func blendWithWithAlphaMask(inputFrameCI: CIImage,
                                       maskImage: CIImage,
                                       backgroundImage: CIImage) -> CIImage? {
        guard let blendFilter = CIFilter(name: "CIBlendWithAlphaMask") else {
            logger.error(msg: "Error creating CIBlendWithAlphaMask CIFilter.")
            return nil
        }

        blendFilter.setValue(backgroundImage, forKey: "inputBackgroundImage")
        blendFilter.setValue(inputFrameCI, forKey: "inputImage")
        blendFilter.setValue(maskImage, forKey: "inputMaskImage")

        // Create the output image.
        guard let outputImage = blendFilter.outputImage else {
            logger.error(msg: "Error creating the blended output image.")
            return nil
        }

        return outputImage
    }

    /// Updates the buffer pool if the previous and new frame dimensions don't match.
    ///
    /// - Parameters:
    ///   - newWidth: New frame width.
    ///   - newHeight: New frame height.
    private func updateBufferPool(newWidth: Int, newHeight: Int) {
        var attributes: [NSString: NSObject] = [:]
        attributes[kCVPixelBufferPixelFormatTypeKey] = NSNumber(value: Int(kCVPixelFormatType_32BGRA))
        attributes[kCVPixelBufferWidthKey] = NSNumber(value: newWidth)
        attributes[kCVPixelBufferHeightKey] = NSNumber(value: newHeight)
        attributes[kCVPixelBufferIOSurfacePropertiesKey] = [AnyHashable: Any]() as NSObject
        CVPixelBufferPoolCreate(nil, nil, attributes as CFDictionary?, &bufferPool)

        bufferPoolWidth = newWidth
        bufferPoolHeight = newHeight
    }

    /// - Returns: Buffer pool used to store the final image data.
    public func getBufferPool() -> CVPixelBufferPool? {
        return bufferPool
    }

    /// Resize CGImage to the given size.
    ///
    /// - Parameters:
    ///   - image: Input image to resize.
    ///   - newSize: Size of the image output.
    ///
    /// - Returns: Resized image.
    private func resizeImage(image: CGImage, newSize: CGSize) -> CGImage? {
        guard let colorSpace = image.colorSpace else {
            logger.error(msg: "Unable to find color space")
            return nil
        }

        let width = Int(newSize.width)
        let height = Int(newSize.height)
        let bitsPerComponent = image.bitsPerComponent
        let bitsPerPixel = image.bitsPerPixel

        // Adjust bitmap info to take into account alpha info.
        var bitmapInfo = image.bitmapInfo
        var alphaInfo = bitmapInfo.rawValue & CGBitmapInfo.alphaInfoMask.rawValue

        // Since iOS 8, we cannot create contexts with unmultiplied alpha info.
        // As a result, if we see a bitmap with an alpha channel that is set to
        // the last, we assume that the alpha has already been premultiplied and
        // do nothing. If the alpha channel is set to first, we similarly still
        // assume it has already been premultiplied and do nothing. We do nothing
        // because the value should not have been there in the first place and we
        // prefer doing nothing over modifying the image erroneously.
        if alphaInfo == CGImageAlphaInfo.last.rawValue {
            alphaInfo = CGImageAlphaInfo.premultipliedLast.rawValue
        } else if alphaInfo == CGImageAlphaInfo.first.rawValue {
            alphaInfo = CGImageAlphaInfo.premultipliedFirst.rawValue
        }

        // Set the new alpha info in case we have changed it.
        // In order to set the value, we take the existing alpha
        // info mask, which is used to extract the value, and invert
        // it instead. This allows us to instead set the bit value
        // with the `alphaInfo` bit. As an example, let's say we
        // have
        //    bitmapInfo    = 0010
        //    alphaInfoMask = 0001
        //    alphaInfo     = 0001
        // In this case, we'd like to set the bit of `0` to the
        // least significant bit. Inverting the `alphaInfoMask`
        // to 1110 keeps all the other bits the same while clearing
        // out the least significant bit for us to set it. In the case
        // that `alphaInfo` is one (as in this case), it will be set to
        // 1. If it was 0, it will also be set to 0 (i.e. no change).
        bitmapInfo = CGBitmapInfo(rawValue: bitmapInfo.rawValue & ~CGBitmapInfo.alphaInfoMask.rawValue | alphaInfo)

        guard let context = CGContext(data: nil,
                                      width: width,
                                      height: height,
                                      bitsPerComponent: bitsPerComponent,
                                      bytesPerRow: width * (bitsPerPixel / bitsPerComponent),
                                      space: colorSpace,
                                      bitmapInfo: bitmapInfo.rawValue) else {
            logger.error(msg: "Unable to create context when resizing image")
            return nil
        }
        context.interpolationQuality = .high
        let rect = CGRect(origin: CGPoint.zero, size: newSize)
        context.draw(image, in: rect)

        return context.makeImage()
    }
}


//
//  NoopSegmentationProcessor.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `NoopSegmentationProcessor` is a processor that does nothing except pass image frames in and out.
/// This is used as a placeholder for implementations of `SegmentationProcessor` that cannot be initialized.
public class NoopSegmentationProcessor: SegmentationProcessor {
    private var buffer: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: 0)

    public func initialize(_ height: Int, width: Int, channels: Int) -> Bool {
        let capacity = height * width * channels
        buffer = UnsafeMutablePointer.allocate(capacity: capacity)
        return true
    }

    public func predict() -> Bool {
        return true
    }

    public func getModelState() -> Int {
        return Int(CwtModelState.LOADED.rawValue)
    }

    public func getInputBuffer() -> UnsafeMutablePointer<UInt8> {
        return buffer
    }

    public func getOutputBuffer() -> UnsafeMutablePointer<UInt8> {
        return buffer
    }
}


//
//  BackgroundBlurConfiguration.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

@objcMembers public class BackgroundBlurConfiguration {
    let blurStrength: BackgroundBlurStrength
    let logger: Logger
    let backgroundFilterProcessor: BackgroundFilterProcessor

    public init(logger: Logger, blurStrength: BackgroundBlurStrength = BackgroundBlurStrength.low) {
        self.blurStrength = blurStrength
        self.logger = logger
        self.backgroundFilterProcessor = BackgroundFilterProcessor(logger: logger)
    }
}


//
//  BackgroundBlurStrength.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Enum defining the different background blur strength options.
@objc public enum BackgroundBlurStrength: Int {
    case low = 10
    case medium = 15
    case high = 25
}


//
//  BackgroundBlurVideoFrameProcessor.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import CoreImage
import UIKit

/// `BackgroundBlurVideoFrameProcessor` is a processor which receives video frames via `VideoSource` and
/// then applies Gaussian blur to each video frame and renders the foreground on top of the blurred image.
/// Gaussian blur is applied using built in `CIGaussianBlur` CIFilter.
@objcMembers public class BackgroundBlurVideoFrameProcessor: VideoSource, VideoSink {
    public var videoContentHint = VideoContentHint.motion

    /// Context used for processing and rendering the final output image.
    private let context = CIContext(options: [.cacheIntermediates: false])

    /// Set of `VideoSource` sinks.
    private let sinks = ConcurrentMutableSet()

    /// Blur strength value to adjust blur intensity.
    private var blurStrength: Int

    /// Background filter processor.
    private let backgroundFilterProcessor: BackgroundFilterProcessor

    /// Logger to log any warnings or errors.
    private let logger: Logger

    /// Public constructor to initialize the processor.
    ///
    /// - Parameters:
    ///   - backgroundBlurConfiguration: `BackgroundBlurConfiguration` class.
    public init(backgroundBlurConfiguration: BackgroundBlurConfiguration) {
        self.blurStrength = backgroundBlurConfiguration.blurStrength.rawValue
        self.backgroundFilterProcessor = backgroundBlurConfiguration.backgroundFilterProcessor
        self.logger = backgroundBlurConfiguration.logger
    }

    /// Receive a video frame from some upstream source. The foreground is segmented and then masked
    /// on top of the blurred background.
    ///
    /// - Parameters:
    ///   - frame: New video frame to consume.
    public func onVideoFrameReceived(frame: VideoFrame) {
        var processedFrame: VideoFrame = frame

        // Update sinks at the end.
        defer {
            updateSinks(frame: processedFrame)
        }

        // Create `VideoFramePixelBuffer` of the input frame.
        guard let pixelBuffer = frame.buffer as? VideoFramePixelBuffer else {
            logger.debug(debugFunction: {
                return "VideoFramePixelBuffer was not found."
            })
            return
        }

        // CIImage of the input pixel buffer.
        let inputFrame = CIImage(cvImageBuffer: pixelBuffer.pixelBuffer)

        guard let inputCgFrame = context.createCGImage(inputFrame, from: inputFrame.extent) else {
            logger.error(msg: "Error creating CGImage of input frame.")
            return
        }

        // Retrieve the foreground alpha mask of the frame.
        guard let foregroundMask = backgroundFilterProcessor.createForegroundAlphaMask(inputFrameCG: inputCgFrame,
                                                                                inputFrameCI: inputFrame)
        else {
            return
        }

        // Blur the input frame with Gaussian Blur which will be used as the background of the final output image.
        let backgroundBlurredImage: CIImage = inputFrame.applyingGaussianBlur(sigma: Double(blurStrength))

        // Create the final output image by blending the alpha mask on top of the input frame to produce
        // the foreground image which is placed on top of the blurred background image.
        guard let outputImage: CIImage = backgroundFilterProcessor.blendWithWithAlphaMask(inputFrameCI: inputFrame,
                                                                                maskImage: foregroundMask,
                                                                                backgroundImage: backgroundBlurredImage)
        else {
            logger.error(msg: "Error producing the final output image.")
            return
        }

        // Create the final `CVPixelBuffer` for the output image.
        var mergedImageBuffer: CVPixelBuffer?
        guard let bufferPool = backgroundFilterProcessor.getBufferPool() else {
            logger.error(msg: "Error retrieving final buffer pool.")
            return
        }
        CVPixelBufferPoolCreatePixelBuffer(nil, bufferPool, &mergedImageBuffer)
        guard let validMergedImageBuffer = mergedImageBuffer else {
            logger.error(msg: "Error creating CVPixelBuffer for output image.")
            return
        }

        // Render the output CGImage image in the buffer.
        context.render(outputImage, to: validMergedImageBuffer)

        processedFrame = VideoFrame(timestampNs: frame.timestampNs,
                                    rotation: frame.rotation,
                                    buffer: VideoFramePixelBuffer(pixelBuffer: validMergedImageBuffer))
    }

    /// Allow builders to change the blur intensity value after initialization.
    ///
    /// - Parameters:
    ///   - newBlurStrength: New blur intensity value.
    public func setBlurStrength(newBlurStrength: BackgroundBlurStrength) {
        blurStrength = newBlurStrength.rawValue
    }

    /// Adds a video sink to the sinks set.
    public func addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    /// Remove a video sink from the sinks set.
    public func removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    /// Update the `VideoSink(s)` with a new frame.
    ///
    /// - Parameters:
    ///    - frame: Next frame to render.
    public func updateSinks(frame: VideoFrame) {
        sinks.forEach({ sink in
            (sink as? VideoSink)?.onVideoFrameReceived(frame: frame)
        })
    }
}


//
//  BackgroundReplacementConfiguration.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation
import UIKit

@objcMembers public class BackgroundReplacementConfiguration {
    let backgroundReplacementImage: UIImage
    let logger: Logger
    let backgroundFilterProcessor: BackgroundFilterProcessor

    public init(logger: Logger, backgroundReplacementImage: UIImage) {
        self.backgroundReplacementImage = backgroundReplacementImage
        self.logger = logger
        self.backgroundFilterProcessor = BackgroundFilterProcessor(logger: logger)
    }
}


//
//  BackgroundReplacementVideoFrameProcessor.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import CoreImage
import Foundation
import UIKit

/// `BackgroundReplacementVideoFrameProcessor` is a processor which receives video frames via `VideoSource`
///  and then creates the foreground image which is rendered on top of a background image.
@objcMembers public class BackgroundReplacementVideoFrameProcessor: VideoSource, VideoSink {
    public var videoContentHint = VideoContentHint.motion

    /// Context used for processing and rendering the final output image.
    private let context = CIContext(options: [.cacheIntermediates: false])

    /// Set of `VideoSource` sinks.
    private let sinks = ConcurrentMutableSet()

    /// Background replacement image.
    private var backgroundReplacementImage: CIImage?

    /// Background filter processor.
    private let backgroundFilterProcessor: BackgroundFilterProcessor

    /// Logger to log any warnings or errors.
    private let logger: Logger

    /// Public constructor to initialize the processor with a `BackgroundReplacementConfiguration`.
    ///
    /// - Parameters:
    ///   - backgroundReplacementConfiguration: `BackgroundReplacementConfiguration` class.
    public init(backgroundReplacementConfiguration: BackgroundReplacementConfiguration) {
        self.backgroundFilterProcessor = backgroundReplacementConfiguration.backgroundFilterProcessor
        self.logger = backgroundReplacementConfiguration.logger

        guard let backgroundReplacementImage = backgroundReplacementConfiguration.backgroundReplacementImage.cgImage
        else {
            logger.error(msg: "Failed to load CGImage of the provided background replacement image.")
            return
        }
        self.backgroundReplacementImage = CIImage(cgImage: backgroundReplacementImage)
    }

    /// Receive a video frame from some upstream source. The foreground is segmented and then masked on top
    /// of the background replacement image.
    ///
    /// - Parameters:
    ///   - frame: New video frame to consume.
    public func onVideoFrameReceived(frame: VideoFrame) {
        var processedFrame: VideoFrame = frame

        // Update sinks at the end.
        defer {
            updateSinks(frame: processedFrame)
        }

        // Create `VideoFramePixelBuffer` of the input frame.
        guard let pixelBuffer = frame.buffer as? VideoFramePixelBuffer else {
            logger.debug(debugFunction: {
                return "VideoFramePixelBuffer was not found."
            })
            return
        }

        // CIImage of the input pixel buffer.
        let inputFrame = CIImage(cvImageBuffer: pixelBuffer.pixelBuffer)

        guard let inputCgFrame = context.createCGImage(inputFrame, from: inputFrame.extent) else {
            logger.error(msg: "Error creating CGImage of input frame.")
            return
        }

        // Retrieve the foreground alpha mask of the frame.
        guard let foregroundMask = backgroundFilterProcessor.createForegroundAlphaMask(inputFrameCG: inputCgFrame,
                                                                                inputFrameCI: inputFrame)
        else {
            return
        }

        guard let backgroundImage = backgroundReplacementImage else {
            logger.error(msg: "Error retrieving the background replacement image.")
            return
        }
        // Create the final output image by blending the alpha mask on top of the input frame to produce
        // the foreground image which is placed on top of the background replacement image.
        guard let outputImage: CIImage = backgroundFilterProcessor.blendWithWithAlphaMask(inputFrameCI: inputFrame,
                                                                                maskImage: foregroundMask,
                                                                                backgroundImage: backgroundImage)
        else {
            logger.error(msg: "Error producing the final output image.")
            return
        }

        // Create the final `CVPixelBuffer` for the output image.
        var mergedImageBuffer: CVPixelBuffer?
        guard let bufferPool = backgroundFilterProcessor.getBufferPool() else {
            logger.error(msg: "Error retrieving final buffer pool.")
            return
        }
        CVPixelBufferPoolCreatePixelBuffer(nil, bufferPool, &mergedImageBuffer)
        guard let validMergedImageBuffer = mergedImageBuffer else {
            logger.error(msg: "Error creating CVPixelBuffer for output image.")
            return
        }

        // Render the output CGImage image in the buffer.
        context.render(outputImage, to: validMergedImageBuffer)

        processedFrame = VideoFrame(timestampNs: frame.timestampNs,
                                    rotation: frame.rotation,
                                    buffer: VideoFramePixelBuffer(pixelBuffer: validMergedImageBuffer))
    }

    /// Allow builders to change background image after initialization.
    ///
    /// - Parameters:
    ///   - newBackgroundReplacementImage: New background replacement image.
    public func setBackgroundImage(newBackgroundReplacementImage: UIImage) {
        guard let backgroundImage = newBackgroundReplacementImage.cgImage else {
            logger.error(msg: "Error trying to change background replacement image.")
            return
        }
        backgroundReplacementImage = CIImage(cgImage: backgroundImage)
    }

    /// Adds a video sink to the sinks set.
    public func addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    /// Remove a video sink from the sinks set.
    public func removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    /// Update the `VideoSink(s)` with a new frame.
    ///
    /// - Parameters:
    ///    - frame: Next frame to render.
    public func updateSinks(frame: VideoFrame) {
        sinks.forEach({ sink in
            (sink as? VideoSink)?.onVideoFrameReceived(frame: frame)
        })
    }
}


//
//  CameraCaptureSource.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `CameraCaptureSource` is an interface for camera capture sources with additional features
/// not covered by `VideoCaptureSource`.
/// All the APIs in this protocol can be called regardless of whether the `MeetingSession.audioVideo` is started or not.
@objc public protocol CameraCaptureSource: VideoCaptureSource {
    /// Current camera device. This is only null if the phone/device doesn't have any cameras
    /// May be called regardless of whether `start` or `stop` has been called.
    var device: MediaDevice? { get set }

    /// Toggle for flashlight on the current device. Will succeed if current device has access to
    /// flashlight, otherwise will stay `false`. May be called regardless of whether `start` or `stop`
    /// has been called.
    var torchEnabled: Bool { get set }

    /// Current camera capture format  Actual format may be adjusted to use supported camera formats.
    /// May be called regardless of whether `start` or `stop` has been called.
    var format: VideoCaptureFormat { get set }

    /// Helper function to switch from front to back cameras or reverse.
    func switchCamera()
}


//
//  CaptureSourceError.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `CaptureSourceError` describes an error resulting from a capture source failure.
/// These can be used to trigger UI, or attempt to restart the capture source.
@objc public enum CaptureSourceError: Int {
    /// Unknown error, and catch-all for errors not otherwise covered.
    case unknown = 0

    /// A  failure observed from a system API used for capturing.
    case systemFailure = 1

    /// A failure observed during configuration.
    case configurationFailure = 2

    /// A temporary failure observed when capture source generates an invalid frame which is ignored.
    case invalidFrame = 3

    public var description: String {
        switch self {
        case .unknown:
            return "unknown"
        case .systemFailure:
            return "systemFailure"
        case .configurationFailure:
            return "configurationFailure"
        case .invalidFrame:
            return "invalidFrame"
        }
    }
}


//
//  CaptureSourceObserver.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `CaptureSourceObserver` observes events resulting from different types of capture devices.
/// Builders may desire this input to decide when to show certain UI elements, or to notify users of failure.
@objc public protocol CaptureSourceObserver {
    /// Called when the capture source has started successfully and has started emitting frames.
    func captureDidStart()

    /// Called when the capture source has stopped when expected. This may occur when switching cameras, for example.
    func captureDidStop()

    /// Called when the capture source failed permanently
    /// - Parameters:
    ///   - error: - The reason why the source has stopped.
    func captureDidFail(error: CaptureSourceError)
}


//
//  DefaultCameraCaptureSource.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import AVFoundation
import Foundation
import UIKit

@objcMembers public class DefaultCameraCaptureSource: NSObject, CameraCaptureSource {
    public var videoContentHint: VideoContentHint = .motion
    private let logger: Logger
    private let cameraLock = NSLock()
    private let deviceType = AVCaptureDevice.DeviceType.builtInWideAngleCamera
    private let sinks = ConcurrentMutableSet()
    private let captureSourceObservers = ConcurrentMutableSet()
    private let output = AVCaptureVideoDataOutput()
    private let captureQueue = DispatchQueue(label: "captureQueue")
    private static let defaultCaptureFormat = VideoCaptureFormat(width: Constants.maxSupportedVideoWidth,
                                                                 height: Constants.maxSupportedVideoHeight,
                                                                 maxFrameRate: Constants.maxSupportedVideoFrameRate)

    private var session = AVCaptureSession()
    private var orientation = UIInterfaceOrientation.portrait
    private var captureDevice: AVCaptureDevice?
    private var eventAnalyticsController: EventAnalyticsController?

    public init(logger: Logger) {
        self.logger = logger
        super.init()

        device = MediaDevice.listVideoDevices().first { mediaDevice in
            mediaDevice.type == MediaDeviceType.videoFrontCamera
        }
        captureDevice = AVCaptureDevice.default(deviceType,
                                               for: .video,
                                               position: .front)

        let notificationCenter = NotificationCenter.default
        notificationCenter.addObserver(self,
                                       selector: #selector(deviceOrientationDidChange),
                                       name: UIDevice.orientationDidChangeNotification,
                                       object: nil)
    }

    deinit {
        if torchEnabled {
            torchEnabled = false
        }
        if session.isRunning {
            session.stopRunning()
        }
        NotificationCenter.default.removeObserver(self)
    }

    public var device: MediaDevice? = MediaDevice.listVideoDevices().first {
        didSet {
            guard let device = device else { return }
            let isUsingFrontCamera = device.type == .videoFrontCamera
            captureDevice = AVCaptureDevice.default(deviceType,
                                                    for: .video,
                                                    position: isUsingFrontCamera ? .front : .back)
            if session.isRunning {
                start() // Restart
            }
        }
    }

    public var format: VideoCaptureFormat = defaultCaptureFormat {
        didSet {
            if captureDevice != nil, session.isRunning {
                start() // Restart
            }
        }
    }

    public var torchEnabled: Bool = false {
        didSet {
            if let captureDevice = captureDevice, torchAvailable {
                do {
                    try captureDevice.lockForConfiguration()
                    if torchEnabled {
                        captureDevice.torchMode = .on
                    } else {
                        captureDevice.torchMode = .off
                    }
                    captureDevice.unlockForConfiguration()
                } catch {
                    logger.error(msg: "Unable to set torch on current camera. Error: \(error.localizedDescription)")
                }
            } else {
                torchEnabled = false
                logger.info(msg: "Torch is not available on current camera.")
            }
        }
    }

    /// Expose current capture device's torch availability
    public var torchAvailable: Bool {
        guard let captureDevice = captureDevice else {
            return false
        }

        return captureDevice.hasTorch && captureDevice.isTorchAvailable
    }

    public func addVideoSink(sink: VideoSink) {
        sinks.add(sink)
    }

    public func removeVideoSink(sink: VideoSink) {
        sinks.remove(sink)
    }

    public func start() {
        cameraLock.lock()
        defer { cameraLock.unlock() }

        session = AVCaptureSession()
        guard let captureDevice = captureDevice else {
            return
        }
        session.beginConfiguration()

        guard let deviceInput = try? AVCaptureDeviceInput(device: captureDevice),
            session.canAddInput(deviceInput) else {
            session.commitConfiguration()
            handleCaptureFailed(reason: .configurationFailure)
            logger.error(msg: "DefaultCameraCaptureSource configuration failure")
            return
        }
        session.addInput(deviceInput)
        updateDeviceCaptureFormat()
        output.setSampleBufferDelegate(self, queue: captureQueue)

        if session.canAddOutput(output) {
            session.addOutput(output)
        } else {
            session.commitConfiguration()
            handleCaptureFailed(reason: .configurationFailure)
            logger.error(msg: "DefaultCameraCaptureSource configuration failure")
            return
        }
        session.commitConfiguration()
        updateOrientation()
        session.startRunning()

        // If the torch was currently on, starting the sessions
        // would turn it off. See if we can turn it back on.
        let currentTorchEnabled = torchEnabled
        self.torchEnabled = currentTorchEnabled

        ObserverUtils.forEach(observers: captureSourceObservers) { (observer: CaptureSourceObserver) in
            observer.captureDidStart()
        }
    }

    public func stop() {
        cameraLock.lock()
        defer { cameraLock.unlock() }

        session.stopRunning()

        // If the torch was currently on, stopping the sessions
        // would turn it off. See if we can turn it back on.
        let currentTorchEnabled = torchEnabled
        self.torchEnabled = currentTorchEnabled

        ObserverUtils.forEach(observers: captureSourceObservers) { (observer: CaptureSourceObserver) in
            observer.captureDidStop()
        }
    }

    public func switchCamera() {
        let isUsingFrontCamera = device?.type == .videoFrontCamera
        device = MediaDevice.listVideoDevices().first { mediaDevice in
            mediaDevice.type == (isUsingFrontCamera ? .videoBackCamera : .videoFrontCamera)
        }

        if device != nil {
            eventAnalyticsController?.pushHistory(historyEventName: .videoInputSelected)
        }
    }

    public func addCaptureSourceObserver(observer: CaptureSourceObserver) {
        captureSourceObservers.add(observer)
    }

    public func removeCaptureSourceObserver(observer: CaptureSourceObserver) {
        captureSourceObservers.remove(observer)
    }

    private func updateOrientation() {
        guard let connection = output.connection(with: AVMediaType.video) else {
            return
        }

        DispatchQueue.main.async {
            self.orientation = UIApplication.shared.statusBarOrientation

            switch self.orientation {
            case .portrait, .unknown:
                connection.videoOrientation = .portrait
            case .portraitUpsideDown:
                connection.videoOrientation = .portraitUpsideDown
            case .landscapeLeft:
                connection.videoOrientation = .landscapeLeft
            case .landscapeRight:
                connection.videoOrientation = .landscapeRight
            @unknown default:
                break
            }
        }
    }

    private func updateDeviceCaptureFormat() {
        guard let captureDevice = captureDevice else {
            return
        }
        // choose a supported format that is closest to `self.format`.
        try? captureDevice.lockForConfiguration()
        let newAVFormat = captureDevice.formats.min { avFormatA, avFormatB in
            let formatA = VideoCaptureFormat.fromAVCaptureDeviceFormat(format: avFormatA)
            let formatB = VideoCaptureFormat.fromAVCaptureDeviceFormat(format: avFormatB)
            return closestFormat(formatA: formatA, formatB: formatB)
        }
        guard let chosenFormat = newAVFormat, chosenFormat != captureDevice.activeFormat else {
            captureDevice.unlockForConfiguration()
            return
        }
        captureDevice.activeFormat = chosenFormat
        captureDevice.unlockForConfiguration()
    }

    func closestFormat(formatA: VideoCaptureFormat, formatB: VideoCaptureFormat) -> Bool {
        let diffA = abs(formatA.width - format.width) + abs(formatA.height - format.height)
        let diffB = abs(formatB.width - format.width) + abs(formatB.height - format.height)
        if diffA == diffB {
            return abs(formatA.maxFrameRate - format.maxFrameRate) < abs(formatB.maxFrameRate - format.maxFrameRate)
        }
        return diffA < diffB
    }

    @objc private func deviceOrientationDidChange(notification: NSNotification) {
        captureQueue.async {
            self.updateOrientation()
        }
    }

    private func handleCaptureFailed(reason: CaptureSourceError) {
        let attributes = [
            EventAttributeName.videoInputError: reason
        ]

        eventAnalyticsController?.publishEvent(name: .videoInputFailed, attributes: attributes)

        ObserverUtils.forEach(observers: captureSourceObservers) { (observer: CaptureSourceObserver) in
            observer.captureDidFail(error: reason)
        }
    }

    public func setEventAnalyticsController(eventAnalyticsController: EventAnalyticsController?) {
        self.eventAnalyticsController = eventAnalyticsController
    }
}

extension DefaultCameraCaptureSource: AVCaptureVideoDataOutputSampleBufferDelegate {
    public func captureOutput(_: AVCaptureOutput,
                              didOutput sampleBuffer: CMSampleBuffer,
                              from _: AVCaptureConnection) {
        guard let frame = VideoFrame(sampleBuffer: sampleBuffer) else {
            handleCaptureFailed(reason: .invalidFrame)
            logger.error(msg: "DefaultCameraCaptureSource could not convert captured CMSampleBuffer to video frame")

            return
        }

        sinks.forEach { item in
            guard let sink = item as? VideoSink else { return }
            sink.onVideoFrameReceived(frame: frame)
        }
    }
}


//
//  VideoCaptureFormat.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation
import AVFoundation

/// `VideoCaptureFormat`describes a given capture format that may be possible to apply to a `VideoCaptureSource`.
/// Note that `VideoCaptureSource` implementations may ignore or adjust unsupported values.
@objcMembers public class VideoCaptureFormat: NSObject {
    /// Capture width in pixels.
    public let width: Int

    /// Capture height in pixels.
    public let height: Int

    /// Max frame rate. When used as input this implies the desired frame rate as well.
    public let maxFrameRate: Int

    public init(width: Int, height: Int, maxFrameRate: Int) {
        self.width = width
        self.height = height
        self.maxFrameRate = maxFrameRate
    }

    override public func isEqual(_ object: Any?) -> Bool {
        guard let object = object as? VideoCaptureFormat else {
            return false
        }
        return width == object.width
            && height == object.height
            && maxFrameRate == object.maxFrameRate
    }

    /// Helper function to convert `AVCaptureDevice.Format` to `VideoCaptureFormat`
    /// - Parameter format: format from the `AVCaptureDevice`
    public static func fromAVCaptureDeviceFormat(format: AVCaptureDevice.Format) -> VideoCaptureFormat {
        let dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription)
        var maxFPS = Constants.maxSupportedVideoFrameRate
        let frameRateRanges = format.videoSupportedFrameRateRanges
        let range = frameRateRanges.min { Int($0.maxFrameRate) < Int($1.maxFrameRate) }
        if let range = range {
            maxFPS = Int(range.maxFrameRate)
        }
        return VideoCaptureFormat(width: Int(dimensions.width),
                                  height: Int(dimensions.height),
                                  maxFrameRate: maxFPS)
    }
}


//
//  VideoCaptureSource.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `VideoCaptureSource` is an interface for various video capture sources (i.e. screen, camera, file) which can emit `VideoFrame` objects.
/// All the APIs in this protocol can be called regardless of whether the `MeetingSession.audioVideo` is started or not.
@objc public protocol VideoCaptureSource: VideoSource {
    /// Start capturing on this source and emitting video frames.
    func start()

    /// Stop capturing on this source and cease emitting video frames.
    func stop()

    /// Add a capture source observer to receive callbacks from the source on lifecycle events
    /// which can be used to trigger UI. This observer is entirely optional.
    /// - Parameters:
    ///   - observer: - New observer.
    func addCaptureSourceObserver(observer: CaptureSourceObserver)

    /// Remove a capture source observer.
    /// - Parameters:
    ///   - observer: - Observer to remove.
    func removeCaptureSourceObserver(observer: CaptureSourceObserver)
}


//
//  DefaultDeviceController.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import AVFoundation

@objcMembers public class DefaultDeviceController: NSObject, DeviceController {
    let videoClientController: VideoClientController
    let logger: Logger
    let audioSession: AudioSession
    let deviceChangeObservers = ConcurrentMutableSet()
    let eventAnalyticsController: EventAnalyticsController

    public init(audioSession: AudioSession,
                videoClientController: VideoClientController,
                eventAnalyticsController: EventAnalyticsController,
                logger: Logger) {
        self.videoClientController = videoClientController
        self.logger = logger
        self.audioSession = audioSession
        self.eventAnalyticsController = eventAnalyticsController
        super.init()
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(handleSystemAudioChange),
                                               name: AVAudioSession.routeChangeNotification,
                                               object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    public func listAudioDevices() -> [MediaDevice] {
        var inputDevices: [MediaDevice] = []
        let loudSpeaker = getSpeakerMediaDevice()
        if let availableInputs = audioSession.availableInputs {
            // On iOS 15, availableInputs returns two entries for bluetooth devices,
            // one with type Unknown.
            // All fields are identical other than uid and portType.
            // This is a workaround to dedup the bluetooth device entry.
            var dedupInputsByName: [String: AVAudioSessionPortDescription] = [:]
            for input in availableInputs {
                if let existedInput = dedupInputsByName[input.portName] {
                    if (existedInput.portType.rawValue == "Unknown") {
                        dedupInputsByName[input.portName] = input
                    }
                } else {
                    dedupInputsByName[input.portName] = input
                }
            }
            inputDevices = dedupInputsByName.values.map { port in MediaDevice.fromAVSessionPort(port: port) }
        }
        // Putting loudSpeaker devices as second element is to align with
        // what apple's AVRoutePickerView will present the list of audio devices:
        // 1. Build-in receiver
        // 2. Build-in loud speaker
        // 3. ...
        inputDevices.insert(loudSpeaker, at: 1)
        return inputDevices
    }

    public func chooseAudioDevice(mediaDevice: MediaDevice) {
        do {
            if mediaDevice.port == nil {
                try audioSession.overrideOutputAudioPort(.speaker)
            } else {
                try audioSession.setPreferredInput(mediaDevice.port)
            }
            eventAnalyticsController.pushHistory(historyEventName: .audioInputSelected)
        } catch {
            logger.error(msg: "Error on setting audio input device: \(error.localizedDescription)")
        }
    }

    @objc private func handleSystemAudioChange(notification: Notification) {
        guard let userInfo = notification.userInfo,
            let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
            let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
        else {
            return
        }

        // Switch over the route change reason.
        switch reason {
        case .newDeviceAvailable, .oldDeviceUnavailable:
            // There are two situation when a device get removed/disconnected
            // 1. The device got removed is not currently using
            // 2. The device got removed is currently using

            // Based on Apple's API of AVAudioSession.routeChangeNotification, The case 1) will not trigger
            // this function.
            DispatchQueue.main.async { [weak self] in
                guard let strongSelf = self else { return }
                var availableDevices = strongSelf.listAudioDevices()
                if reason == .oldDeviceUnavailable {
                    // We need to manually remove the previous input because in some
                    // situation the mediaDevice get disconnected is still showing in .availableInputs

                    // This is an unexpect behavior from Apple's API
                    let oldDevice = userInfo[AVAudioSessionRouteChangePreviousRouteKey]
                        as? AVAudioSessionRouteDescription
                    if let oldDevice = oldDevice, !oldDevice.inputs.isEmpty {
                        availableDevices.removeAll(where: { mediaDevice in
                            mediaDevice.port?.uid == oldDevice.inputs[0].uid
                        })
                    }
                }
                ObserverUtils.forEach(observers: strongSelf.deviceChangeObservers) { (observer: DeviceChangeObserver) in
                    observer.audioDeviceDidChange(freshAudioDeviceList: availableDevices)
                }
            }
        default: ()
        }
    }

    public func addDeviceChangeObserver(observer: DeviceChangeObserver) {
        deviceChangeObservers.add(observer)
    }

    public func removeDeviceChangeObserver(observer: DeviceChangeObserver) {
        deviceChangeObservers.remove(observer)
    }

    public func switchCamera() {
        videoClientController.switchCamera()
    }

    public func getActiveCamera() -> MediaDevice? {
        return videoClientController.getCurrentDevice()
    }

    public func getActiveAudioDevice() -> MediaDevice? {
        // Check for speaker case
        if audioSession.currentRoute.outputs.count > 0 {
            let currentOutput = audioSession.currentRoute.outputs[0]

            if currentOutput.portType == .builtInSpeaker {
                return getSpeakerMediaDevice()
            }
        }
        if audioSession.currentRoute.inputs.count > 0 {
            return MediaDevice.fromAVSessionPort(port: audioSession.currentRoute.inputs[0])
        } else {
            return nil
        }
    }

    private func getSpeakerMediaDevice() -> MediaDevice {
        return MediaDevice(label: "Built-in Speaker", type: MediaDeviceType.audioBuiltInSpeaker)
    }
}


//
//  DeviceChangeObserver.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `DeviceChangeObserver` listens to the change of Audio Device.
@objc public protocol DeviceChangeObserver {
    /// Called when listAudioDevices() output changed
    /// In another word, when a new media device become available
    /// or old media device become unavailable
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter freshAudioDeviceList : updated list of available devices
    func audioDeviceDidChange(freshAudioDeviceList: [MediaDevice])
}


//
//  DeviceController.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `DeviceController` keeps track of the devices being used for audio device
/// (e.g. built-in speaker), video input (e.g. camera)).
/// The list functions return `MediaDevice` objects.
/// Changes in device availability are broadcast to any registered
/// `DeviceChangeObserver`.
@objc public protocol DeviceController {
    /// List available audio devices
    /// - Returns: list of Media Devices
    func listAudioDevices() -> [MediaDevice]

    /// Choose audio devices
    /// - Parameter mediaDevice: the device used as audio route
    func chooseAudioDevice(mediaDevice: MediaDevice)

    /// Add device change observer
    /// - Parameter observer: the object that will receive notification
    func addDeviceChangeObserver(observer: DeviceChangeObserver)

    /// Remove device change observer
    /// - Parameter observer: the object that will be removed
    func removeDeviceChangeObserver(observer: DeviceChangeObserver)

    /// Switch between front/back camera. This will no-op if using a custom source,
    /// e.g. one passed in via `startLocalVideo`
    func switchCamera()

    /// Get the currently active camera, if any. This will return null if using a custom source,
    /// e.g. one passed in via `AudioVideoControllerFacade.startLocalVideo`
    /// - Returns: a media device or nil if no device is present
    func getActiveCamera() -> MediaDevice?

    /// Get currently used audio device
    /// - Returns: a media device or nil if no device is present
    func getActiveAudioDevice() -> MediaDevice?
}


//
//  MediaDevice.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import AmazonChimeSDKMedia
import AVFoundation
import Foundation

/// `MediaDevice` represents an IOS audio/video device.
@objcMembers public class MediaDevice: NSObject {
    /// Label of MediaDevice
    public let label: String

    /// Type of MediaDevice (ex: Bluetooth Audio, Front Camera)
    public let type: MediaDeviceType

    /// Audio Information based on iOS native `AVAudioSessionPortDescription`
    /// It will be null when it represent a video device.
    public let port: AVAudioSessionPortDescription?

    /// Create `MediaDevice` for audio from audio information based on iOS native `AVAudioSessionPortDescription`.
    /// - Parameter port: Audio information
    static func fromAVSessionPort(port: AVAudioSessionPortDescription) -> MediaDevice {
        return MediaDevice(label: port.portName, port: port)
    }

    /// List available video capture devices from the hardware
    public static func listVideoDevices() -> [MediaDevice] {
        let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
                                                                mediaType: .video,
                                                                position: .unspecified)
        return discoverySession.devices.map { device in
            var position = MediaDeviceType.other
            if device.position == .front {
                position = .videoFrontCamera
            } else if device.position == .back {
                position = .videoBackCamera
            }
            return MediaDevice(label: device.localizedName, type: position)
        }
    }

    /// List available `VideoCaptureFormat` from the video capture device.
    /// This methods returns an empty array for `MediaDevice` that's not used for video.
    /// - Parameter mediaDevice: Video capture device to query
    public static func listSupportedVideoCaptureFormats(mediaDevice: MediaDevice) -> [VideoCaptureFormat] {
        guard mediaDevice.type == .videoFrontCamera || mediaDevice.type == .videoBackCamera else {
            return []
        }
        let position: AVCaptureDevice.Position = mediaDevice.type == .videoFrontCamera ? .front : .back
        let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
                                                                mediaType: .video,
                                                                position: position)
        guard let device = discoverySession.devices.first else {
            return []
        }
        var supportedFormats: [VideoCaptureFormat] = []
        for avFormat in device.formats {
            var format = VideoCaptureFormat.fromAVCaptureDeviceFormat(format: avFormat)
            if format.height > Constants.maxSupportedVideoHeight
                || format.width > Constants.maxSupportedVideoWidth {
                continue
            }
            if format.maxFrameRate > Constants.maxSupportedVideoFrameRate {
                format = VideoCaptureFormat(width: format.width,
                                            height: format.height,
                                            maxFrameRate: Constants.maxSupportedVideoFrameRate)
            }
            if !supportedFormats.contains(format) {
                supportedFormats.append(format)
            }
        }
        return supportedFormats
    }

    public init(label: String, type: MediaDeviceType) {
        self.label = label
        self.type = type
        self.port = nil
    }

    public init(label: String, port: AVAudioSessionPortDescription? = nil) {
        self.label = label
        self.port = port
        if let port = port {
            switch port.portType {
            case .bluetoothLE, .bluetoothHFP, .bluetoothA2DP:
                type = .audioBluetooth
            case .builtInReceiver, .builtInMic:
                type = .audioHandset
            case .headphones, .headsetMic:
                type = .audioWiredHeadset
            case .builtInSpeaker:
                type = .audioBuiltInSpeaker
            default:
                type = .other
            }
        } else {
            self.type = .other
        }
    }

    override public var description: String {
        return "\(label) - \(type)"
    }
}


//
//  MediaDeviceType.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

@objc public enum MediaDeviceType: Int, CustomStringConvertible {
    case audioBluetooth
    case audioWiredHeadset
    case audioBuiltInSpeaker
    case audioHandset
    case videoFrontCamera
    case videoBackCamera
    case other

    public var description: String {
        switch self {
        case .audioBluetooth:
            return "audioBluetooth"
        case .audioWiredHeadset:
            return "audioWiredHeadset"
        case .audioBuiltInSpeaker:
            return "audioBuiltInSpeaker"
        case .audioHandset:
            return "audioHandset"
        case .videoFrontCamera:
            return "videoFrontCamera"
        case .videoBackCamera:
            return "videoBackCamera"
        case .other:
            return "other"
        }
    }
}


//
//  AudioClientMetric.swift
//  AmazonChimeSDK
//
//  Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//

import Foundation

// Maps to audio_metric_name_t
enum AudioClientMetric: Int {
    case clientMicDeviceFramesLostPercent = 0
    case serverPreJbMicPacketsLostPercent = 1
    case serverMicMaxJitterMs = 2
    case serverPostJbMic1sPacketsLostPercent = 3
    case serverPostJbMic5sPacketsLostPercent = 4
    case clientPreJbSpkPacketsLostPercent = 5
    case clientSpkMaxJitterMs = 6
    case clientPostJbSpk1sPacketsLostPercent = 7
    case clientPostJbSpk5sPacketsLostPercent = 8
}


//
//  DefaultEventBuffer.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

@objcMembers class DefaultEventBuffer: NSObject, EventBuffer {
    private let ingestionConfiguration: IngestionConfiguration
    private let eventDao: EventDao
    private let converter: IngestionEventConverter
    private let dirtyEventDao: DirtyEventDao
    private let eventSender: EventSender
    private let logger: Logger
    private let eventReporterQueue = DispatchQueue(label: "eventSQLiteQueue",
                                                   qos: .background,
                                                   attributes: .concurrent)
    private let twoDaysInMilliSeconds: Int64 = 172800000

    init(ingestionConfiguration: IngestionConfiguration,
         eventDao: EventDao,
         dirtyEventDao: DirtyEventDao,
         converter: IngestionEventConverter,
         eventSender: EventSender,
         logger: Logger)
    {
        self.ingestionConfiguration = ingestionConfiguration
        self.dirtyEventDao = dirtyEventDao
        self.eventDao = eventDao
        self.converter = converter
        self.eventSender = eventSender
        self.logger = logger
        super.init()
        self.processDirtyEvents()
    }

    func add(item: SDKEvent) {
        let uuid = UUID().uuidString

        let meetingItem = MeetingEventItem(id: uuid,
                                           data: converter.toIngestionMeetingEvent(event: item,
                                                                                ingestionConfiguration: ingestionConfiguration))
        // If there is meeting failure, it is possible that people just close the app.
        // In order to not lose the data, we put it in the db even for immediate events
        _ = eventDao.insertMeetingEvent(event: meetingItem)

        sendIfImmediateEvents(item: item, meetingItem: meetingItem)
    }

    func process() {
        processEvents(meetingEventItems: eventDao.queryMeetingEventItems(size: ingestionConfiguration.flushSize))
    }

    private func sendIfImmediateEvents(item: SDKEvent, meetingItem: MeetingEventItem) {
        let eventName = item.name
        let eventAttributes = item.eventAttributes
        if shouldImmediatelySend(eventName: eventName, eventAttributes: eventAttributes) {
            processEvents(meetingEventItems: [meetingItem])
        }
    }

    private func remove(ids: [String]) {
        if ids.isEmpty {
            return
        }
        _ = eventDao.deleteMeetingEventsByIds(ids: ids)
    }

    private func toDirtyMeetingEventItems(items: [MeetingEventItem]) -> [DirtyMeetingEventItem] {
        let currentTime = DateUtils.getCurrentTimeStampMs()
        return items.map { (item) -> DirtyMeetingEventItem in
            DirtyMeetingEventItem(id: item.id, data: item.data, ttl: currentTime + twoDaysInMilliSeconds)
        }
    }

    func processEvents(meetingEventItems: [MeetingEventItem]) {
        eventReporterQueue.sync {
            let ingestionRecord = self.converter.toIngestionRecord(meetingEvents: meetingEventItems, ingestionConfiguration:ingestionConfiguration)
            if ingestionRecord.events.isEmpty {
                return
            }
            let idsToRemove = ingestionRecord.events.flatMap { (ingestionEvent: IngestionEvent) -> [String] in
                ingestionEvent.payloads.compactMap { (payload) -> String? in
                    payload.id
                }
            }
            self.eventSender.sendEvents(ingestionRecord: ingestionRecord) { isSuccess in
                if !isSuccess {
                    self.logger.info(msg: "Unable to send http request. Putting it in dirty events")
                    _ =  self.dirtyEventDao.insertDirtyMeetingEventItems(dirtyEvents: self.toDirtyMeetingEventItems(items: meetingEventItems))
                }
                self.remove(ids: idsToRemove)
            }
        }
    }

    private func shouldImmediatelySend(eventName: String, eventAttributes: [AnyHashable: Any]) -> Bool {
        if let meetingStatus = eventAttributes[EventAttributeName.meetingStatus] as? MeetingSessionStatusCode {
            return (eventName == String(describing: EventName.meetingFailed) &&
                (meetingStatus == .audioAuthenticationRejected ||
                    meetingStatus == .audioInternalServerError ||
                    meetingStatus == .audioServiceUnavailable ||
                    meetingStatus == .audioDisconnected)) || eventName == String(describing: EventName.meetingEnded)
        }
        return false
    }

    private func processDirtyEvents() {
        eventReporterQueue.sync {
            let dirtyEvents = dirtyEventDao.queryDirtyMeetingEventItems(size: ingestionConfiguration.flushSize)
            let ingestionRecord = converter.toIngestionRecord(dirtyMeetingEvents: dirtyEvents, ingestionConfiguration: ingestionConfiguration)
            if !ingestionRecord.events.isEmpty {
                // Keep sending it until dirtyevents are emtpy
                // if it still fails and ttl is less than current time
                // just delete them
                eventSender.sendEvents(ingestionRecord: ingestionRecord) { isSuccess in
                    if isSuccess {
                        let idsToRemove = ingestionRecord.events.flatMap { (ingestionEvent: IngestionEvent) -> [String] in
                            ingestionEvent.payloads.compactMap { (payload) -> String? in
                                payload.id
                            }
                        }
                        self.removeDirtyEvents(ids: idsToRemove)
                        self.processDirtyEvents()
                    } else {
                        let currentTime = DateUtils.getCurrentTimeStampMs()
                        let idsToRemove = ingestionRecord.events.flatMap { (ingestionEvent: IngestionEvent) -> [String] in
                            ingestionEvent.payloads.filter { (payload) -> Bool in
                                if let ttl = payload.ttl {
                                    return ttl < currentTime
                                }
                                return true
                            }.compactMap { (payload) -> String? in
                                payload.id
                            }
                        }
                        self.removeDirtyEvents(ids: idsToRemove)
                    }
                }
            }
        }
    }

    private func removeDirtyEvents(ids: [String]) {
        if ids.isEmpty {
            return
        }

        _ = dirtyEventDao.deleteDirtyMeetingEventsByIds(ids: ids)
    }
}


//
//  DefaultEventReporter.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

@objcMembers public class DefaultEventReporter: NSObject, EventReporter {
    private let eventBuffer: EventBuffer
    private let ingestionConfiguration: IngestionConfiguration
    private let logger: Logger
    private var timer: Scheduler?
    private var isStarted = false

    init(ingestionConfiguration: IngestionConfiguration,
         eventBuffer: EventBuffer,
         logger: Logger,
         timer: Scheduler? = nil) {
        self.eventBuffer = eventBuffer
        self.ingestionConfiguration = ingestionConfiguration
        self.logger = logger
        self.timer = timer
        super.init()
        start()
    }

    public func report(event: SDKEvent) {
        if ingestionConfiguration.disabled {
            return
        }

        eventBuffer.add(item: event)
    }

    public func start() {
        if isStarted {
            return
        }

        isStarted = true

        if timer == nil {
            timer = IntervalScheduler(
                intervalMs: Int(ingestionConfiguration.flushIntervalMs),
                callback: { [weak self] in
                    guard let strongSelf = self else { return }
                    strongSelf.eventBuffer.process()
                }
            )
        }

        timer?.start()
    }

    public func stop() {
        if !isStarted {
            return
        }

        timer?.stop()

        isStarted = false
    }

    deinit {
        timer?.stop()
    }
}


//
//  DefaultEventSender.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

@objcMembers class DefaultEventSender: EventSender {
    private let ingestionConfiguration: IngestionConfiguration
    private let logger: Logger
    // 408: Request Timeout
    // 429: Too many request
    // 500: Internal Server Error
    // 502: Bad Gateway
    // 503: Service Unavailable
    // 504: Gateway timeout
    private let retryableStatusSet: Set = [408, 429, 500, 502, 503, 504]
    init(ingestionConfiguration: IngestionConfiguration, logger: Logger) {
        self.ingestionConfiguration = ingestionConfiguration
        self.logger = logger
    }

    func sendEvents(ingestionRecord: IngestionRecord, completionHandler: @escaping (Bool) -> Void) {
        do {
            let encodedRecord = try JSONEncoder().encode(ingestionRecord)
            HttpUtils.post(url: ingestionConfiguration.ingestionUrl,
                           jsonData: encodedRecord,
                           logger: logger,
                           httpRetryPolicy: DefaultBackoffRetry(maxRetry: ingestionConfiguration.retryCountLimit,
                                                                   backOffInSeconds: 0,
                                                                   retryableStatusCodes: retryableStatusSet),
                           headers: ["Authorization": "Bearer \(ingestionConfiguration.clientConfiguration.eventClientJoinToken)"]) { _, error in
                if error != nil {
                    completionHandler(false)
                } else {
                    completionHandler(true)
                }
            }
        } catch {
            logger.error(msg: "Unable to encode ingestion \(error.localizedDescription)")
            completionHandler(false)
        }
    }
}


//
//  DefaultMeetingEventReporterFactory.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

@objcMembers public class DefaultMeetingEventReporterFactory: EventReporterFactory {
    private let ingestionConfiguration: IngestionConfiguration
    private let logger: Logger

    public init(ingestionConfiguration: IngestionConfiguration, logger: Logger) {
        self.ingestionConfiguration = ingestionConfiguration
        self.logger = logger
    }

    public func createEventReporter() -> EventReporter? {
        if ingestionConfiguration.disabled {
            return nil
        }

        let converter = IngestionEventConverter()
        let sqliteManager = SQLiteDatabaseManager(sqliteClient: SQLiteClient(databaseName: "AmazonChimeSDKEvents.db",
                                                                             logger: logger))
        let eventDao = EventSQLiteDao(sqliteManager: sqliteManager, logger: logger)
        let dirtyEventDao = DirtyEventSQLiteDao(sqliteManager: sqliteManager, logger: logger)
        let eventSender = DefaultEventSender(ingestionConfiguration: ingestionConfiguration, logger: logger)
        let eventBuffer = DefaultEventBuffer(ingestionConfiguration: ingestionConfiguration,
                                            eventDao: eventDao,
                                            dirtyEventDao: dirtyEventDao,
                                            converter: converter,
                                            eventSender: eventSender,
                                            logger: logger)

        return DefaultEventReporter(ingestionConfiguration: ingestionConfiguration,
                                                 eventBuffer: eventBuffer,
                                                 logger: logger)
    }
}


//
//  EventBuffer.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `EventBuffer` defines storing and consuming of event data.
@objc public protocol EventBuffer {
    /// Add an item.
    /// - Parameter item: item to add
    func add(item: SDKEvent)

    /// Process the data in the buffer
    func process()
}


//
//  EventClientConfiguration.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `EventClientType` defines type of `EventClientConfiguration`
@objc public enum EventClientType: Int, CaseIterable, CustomStringConvertible {
    case meet
    case chat

    public var description: String {
        switch self {
        case .meet:
            return "Meet"
        case .chat:
            return "Chat"
        }
    }
}

/// `EventClientConfiguration` contains speciic data required to send as metadata.
@objc public protocol EventClientConfiguration {

    /**
     The type of the Ingestion event
     - Attention: replaced with `tag`
     */
    var type: EventClientType { get }
    var eventClientJoinToken: String { get }

    /**
     Tagging the source of the events, which will be translated to `Type` for Ingestion event
     */
    var tag: String { get }

    /**
     The attributes that will be sent to Ingestion Service as metadata along with common attributes
     */
    var metadataAttributes: [String: Any] { get }
}


//
//  EventReporter.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `EventReporter` process data. It will be called in `DefaultEventAnalyticsController`.
@objc public protocol EventReporter {
    /// Process the event. For instance, in the default implementation, it will save it to Event Table.
    /// - Parameters:
    ///   - event: SDK related events
    func report(event: SDKEvent)

    /// Start the EventReporter
    func start()

    /// Stop the EventReporter
    func stop()
}


//
//  EventReporterFactory.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

@objc public protocol EventReporterFactory {
    func createEventReporter() -> EventReporter?
}


//
//  EventSender.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `EventSender` handles the sending of ingestion record
@objc public protocol EventSender {

    /// Send events as `IngestionRecord`
    /// - Parameters:
    ///   - ingestionRecord: ingestion record
    ///   - completionHandler: complete handler to execute when send event succeeded or failed
    func sendEvents(ingestionRecord: IngestionRecord, completionHandler: @escaping (Bool) -> Void)
}


//
//  IngestionConfiguration.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `IngestionConfiguration` defines the configuration needed for ingestion service.
/// This will be passed down to `DefaultEventReporter`
@objcMembers public class IngestionConfiguration: NSObject {
    /// Event client configuration specific that has different properties based on type.
    /// For instance, meeting client configuration should have meetingId and attendeeId.
    public let clientConfiguration: EventClientConfiguration
    /// Url of ingestion endpoint to send data.
    public let ingestionUrl: String
    /// Whether ingestion is enabled or disabled.
    public let disabled: Bool
    /// Size to send to the server in a batch.
    /// Constraints:  >= 1 and <=100.
    public let flushSize: Int
    /// Interval to continously send to the server in a batch.
    /// Constraints: >= 300 ms.
    public let flushIntervalMs: Int64
    /// Number of retries.
    /// Constraints:  >= 1 and <= 3.
    public let retryCountLimit: Int

    init(clientConfiguration: EventClientConfiguration,
         ingestionUrl: String,
         disabled: Bool,
         flushSize: Int,
         flushIntervalMs: Int64,
         retryCountLimit: Int) {
        self.clientConfiguration = clientConfiguration
        self.ingestionUrl = ingestionUrl
        self.disabled = disabled
        self.flushSize = min(max(flushSize, 1), 100)
        self.flushIntervalMs = max(flushIntervalMs, 300)
        self.retryCountLimit = min(max(retryCountLimit, 1), 3)
    }
}


//
//  IngestionConfigurationBuilder.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `IngestionConfigurationBuilder` helps to create `IngestionConfiguration`
/// by providing builder pattern.
@objcMembers public class IngestionConfigurationBuilder: NSObject {
    private var flushSize: Int = 20
    private var flushIntervalMs: Int64 = 5000
    private var retryCountLimit: Int = 2

    public override init() {}

    public func setFlushSize(flushSize: Int) -> IngestionConfigurationBuilder {
        self.flushSize = flushSize
        return self
    }

    public func setFlushIntervalMs(flushIntervalMs: Int64) -> IngestionConfigurationBuilder {
        self.flushIntervalMs = flushIntervalMs
        return self
    }

    public func setRetryCountLimit(retryCountLimit: Int) -> IngestionConfigurationBuilder {
        self.retryCountLimit = retryCountLimit
        return self
    }

    public func build(disabled: Bool = true,
                      ingestionUrl: String,
                      clientConiguration: EventClientConfiguration) -> IngestionConfiguration {
        return IngestionConfiguration(clientConfiguration: clientConiguration,
                                      ingestionUrl: ingestionUrl,
                                      disabled: disabled,
                                      flushSize: flushSize,
                                      flushIntervalMs: flushIntervalMs,
                                      retryCountLimit: retryCountLimit)
    }
}


//
//  IngestionEvent.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// Event data that will be send to the ingestion server
@objcMembers public class IngestionEvent: NSObject, Codable {
    /// Event Client Type associated with this event
    public let type: String
    /// metadata that could be overriden
    public let metadata: [String: AnyCodable?]
    /// Payload associated with this ingestion event.
    public let payloads: [IngestionPayload]
    /// Version of payload. Different event format could give different version.
    public let version: Int
    public convenience init(type: String,
                            metadata: [String: AnyCodable?],
                            payloads: [IngestionPayload]) {
        self.init(type: type,
                  metadata: metadata,
                  payloads: payloads,
                  version: 1)
    }

    public init(type: String,
                metadata: [String: AnyCodable?],
                payloads: [IngestionPayload],
                version: Int) {
        self.type = type
        self.metadata = metadata
        self.payloads = payloads
        self.version = version
    }

    enum CodingKeys: String, CodingKey {
        case version = "v"
        case type
        case metadata
        case payloads
    }
}


//
//  IngestionEventConverter.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

private typealias EAName = EventAttributeName

/// `IngestionEventConverter` converts data from payload into `MeetingEventItem`/`DirtyEventItem`or vice versa.
@objcMembers public class IngestionEventConverter: NSObject {

    public override init() {
        super.init()
    }

    func toIngestionMeetingEvent(event: SDKEvent,
                                 ingestionConfiguration: IngestionConfiguration) -> IngestionMeetingEvent {
        let eventAttributes = event.eventAttributes
        let clientConfig = ingestionConfiguration.clientConfiguration

        var meetingStatus: String?
        if let status = eventAttributes[EventAttributeName.meetingStatus] as? MeetingSessionStatusCode {
            meetingStatus = String(describing: status)
        }

        var videoErrorStr: String?
        if let videoError = eventAttributes[EventAttributeName.videoInputError] as? CaptureSourceError {
            videoErrorStr = String(describing: videoError)
        }

        var attributes = [String:AnyCodable]()
        attributes[EAName.timestampMs.description] = AnyCodable(eventAttributes[EAName.timestampMs])
        attributes[EAName.maxVideoTileCount.description] = AnyCodable(eventAttributes[EAName.maxVideoTileCount])
        attributes[EAName.meetingStartDurationMs.description] = AnyCodable(
            eventAttributes[EAName.meetingStartDurationMs]
        )
        attributes[EAName.meetingDurationMs.description] = AnyCodable(eventAttributes[EAName.meetingDurationMs])
        attributes[EAName.meetingErrorMessage.description] = AnyCodable(eventAttributes[EAName.meetingErrorMessage])
        attributes[EAName.meetingStatus.description] = AnyCodable(meetingStatus)
        attributes[EAName.poorConnectionCount.description] = AnyCodable(eventAttributes[EAName.poorConnectionCount])
        attributes[EAName.retryCount.description] = AnyCodable(eventAttributes[EAName.retryCount])
        attributes[EAName.videoInputError.description] = AnyCodable(videoErrorStr)

        clientConfig.metadataAttributes.forEach({ (key: String, value: Any) in
            attributes[key] = AnyCodable(value)
        })

        // Some meta data like meetingId is needed
        // Since these meta data changes from meeting to meeting
        return IngestionMeetingEvent(name: String(describing: event.name),
                                     eventAttributes: attributes)
    }

    func toIngestionRecord(dirtyMeetingEvents: [DirtyMeetingEventItem], ingestionConfiguration: IngestionConfiguration) -> IngestionRecord {
        if dirtyMeetingEvents.isEmpty {
            return IngestionRecord(metadata: [:], events: [])
        }

        // TODO: Group by meeting ID won't work since attendees can rejoin with different attendeeIDs
        let dirtyMeetingEventsGrouped = Dictionary(grouping: dirtyMeetingEvents, by: { $0.data.getMeetingId() })

        let eventType = ingestionConfiguration.clientConfiguration.tag

        let ingestionEvents = dirtyMeetingEventsGrouped.compactMap { (group) -> IngestionEvent? in
            if let sampleMeetingEventItem = group.value.first {
                // TODO: Using first event as sample for retrieveing metadata won't always work, since attendees can rejoin with different attendeeIDs
                let metadata = toIngestionMetadata(ingestionMeetingEvent: sampleMeetingEventItem.data,
                                                   clientConfig: ingestionConfiguration.clientConfiguration)
                let payloads = group.value.map {
                    toIngestionPayload(meetingEventItemId: $0.id,
                                       meetingEvent: $0.data,
                                       dirtyMeetingEventTtl: $0.ttl)
                }
                return IngestionEvent(type: eventType,
                                      metadata: metadata,
                                      payloads: payloads)
            }
            return nil
        }

        let rootMeta = toIngestionMetadata(attributes: EventAttributeUtils.getCommonAttributes(ingestionConfiguration: ingestionConfiguration))

        return IngestionRecord(metadata: rootMeta,
                               events: ingestionEvents)
    }

    func toIngestionRecord(meetingEvents: [MeetingEventItem], ingestionConfiguration: IngestionConfiguration) -> IngestionRecord {
        if meetingEvents.isEmpty {
            return IngestionRecord(metadata: [:], events: [])
        }
        // TODO: Group by meeting ID won't work since attendees can rejoin with different attendeeID
        let meetingEventsByMeetingId = Dictionary(grouping: meetingEvents,
                                                  by: { $0.data.getMeetingId() })

        let type = ingestionConfiguration.clientConfiguration.tag

        // When sending a batch, server accepts the data as
        // "events": [
        //   {
        //      "metadata": { "meetingId": "meetingId2" } // This overrides record level metadata
        //      "type": "Meet",
        //      "v": 1,
        //      "payloads": []
        //   },
        //   {
        //      "metadata": { "meetingId": "meetingId1" } // This overrides record level metadata
        //      "type": "Meet",
        //      "v": 1,
        //      "payloads": []
        //   },
        // ]
        // This is to group events so that it contains different metadata to override
        let ingestionEvents = meetingEventsByMeetingId.compactMap { (group) -> IngestionEvent? in
            if let sampleMeetingEventItem = group.value.first {
                // TODO: Using first event as sample for retrieveing metadata won't always work, since attendees can rejoin with different attendeeIDs
                let metadata = toIngestionMetadata(ingestionMeetingEvent: sampleMeetingEventItem.data,
                                                   clientConfig: ingestionConfiguration.clientConfiguration)
                let payloads = group.value.map {
                    toIngestionPayload(meetingEventItemId: $0.id,
                                       meetingEvent: $0.data,
                                       dirtyMeetingEventTtl: nil)
                }
                return IngestionEvent(type: type,
                                      metadata: metadata,
                                      payloads: payloads)
            }
            return nil
        }

        let rootMeta = toIngestionMetadata(attributes: EventAttributeUtils.getCommonAttributes(ingestionConfiguration: ingestionConfiguration))

        return IngestionRecord(metadata: rootMeta,
                               events: ingestionEvents)
    }

    private func toIngestionPayload(meetingEventItemId: String,
                                    meetingEvent: IngestionMeetingEvent,
                                    dirtyMeetingEventTtl: Int64?) -> IngestionPayload {
        return IngestionPayload(name: meetingEvent.name,
                                ts: meetingEvent.getTimestampMs() ?? 0,
                                id: meetingEventItemId,
                                maxVideoTileCount: meetingEvent.getMaxVideoTileCount(),
                                meetingStartDurationMs: meetingEvent.getMeetingStartDurationMs(),
                                meetingDurationMs: meetingEvent.getMeetingDurationMs(),
                                meetingErrorMessage: meetingEvent.getMeetingErrorMessage(),
                                meetingStatus: meetingEvent.getMeetingStatus(),
                                poorConnectionCount: meetingEvent.getPoorConnectionCount(),
                                retryCount: meetingEvent.getRetryCount(),
                                videoInputErrorMessage: meetingEvent.getVideoInputErrorMessage(),
                                ttl: dirtyMeetingEventTtl)
    }

    private func toIngestionMetadata(attributes: [String: Any]) -> [String: AnyCodable?] {
        var result = [String: AnyCodable]()
        attributes.forEach { (key: String, value: Any) in
            result[key] = AnyCodable(value)
        }
        return result
    }

    private func toIngestionMetadata(ingestionMeetingEvent: IngestionMeetingEvent,
                                     clientConfig: EventClientConfiguration) -> [String: AnyCodable?] {
        var result = [String: AnyCodable]()
        clientConfig.metadataAttributes.keys.forEach { key in
            result[key] = ingestionMeetingEvent.eventAttributes[key] ?? nil
        }
        return result
    }
}


//
//  IngestionPayload.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

@objcMembers public class IngestionPayload: NSObject, Codable {
    public let name: String
    public let ts: Int64
    public let id: String?
    public let maxVideoTileCount: Int?
    public let meetingStartDurationMs: Int64?
    public let meetingDurationMs: Int64?
    public let meetingErrorMessage: String?
    public let meetingStatus: String?
    public let poorConnectionCount: Int?
    public let retryCount: Int?
    public let videoInputErrorMessage: String?
    public let ttl: Int64?

    public init(name: String,
                ts: Int64,
                id: String? = nil,
                maxVideoTileCount: Int? = nil,
                meetingStartDurationMs: Int64? = nil,
                meetingDurationMs: Int64? = nil,
                meetingErrorMessage: String? = nil,
                meetingStatus: String? = nil,
                poorConnectionCount: Int? = nil,
                retryCount: Int? = nil,
                videoInputErrorMessage: String? = nil,
                ttl: Int64? = nil) {
        self.name = name
        self.ts = ts
        self.id = id
        self.maxVideoTileCount = maxVideoTileCount
        self.meetingStartDurationMs = meetingStartDurationMs
        self.meetingDurationMs = meetingDurationMs
        self.meetingErrorMessage = meetingErrorMessage
        self.meetingStatus = meetingStatus
        self.poorConnectionCount = poorConnectionCount
        self.retryCount = retryCount
        self.videoInputErrorMessage = videoInputErrorMessage
        self.ttl = ttl
    }
}


//
//  IngestionRecord.swift
//  AmazonChimeSDK
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//

import Foundation

/// `IngestionRecord` is the format of data that will be consumed on the ingestion server.
@objcMembers public class IngestionRecord: NSObject, Codable {
    /// Metadata associated with the event. This includes deviceName, OSVersion, and etc.
    public let metadata: [String: AnyCodable?]
    /// List of `IngestionEvent`
    public let events: [IngestionEvent]

    public init(metadata: [String: AnyCodable?], events: [IngestionEvent]) {
        self.events = events
        self.metadata = metadata
    }
}