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

import Foundation
import CoreImage
import UIKit

/// Utils to help converting image byte array to CGImage and CGImage to byte array.
public class ImageConversionUtils {

    /// A utility function that converts a byte array to a CGImage.
    ///
    /// - Parameters:
    ///   - raw: Pointer to the byte array of the image.
    ///   - frameWidth: Image width.
    ///   - frameHeight: Image height.
    ///   - bytesPerPixel: Total bytes per pixel (color space).
    ///   - bitsPerComponent: Total bits per component.
    ///
    /// - Returns: A CGImage of the input byte array.
    public class func byteArrayToCGImage(raw: UnsafeMutablePointer<UInt8>,
                                         frameWidth: Int,
                                         frameHeight: Int,
                                         bytesPerPixel: Int,
                                         bitsPerComponent: Int) -> CGImage? {

        let bytesPerRow = frameWidth * bytesPerPixel
        let size = frameWidth * frameHeight * bytesPerPixel

        guard let imageCfData = CFDataCreate(nil, raw, size) else {
            return nil
        }

        guard let cgProvider = CGDataProvider.init(data: imageCfData) else {
            return nil
        }

        return CGImage.init(width: frameWidth,
                            height: frameHeight,
                            bitsPerComponent: bitsPerComponent,
                            bitsPerPixel: bytesPerPixel * bitsPerComponent,
                            bytesPerRow: bytesPerRow,
                            space: CGColorSpaceCreateDeviceRGB(),
                            bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.last.rawValue),
                            provider: cgProvider,
                            decode: nil,
                            shouldInterpolate: true,
                            intent: CGColorRenderingIntent.defaultIntent)
    }

    /// A utility function that converts a CGImage to a byte array.
    ///
    /// - Parameters:
    ///   - cgImage: The CGImage to convert.
    ///
    /// - Returns: A UInt8 byte array of the CGImage.
    public class func cgImageToByteArray(cgImage: CGImage) -> [UInt8]? {
        let height = cgImage.height
        let width = cgImage.width

        let bitsPerComponent = cgImage.bitsPerComponent
        let bytesPerRow = cgImage.bytesPerRow
        let size = height * bytesPerRow

        var byteArray: [UInt8] = [UInt8](repeating: 0, count: size)

        guard let contextRef = CGContext(data: &byteArray,
                                         width: width,
                                         height: height,
                                         bitsPerComponent: bitsPerComponent,
                                         bytesPerRow: bytesPerRow,
                                         space: CGColorSpaceCreateDeviceRGB(),
                                         bitmapInfo: cgImage.bitmapInfo.rawValue)
        else {
            return nil
        }
        contextRef.draw(cgImage, in: CGRect(x: 0.0, y: 0.0, width: CGFloat(width), height: CGFloat(height)))

        return byteArray
    }
}


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

import Foundation

@objcMembers class ObserverUtils: NSObject {
    public static func forEach<T>(
        observers: ConcurrentMutableSet,
        observerFunction: @escaping (_ observer: T) -> Void
    ) {
        DispatchQueue.main.async {
            observers.forEach { observer in
                if let observer = observer as? T {
                    observerFunction(observer)
                }
            }
        }
    }
}


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

import AmazonChimeSDKMedia
import Foundation
import UIKit

@objcMembers public class TURNRequestService: NSObject {
    private static let contentTypeHeader = "Content-Type"
    private static let contentType = "application/json"
    private static let userAgentTypeHeader = "User-Agent"
    private static let meetingIdKey = "meetingId"
    private static let tokenHeader = "X-Chime-Auth-Token"
    private static let tokenKey = "_aws_wt_session"
    private static let turnRequestHttpMethod = "POST"

    static func postTURNRequest(meetingId: String,
                                turnControlUrl: String,
                                joinToken: String,
                                logger: Logger,
                                completion: @escaping (MeetingSessionTURNCredentials?) -> Void) {
        guard let turnRequest = constructTURNRequest(meetingId: meetingId,
                                                     turnControlUrl: turnControlUrl,
                                                     joinToken: joinToken) else {
            logger.error(msg: "Failed to construct TURN request")
            completion(nil)
            return
        }
        let session = URLSession(configuration: .default)
        session.dataTask(with: turnRequest) { data, resp, error in
            if let error = error {
                logger.error(msg: "Failed to make TURN request, error: \(error.localizedDescription)")
                completion(nil)
                return
            }
            if let httpResponse = resp as? HTTPURLResponse {
                guard httpResponse.statusCode == 200 else {
                    logger.error(msg: "Received status code \(httpResponse.statusCode) when making TURN request")
                    completion(nil)
                    return
                }
            }
            if let turnCredentials = processTurnResponse(data: data) {
                completion(turnCredentials)
            } else {
                logger.error(msg: "Failed to decode TURN response")
                completion(nil)
            }
        }.resume()
    }

    private static func constructTURNRequest(meetingId: String,
                                             turnControlUrl: String,
                                             joinToken: String) -> URLRequest? {
        guard let serverUrl = URL(string: turnControlUrl) else {
            return nil
        }
        var request = URLRequest(url: serverUrl)
        request.httpMethod = turnRequestHttpMethod
        request.addValue("\(tokenKey)=\(joinToken)", forHTTPHeaderField: tokenHeader)
        request.addValue(contentType, forHTTPHeaderField: contentTypeHeader)
        request.addValue(getUserAgent(), forHTTPHeaderField: userAgentTypeHeader)

        // Write meetingId into HTTP request body
        let meetingIdDict = [meetingIdKey: meetingId]
        do {
            request.httpBody = try JSONSerialization.data(withJSONObject: meetingIdDict)
        } catch {
            return nil
        }
        return request
    }

    private static func processTurnResponse(data: Data?) -> MeetingSessionTURNCredentials? {
        guard let data = data else { return nil }
        let jsonDecoder = JSONDecoder()
        do {
            let turnCredentials: MeetingSessionTURNCredentials = try jsonDecoder.decode(
                MeetingSessionTURNCredentials.self, from: data
            )
            return turnCredentials
        } catch {
            return nil
        }
    }

    private static func getUserAgent() -> String {
        let model = UIDevice.current.model
        let systemVersion = UIDevice.current.systemVersion
        let scaleFactor = UIScreen.main.scale
        let defaultAgent = "(\(model); iOS \(systemVersion); Scale/\(String(format: "%.2f", scaleFactor)))"
        if let dict = Bundle.main.infoDictionary {
            if let identifier = dict[kCFBundleExecutableKey as String] ?? dict[kCFBundleIdentifierKey as String],
                let version = dict[kCFBundleVersionKey as String] {
                return "\(identifier)/\(version) \(defaultAgent)"
            }
        }
        return defaultAgent
    }
}


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

import Foundation

protocol BackoffRetry {
    func calculateBackOff() -> Int
    func getRetryCount() -> Int
    func isRetryCountLimitReached() -> Bool
    func incrementRetryCount()
    func isRetryableCode(responseCode: Int) -> Bool
}


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

import Foundation

class DefaultBackoffRetry: BackoffRetry {
    private let maxRetry: Int
    private let backOffInSeconds: Int
    private let retryableStatusCodes: Set<Int>

    private var retryCount = 0
    private var multiplier = 2

    init(maxRetry: Int = 0,
         backOffInSeconds: Int = 0,
         retryableStatusCodes: Set<Int> = []) {
        self.retryableStatusCodes = retryableStatusCodes
        self.backOffInSeconds = backOffInSeconds
        self.maxRetry = maxRetry
    }

    func calculateBackOff() -> Int {
        // Making sure it does not exceed 2 hours
        return backOffInSeconds * Int(min(7200.0, max(pow(Double(multiplier), Double(retryCount)), 0.0)))
    }

    func getRetryCount() -> Int {
        return retryCount
    }

    func isRetryCountLimitReached() -> Bool {
        return maxRetry > retryCount
    }

    func incrementRetryCount() {
        retryCount += 1
    }

    func isRetryableCode(responseCode: Int) -> Bool {
        return retryableStatusCodes.isEmpty || retryableStatusCodes.contains(responseCode)
    }
}


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

import Foundation

enum HttpMethod: String, CustomStringConvertible {
    case post
    case put
    case get
    case delete

    public var description: String {
        switch self {
        case .post:
            return "post"
        case .put:
            return "put"
        case .get:
            return "get"
        case .delete:
            return "delete"
        }
    }
}


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

import Foundation

// Every Error is NSError
// We can obtain code from NSError as statusCode
typealias CompletionFunc = (Data?, Error?) -> Void

class HttpUtils {
    private static let tag = "HttpUtils"
    private static let domain = "AmazonChimeSDK"
    public class func post(url: String,
                           jsonData: Data? = nil,
                           logger: Logger? = nil,
                           httpRetryPolicy: BackoffRetry = DefaultBackoffRetry(),
                           headers: [String: String] = [:],
                           // https://developer.apple.com/documentation/foundation/urlsession/1409000-shared
                           urlSession: URLSessionProtocol = URLSession(configuration: .default),
                           completion: @escaping CompletionFunc) {
        makePostRequest(url: url,
                        method: .post,
                        jsonData: jsonData,
                        logger: logger,
                        headers: headers,
                        urlSession: urlSession) { data, error in
            if error == nil ||
                !httpRetryPolicy.isRetryableCode(responseCode: (error as NSError?)?.code ?? 0) ||
                httpRetryPolicy.isRetryCountLimitReached() {
                completion(data, error)
                return
            } else {
                httpRetryPolicy.incrementRetryCount()
                DispatchQueue.main.asyncAfter(deadline: .now() + DispatchTimeInterval.seconds(httpRetryPolicy.calculateBackOff())) {
                    post(url: url, jsonData: jsonData, logger: logger, httpRetryPolicy: httpRetryPolicy, urlSession: urlSession, completion: completion)
                }
            }
        }
    }

    private class func makePostRequest(url: String,
                                       method: HttpMethod,
                                       jsonData: Data?,
                                       logger: Logger?,
                                       headers: [String: String],
                                       urlSession: URLSessionProtocol,
                                       completion: @escaping CompletionFunc)
    {
        guard let serverUrl = URL(string: url) else {
            let msg = "Invalid url parameter"
            HttpUtils.printError(logger: logger, msg: msg)
            completion(nil, NSError(domain: HttpUtils.domain, code: 0, userInfo: [
                NSLocalizedDescriptionKey: msg
            ]))
            return
        }

        var request = URLRequest(url: serverUrl)
        request.httpMethod = String(describing: method)

        if let data = jsonData, method == .post {
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            for (key, value) in headers {
                request.addValue(value, forHTTPHeaderField: key)
            }
            request.httpBody = data
        }

        urlSession.dataTask(with: request) { data, resp, error in
            if let error = error {
                HttpUtils.printError(logger: logger, msg: error.localizedDescription)
                completion(nil, error)
                return
            }
            guard let httpResponse = resp as? HTTPURLResponse else {
                completion(data, nil)
                return
            }

            guard 200 ... 299 ~= httpResponse.statusCode else {
                HttpUtils.printError(logger: logger, msg: "Received status code \(httpResponse.statusCode)")
                completion(nil, NSError(domain: HttpUtils.domain, code: httpResponse.statusCode, userInfo: nil))
                return
            }
            guard let data = data else {
                // No data, but no error
                completion(nil, nil)
                return
            }
            completion(data, nil)
        }.resume()
    }

    private static func printError(logger: Logger?, msg: String) {
        logger?.error(msg: "\(HttpUtils.tag): \(msg)")
    }
}


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

import Foundation

protocol URLSessionProtocol {
    func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable(Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}

extension URLSession: URLSessionProtocol {}


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

import AmazonChimeSDKMedia
import Foundation

class DefaultVideoClient: VideoClient {
    private let logger: Logger

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

        super.init()
    }

    override func videoLogCallBack(_ logLevel: video_client_loglevel_t, msg: String?) {
        guard let msg = msg else { return }
        switch logLevel.rawValue {
        case Constants.errorLevel, Constants.fatalLevel:
            logger.error(msg: msg)
        default:
            logger.default(msg: msg)
        }
    }
}


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

import AmazonChimeSDKMedia
import AVFoundation
import Foundation
import UIKit

class DefaultVideoClientController: NSObject {
    var clientMetricsCollector: ClientMetricsCollector
    var logger: Logger
    var videoClient: VideoClientProtocol?
    var videoSourceAdapter = VideoSourceAdapter()
    var videoClientState: VideoClientState = .uninitialized
    let videoTileControllerObservers = ConcurrentMutableSet()
    let videoObservers = ConcurrentMutableSet()
    var dataMessageObservers = [String: ConcurrentMutableSet]()
    // We have designed the SDK API to allow using `RemoteVideoSource` as a key in a map, e.g. for  `updateVideoSourceSubscription`.
    // Therefore we need to map to a consistent set of sources from the internal sources, by using attendeeId as a unique identifier.
    var cachedRemoteVideoSources = ConcurrentMutableSet()
    var primaryMeetingPromotionObserver: PrimaryMeetingPromotionObserver?

    private let configuration: MeetingSessionConfiguration
    private let defaultVideoClient: VideoClientProtocol

    // This internal camera capture source is used for `startLocalVideo()` API without parameter.
    private let internalCaptureSource: DefaultCameraCaptureSource
    private var isInternalCaptureSourceRunning = true
    private let eventAnalyticsController: EventAnalyticsController

    init(videoClient: VideoClientProtocol,
         clientMetricsCollector: ClientMetricsCollector,
         configuration: MeetingSessionConfiguration,
         logger: Logger,
         eventAnalyticsController: EventAnalyticsController) {
        self.defaultVideoClient = videoClient
        self.clientMetricsCollector = clientMetricsCollector
        self.configuration = configuration
        self.logger = logger
        self.internalCaptureSource = DefaultCameraCaptureSource(logger: logger)
        self.internalCaptureSource.setEventAnalyticsController(eventAnalyticsController: eventAnalyticsController)
        self.eventAnalyticsController = eventAnalyticsController
        super.init()
    }

    // MARK: VideoClientController

    private func checkVideoPermission() throws {
        if AVCaptureDevice.authorizationStatus(for: .video) != .authorized {
            let attributes = [
                EventAttributeName.videoInputError: PermissionError.videoPermissionError
            ]
            eventAnalyticsController.publishEvent(name: .videoInputFailed, attributes: attributes)
            throw PermissionError.videoPermissionError
        }
    }

    private func stopVideoClient() {
        logger.info(msg: "Stopping VideoClient")
        videoClient?.stop()
        videoClientState = .stopped
        // SDK owns the lifecycle of the internal capture source
        stopInternalCaptureSourceIfRunning()
    }

    private func destroyVideoClient() {
        logger.info(msg: "VideoClient is being destroyed")
        videoTileControllerObservers.removeAll()
        videoClient?.delegate = nil
        videoClient = nil
        videoClientState = .uninitialized
    }

    func initialize() {
        guard videoClientState == .uninitialized else {
            logger.info(msg: "VideoClientState is not UNINITIALIZED, no need to initialize again")
            return
        }
        logger.info(msg: "Initializing VideoClient")
        videoClient = defaultVideoClient
        videoClient?.delegate = self
        videoClientState = .initialized
    }

    func startInitializedVideoClient() {
        guard let videoClient = videoClient else {
            logger.error(msg: "VideoClient is not initialized properly")
            return
        }
        logger.info(msg: "Starting VideoClient")

        let videoConfig: VideoConfiguration = VideoConfiguration()
        videoConfig.isUsing16by9AspectRatio = true
        videoConfig.isUsingSendSideBwe = true
        videoConfig.isDisablingSimulcastP2P = true
        videoConfig.isUsingPixelBufferRenderer = true
        videoConfig.isUsingOptimizedTwoSimulcastStreamTable = true
        videoConfig.isExcludeSelfContentInIndex = true
        videoConfig.isUsingInbandTurnCreds = true

        // Default to idle mode, no video but signaling connection is
        // established for messaging
        videoClient.setReceiving(false)

        videoClient.start(configuration.meetingId,
                          token: configuration.credentials.joinToken,
                          sending: false,
                          config: videoConfig,
                          appInfo: DeviceUtils.getDetailedInfo(),
                          signalingUrl: configuration.urls.signalingUrl)
        videoClientState = .started
    }
}

// MARK: - VideoClientDelegate

extension DefaultVideoClientController: VideoClientDelegate {
    func didReceive(_ buffer: CVPixelBuffer?,
                    profileId: String?,
                    pauseState: PauseState,
                    videoId: UInt32,
                    timestampNs: Int64,
                    rotation: VideoRotationInternal) {
        // Translate the Obj-C enum to the public Swift enum
        var translatedPauseState: VideoPauseState
        switch pauseState {
        case .Unpaused:
            translatedPauseState = .unpaused
        case .PausedByUserRequest:
            translatedPauseState = .pausedByUserRequest
        case .PausedForPoorConnection:
            translatedPauseState = .pausedForPoorConnection
        default:
            translatedPauseState = .unpaused
        }

        var frame: VideoFrame?
        if let buffer = buffer {
            let pixelBuffer = VideoFramePixelBuffer(pixelBuffer: buffer)
            frame = VideoFrame(timestampNs: timestampNs,
                               rotation: VideoRotation(internalValue: rotation),
                               buffer: pixelBuffer)
        }
        ObserverUtils.forEach(observers: videoTileControllerObservers) { (observer: VideoTileController) in
            observer.onReceiveFrame(frame: frame,
                                    videoId: Int(videoId),
                                    attendeeId: profileId,
                                    pauseState: translatedPauseState)
        }
    }

    // swiftlint:enable function_parameter_count
    public func videoClientIsConnecting(_ client: VideoClient?) {
        logger.info(msg: "videoClientIsConnecting")
        ObserverUtils.forEach(observers: videoObservers) { (observer: AudioVideoObserver) in
            observer.videoSessionDidStartConnecting()
        }
    }

    public func videoClientDidConnect(_ client: VideoClient?, controlStatus: Int32) {
        logger.info(msg: "videoClientDidConnect, \(controlStatus)")
        ObserverUtils.forEach(observers: videoObservers) { (observer: AudioVideoObserver) in
            switch Int(controlStatus) {
            case Constants.videoClientStatusCallAtCapacityViewOnly:
                observer.videoSessionDidStartWithStatus(
                    sessionStatus: MeetingSessionStatus(statusCode: MeetingSessionStatusCode.videoAtCapacityViewOnly)
                )
            default:
                observer.videoSessionDidStartWithStatus(sessionStatus:
                    MeetingSessionStatus(statusCode: MeetingSessionStatusCode.ok))
            }
        }
    }

    public func videoClientDidFail(_ client: VideoClient?, status: video_client_status_t, controlStatus: Int32) {
        logger.info(msg: "videoClientDidFail")
        ObserverUtils.forEach(observers: videoObservers) { (observer: AudioVideoObserver) in
            observer.videoSessionDidStopWithStatus(sessionStatus:
                MeetingSessionStatus(statusCode: .videoServiceUnavailable))
        }
    }

    public func videoClientDidStop(_ client: VideoClient?) {
        logger.info(msg: "videoClientDidStop")
        ObserverUtils.forEach(observers: videoObservers) { (observer: AudioVideoObserver) in
            observer.videoSessionDidStopWithStatus(sessionStatus: MeetingSessionStatus(statusCode: .ok))
        }

        // We will not be promoted on reconnection
        self.primaryMeetingPromotionObserver?
            .didDemoteFromPrimaryMeeting(status: MeetingSessionStatus.init(statusCode: MeetingSessionStatusCode.audioInternalServerError))
        self.primaryMeetingPromotionObserver = nil
    }

    public func videoClient(_ client: VideoClient?, cameraSendIsAvailable available: Bool) {
        logger.info(msg: "videoClientCameraSendIsAvailable \(available)")
        ObserverUtils.forEach(observers: videoObservers) { (observer: AudioVideoObserver) in
            observer.cameraSendAvailabilityDidChange(
                available: available
            )
        }
    }

    public func videoClientRequestTurnCreds(_ videoClient: VideoClient?) {
        let turnControlUrl = configuration.urls.turnControlUrl
        let joinToken = configuration.credentials.joinToken
        let meetingId = configuration.meetingId
        let signalingUrl = configuration.urls.signalingUrl

        logger.info(msg: "Requesting TURN creds")

        TURNRequestService.postTURNRequest(meetingId: meetingId,
                                           turnControlUrl: turnControlUrl,
                                           joinToken: joinToken,
                                           logger: logger) { [weak self] turnCredentials in
            if let strongSelf = self, let turnCredentials = turnCredentials {
                let turnResponse = turnCredentials.toTURNSessionResponse(urlRewriter: strongSelf.configuration.urlRewriter,
                                                                         signalingUrl: signalingUrl)
                (strongSelf.videoClient as? VideoClient)?.updateTurnCreds(turnResponse, turn: VIDEO_CLIENT_TURN_FEATURE_ON)
            }
        }
    }

    public func videoClientMetricsReceived(_ metrics: [AnyHashable: Any]?) {
        guard let metrics = metrics else { return }
        clientMetricsCollector.processVideoClientMetrics(metrics: metrics)
    }

    public func videoClientDataMessageReceived(_ messages: [DataMessageInternal]?) {
        guard let messages = messages else { return }
        for message in messages {
            let dataMessage = DataMessage(message: message)
            if let observers = dataMessageObservers[dataMessage.topic] {
                ObserverUtils.forEach(observers: observers) { (observer: DataMessageObserver) in
                    observer.dataMessageDidReceived(dataMessage: dataMessage)
                }
            }
        }
    }

    public func remoteVideoSourcesDidBecomeAvailable(_ sourcesInternal: [RemoteVideoSourceInternal]) {
        if sourcesInternal.isEmpty { return } // Don't callback for empty lists

        var sources = [RemoteVideoSource]()
        sourcesInternal.forEach { source in
            var foundCachedRemoteVideoSource = false
            cachedRemoteVideoSources.forEach { cachedRemoteVideoSource in
                if let cachedRemoteVideoSource = cachedRemoteVideoSource as? RemoteVideoSource {
                    if source.attendeeId == cachedRemoteVideoSource.attendeeId {
                        sources.append(cachedRemoteVideoSource)
                        foundCachedRemoteVideoSource = true
                    }
                }
            }

            if foundCachedRemoteVideoSource {
                return
            }
            // Otherwise create a new one and add to cached set
            let newSource = RemoteVideoSource()
            newSource.attendeeId = source.attendeeId
            sources.append(newSource)
            cachedRemoteVideoSources.add(newSource)
        }
        ObserverUtils.forEach(observers: videoObservers) { (observer: AudioVideoObserver) in
            observer.remoteVideoSourcesDidBecomeAvailable(sources: sources)
        }
    }

    public func remoteVideoSourcesDidBecomeUnavailable(_ sourcesInternal: [RemoteVideoSourceInternal]) {
        if sourcesInternal.isEmpty { return } // Don't callback for empty lists
        var sourcesToRemove = [RemoteVideoSource]()
        sourcesInternal.forEach { source in
            cachedRemoteVideoSources.forEach { cachedRemoteVideoSource in
                if let cachedRemoteVideoSource = cachedRemoteVideoSource as? RemoteVideoSource {
                    if source.attendeeId == cachedRemoteVideoSource.attendeeId {
                        sourcesToRemove.append(cachedRemoteVideoSource)
                    }
                }
            }
        }
        sourcesToRemove.forEach { sourceToRemove in
            cachedRemoteVideoSources.remove(sourceToRemove)
        }
        ObserverUtils.forEach(observers: videoObservers) { (observer: AudioVideoObserver) in
            observer.remoteVideoSourcesDidBecomeUnavailable(sources: sourcesToRemove)
        }
    }

    public func videoClientTurnURIsReceived(_ uris: [String]) -> [String] {
        return uris.map(self.configuration.urlRewriter)
    }

    public func videoClientDidPromote(toPrimaryMeeting status: video_client_status_t) {
        let code: MeetingSessionStatusCode
        switch status {
        case VIDEO_CLIENT_OK:
            code = MeetingSessionStatusCode.ok
        case VIDEO_CLIENT_ERR_PRIMARY_MEETING_JOIN_AT_CAPACITY:
            code = MeetingSessionStatusCode.audioCallAtCapacity
        case VIDEO_CLIENT_ERR_PRIMARY_MEETING_JOIN_AUTHENTICATION_FAILED:
            code = MeetingSessionStatusCode.audioAuthenticationRejected
        default:
            code = MeetingSessionStatusCode.unknown
        }
        self.primaryMeetingPromotionObserver?
            .didPromoteToPrimaryMeeting(status: MeetingSessionStatus.init(statusCode: code))
    }

    public func videoClientDidDemote(fromPrimaryMeeting status: video_client_status_t) {
        let code: MeetingSessionStatusCode
        switch status {
        case VIDEO_CLIENT_OK:
            code = MeetingSessionStatusCode.ok
        case VIDEO_CLIENT_ERR_PRIMARY_MEETING_JOIN_AUTHENTICATION_FAILED:
            code = MeetingSessionStatusCode.audioAuthenticationRejected
        default:
            code = MeetingSessionStatusCode.unknown
        }
        self.primaryMeetingPromotionObserver?
            .didDemoteFromPrimaryMeeting(status: MeetingSessionStatus.init(statusCode: code))
        self.primaryMeetingPromotionObserver = nil
    }
}

extension DefaultVideoClientController: VideoClientController {
    // MARK: - Lifecycle: start and initialize

    public func start() {
        switch videoClientState {
        case .uninitialized:
            initialize()
            startInitializedVideoClient()
        case .started:
            logger.info(msg: "VideoClientState is already STARTED")
        case .initialized, .stopped:
            startInitializedVideoClient()
        }
    }

    // MARK: - Lifecycle: stop and destroy

    public func stopAndDestroy() {
        DispatchQueue.global().async {
            switch self.videoClientState {
            case .uninitialized:
                self.logger.info(msg: "VideoClient is uninitialized so cannot be stopped and destroyed")
            case .started:
                self.stopVideoClient()
                self.destroyVideoClient()
            case .initialized, .stopped:
                self.destroyVideoClient()
            }
        }
    }

    // MARK: - Video selection

    public func startLocalVideo() throws {
        let config = LocalVideoConfiguration()
        try startLocalVideo(config: config)
    }

    public func startLocalVideo(config: LocalVideoConfiguration) throws {
        try checkVideoPermission()
        logger.info(msg: "Starting local video with internal source and config")
        setVideoSource(source: internalCaptureSource, config: config)
        internalCaptureSource.start()
        isInternalCaptureSourceRunning = true
    }

    public func startLocalVideo(source: VideoSource) {
        let config = LocalVideoConfiguration()
        startLocalVideo(source: source, config: config)
    }

    public func startLocalVideo(source: VideoSource, config: LocalVideoConfiguration) {
        stopInternalCaptureSourceIfRunning()
        setVideoSource(source: source, config: config)

        logger.info(msg: "Starting local video with custom source and custom config")
    }

    private func stopInternalCaptureSourceIfRunning() {
        if isInternalCaptureSourceRunning {
            internalCaptureSource.stop()
            isInternalCaptureSourceRunning = false
        }
    }

    private func setVideoSource(source: VideoSource, config: LocalVideoConfiguration) {
        guard videoClientState != .uninitialized else {
            logger.fault(msg: "VideoClient is not initialized so returning without doing anything")
            return
        }

        videoSourceAdapter.source = source
        videoClient?.setExternalVideoSource(videoSourceAdapter)
        videoClient?.setSending(true)

        let simulcastEnabled = config.simulcastEnabled
        logger.info(msg: "Setting simulcast")
        videoClient?.setSimulcast(simulcastEnabled)

        if (config.maxBitRateKbps > 0) {
            logger.info(msg: "Setting max bit rate in kbps for local video")
            videoClient?.setMaxBitRateKbps(config.maxBitRateKbps)
        }
    }

    public func stopLocalVideo() {
        guard videoClientState != .uninitialized else {
            logger.fault(msg: "VideoClient is not initialized so returning without doing anything")
            return
        }
        logger.info(msg: "Stopping local video")
        videoClient?.setSending(false)
        stopInternalCaptureSourceIfRunning()
    }

    public func startRemoteVideo() {
        guard videoClientState != .uninitialized else {
            logger.fault(msg: "VideoClient is not initialized so returning without doing anything")
            return
        }
        logger.info(msg: "Starting remote video")
        videoClient?.setReceiving(true)
    }

    public func stopRemoteVideo() {
        guard videoClientState != .uninitialized else {
            logger.fault(msg: "VideoClient is not initialized so returning without doing anything")
            return
        }
        logger.info(msg: "Stopping remote video")
        videoClient?.setReceiving(false)
    }

    public func switchCamera() {
        if isInternalCaptureSourceRunning {
            logger.info(msg: "Switching camera on internal source")
            internalCaptureSource.switchCamera()
        }
    }

    public func getCurrentDevice() -> MediaDevice? {
        if isInternalCaptureSourceRunning {
            return internalCaptureSource.device
        }
        return nil
    }

    public func getConfiguration() -> MeetingSessionConfiguration {
        return configuration
    }

    public func subscribeToVideoClientStateChange(observer: AudioVideoObserver) {
        videoObservers.add(observer)
    }

    public func unsubscribeFromVideoClientStateChange(observer: AudioVideoObserver) {
        videoObservers.remove(observer)
    }

    public func subscribeToVideoTileControllerObservers(observer: VideoTileController) {
        videoTileControllerObservers.add(observer)
    }

    public func unsubscribeFromVideoTileControllerObservers(observer: VideoTileController) {
        videoTileControllerObservers.remove(observer)
    }

    public func pauseResumeRemoteVideo(_ videoId: UInt32, pause: Bool) {
        logger.info(msg: "pauseResumeRemoteVideo")
        videoClient?.setRemotePause(videoId, pause: pause)
    }

    public func updateVideoSourceSubscriptions(addedOrUpdated: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration>, removed: Array<RemoteVideoSource>) {
        guard videoClientState != .uninitialized else {
            logger.fault(msg: "VideoClient is not initialized so returning without doing anything")
            return
        }
        logger.info(msg: "Updating video subscriptions")

        let addedOrUpdatedInternal = Dictionary(uniqueKeysWithValues:
            addedOrUpdated.map { source, config in
                (RemoteVideoSourceInternal(attendeeId: source.attendeeId),
                 VideoSubscriptionConfigurationInternal(
                    priority: PriorityInternal(rawValue: UInt(config.priority.rawValue)) ?? PriorityInternal.highest,
                    targetResolution: ResolutionInternal.init(width: Int32(config.targetResolution.width),
                                                              height: Int32(config.targetResolution.height))))
        })

        var removedInternal = [RemoteVideoSourceInternal]()
        removed.forEach{ source in
            removedInternal.append(RemoteVideoSourceInternal(attendeeId: source.attendeeId))
        }

        videoClient?.updateVideoSourceSubscriptions(addedOrUpdatedInternal as Dictionary<AnyHashable, Any>, withRemoved: removedInternal as [Any])
    }

    public func subscribeToReceiveDataMessage(topic: String, observer: DataMessageObserver) {
        if dataMessageObservers[topic] == nil {
            dataMessageObservers[topic] = ConcurrentMutableSet()
        }
        dataMessageObservers[topic]?.add(observer)
    }

    public func unsubscribeFromReceiveDataMessageFromTopic(topic: String) {
        dataMessageObservers.removeValue(forKey: topic)
    }

    public func sendDataMessage(topic: String, data: Any, lifetimeMs: Int32 = 0) throws {
        guard videoClientState == .started else {
            logger.error(msg: "Cannot send data message because videoClientState=\(videoClientState.description)")
            return
        }

        if lifetimeMs < 0 {
            throw SendDataMessageError.negativeLifetimeParameter
        }

        if topic.range(of: Constants.dataMessageTopicRegex, options: .regularExpression) == nil {
            throw SendDataMessageError.invalidTopic
        }

        var dataContainer: Data?
        if let dataAsString = data as? String {
            dataContainer = dataAsString.data(using: .utf8)
        } else if let dataAsByteArray = data as? [UInt8] {
            dataContainer = Data(dataAsByteArray)
        } else if JSONSerialization.isValidJSONObject(data) {
            dataContainer = try JSONSerialization.data(withJSONObject: data)
        } else if let dataAsData = data as? Data {
            dataContainer = dataAsData
        } else {
            throw SendDataMessageError.invalidData
        }

        if let container: Data = dataContainer {
            if container.count > Constants.dataMessageMaxDataSizeInByte {
                throw SendDataMessageError.invalidDataLength
            }
            container.withUnsafeBytes { dataBytes in
                let buffer: UnsafePointer<Int8> = dataBytes.baseAddress!.assumingMemoryBound(to: Int8.self)

                videoClient?.sendDataMessage(topic,
                                             data: buffer,
                                             dataLen: UInt32(container.count),
                                             lifetimeMs: lifetimeMs)
            }
        } else {
            throw SendDataMessageError.invalidData
        }
    }

    func promoteToPrimaryMeeting(credentials: MeetingSessionCredentials,
                                  observer: PrimaryMeetingPromotionObserver) {
        guard videoClientState == .started else {
            logger.error(msg: "Cannot join primary meeting because videoClientState=\(videoClientState)")
            observer.didPromoteToPrimaryMeeting(status: MeetingSessionStatus(statusCode: MeetingSessionStatusCode.audioServiceUnavailable))
            return
        }
        primaryMeetingPromotionObserver = observer
        videoClient?.promotePrimaryMeeting(credentials.attendeeId,
                                             externalUserId: credentials.externalUserId,
                                             joinToken: credentials.joinToken)
    }

    func demoteFromPrimaryMeeting() {
        guard videoClientState == .started else {
            logger.error(msg: "Cannot leave primary meeting because videoClientState=\(videoClientState)")
            return
        }
        videoClient?.demoteFromPrimaryMeeting()
    }
}


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

import AmazonChimeSDKMedia
import Foundation

@objc public protocol VideoClientController {
    func start()
    func stopAndDestroy()
    func startLocalVideo() throws
    func startLocalVideo(config: LocalVideoConfiguration) throws
    func startLocalVideo(source: VideoSource)
    func startLocalVideo(source: VideoSource, config: LocalVideoConfiguration)
    func stopLocalVideo()
    func startRemoteVideo()
    func stopRemoteVideo()
    func switchCamera()
    func getCurrentDevice() -> MediaDevice?
    func getConfiguration() -> MeetingSessionConfiguration
    func subscribeToVideoClientStateChange(observer: AudioVideoObserver)
    func unsubscribeFromVideoClientStateChange(observer: AudioVideoObserver)
    func subscribeToVideoTileControllerObservers(observer: VideoTileController)
    func unsubscribeFromVideoTileControllerObservers(observer: VideoTileController)
    func pauseResumeRemoteVideo(_ videoId: UInt32, pause: Bool)
    func subscribeToReceiveDataMessage(topic: String, observer: DataMessageObserver)
    func unsubscribeFromReceiveDataMessageFromTopic(topic: String)
    func sendDataMessage(topic: String, data: Any, lifetimeMs: Int32) throws
    func updateVideoSourceSubscriptions(addedOrUpdated: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration>, removed: Array<RemoteVideoSource>)
    func promoteToPrimaryMeeting(credentials: MeetingSessionCredentials,
                                 observer: PrimaryMeetingPromotionObserver)
    func demoteFromPrimaryMeeting()
}


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

import AmazonChimeSDKMedia
import Foundation

@objc public protocol VideoClientProtocol {
    var delegate: VideoClientDelegate! { get set }

    static func globalInitialize()

    func start(_ callId: String!,
               token: String!,
               sending: Bool,
               config: VideoConfiguration!,
               appInfo: app_detailed_info_t,
               signalingUrl: String!)

    func start(_ callId: String!,
               token: String!,
               sending: Bool,
               config: VideoConfiguration!,
               appInfo: app_detailed_info_t)

    func stop()

    func setSending(_ sending: Bool)

    func setReceiving(_ receiving: Bool)

    func setExternalVideoSource(_ source: VideoSourceInternal!)

    func getServiceType() -> video_client_service_type_t

    func setRemotePause(_ video_id: UInt32, pause: Bool)

    func videoLogCallBack(_ logLevel: video_client_loglevel_t, msg: String!)

    func sendDataMessage(_ topic: String!, data: UnsafePointer<Int8>!, dataLen: UInt32, lifetimeMs: Int32)

    func updateVideoSourceSubscriptions(_ addedOrUpdated: [AnyHashable: Any]!,
                                        withRemoved: [Any]!)

    func promotePrimaryMeeting(_ attendeeId: String!, externalUserId: String!, joinToken: String!)

    func demoteFromPrimaryMeeting()

    func setSimulcast(_ simulcast: Bool)

    func setMaxBitRateKbps(_ maxBitRate: UInt32)
}

extension VideoClient: VideoClientProtocol {}


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

import Foundation

@objc enum VideoClientState: Int32 {
    case uninitialized = -1
    case initialized = 0
    case started = 1
    case stopped = 2

    var description: String {
        switch self {
        case .uninitialized:
            return "uninitialized"
        case .initialized:
            return "initialized"
        case .started:
            return "started"
        case .stopped:
            return "stopped"
        }
    }
}


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

import Foundation
import CoreMedia

/// `VideoFrameResender` contains logic to resend video frames as needed to maintain a minimum frame rate
/// This can be useful with sources which may pause the generation of frames (like in-app ReplayKit screen sharing)
/// so that internally encoders don't get in a poor state, and new receivers can immediately receive frames
@objcMembers public class VideoFrameResender: NSObject {
    private let minFrameRate: UInt
    private let resendQueue = DispatchQueue.global()
    // Will be nil until the first frame is sent, and nil again on stop
    private var resendTimer: DispatchSourceTimer?
    // Cached constant values
    private let resendTimeInterval: CMTime
    private let resendScheduleLeewayMs = DispatchTimeInterval.milliseconds(20)

    private var lastSendTimestamp: CMTime?
    private var lastVideoFrame: VideoFrame?

    private let resendFrameHandler: (VideoFrame) -> Void

    /// Callback will be triggered with a previous `VideoFrame` which will have different timestamp
    /// than originally sent with so it won't be dropped by downstream encoders
    init(minFrameRate: UInt, resendFrameHandler: @escaping (VideoFrame) -> Void) {
        self.minFrameRate = minFrameRate
        self.resendFrameHandler = resendFrameHandler
        self.resendTimeInterval = CMTime(value: CMTimeValue(Constants.millisecondsPerSecond / Int(minFrameRate)),
                                      timescale: CMTimeScale(Constants.millisecondsPerSecond))
    }

    func stop() {
        resendTimer?.cancel()
        resendTimer = nil
    }

    /// Calling this function will kick off a timer which will begin checking if frames need to be resent
    /// to maintain a minimum frame frame
    func frameDidSend(videoFrame: VideoFrame) {
        lastSendTimestamp = CMClockGetTime(CMClockGetHostTimeClock())
        lastVideoFrame = videoFrame

        if let resendTimer = resendTimer,
           resendTimer.isCancelled == false {
            // There is already a timer running
            return
        }
        let timer = DispatchSource.makeTimerSource(flags: .strict, queue: resendQueue)
        resendTimer = timer

        // This timer is invoked every resendTimeInterval when no frame is sent from video source
        timer.setEventHandler(handler: { [weak self] in
            guard let `self` = self else {
                timer.cancel()
                return
            }

            guard let lastSendTimestamp = self.lastSendTimestamp,
                  let lastVideoFrame = self.lastVideoFrame else { return }

            let currentTimestamp = CMClockGetTime(CMClockGetHostTimeClock())
            let delta = CMTimeSubtract(currentTimestamp, lastSendTimestamp)

            // Resend the last input frame if there is no new input frame after resendTimeInterval
            if delta > self.resendTimeInterval {
                // Update the timestamp so it's not dropped by downstream as a duplicate
                let lastVideoFrameTime = CMTimeMake(value: lastVideoFrame.timestampNs,
                                                    timescale: Int32(Constants.nanosecondsPerSecond))
                let newVideoFrame = VideoFrame(timestampNs: Int64(CMTimeAdd(lastVideoFrameTime, delta).seconds
                                                                    * Double(Constants.nanosecondsPerSecond)),
                                               rotation: lastVideoFrame.rotation,
                                               buffer: lastVideoFrame.buffer)

                // Cancel the current timer so that new frames will kick it off again
                self.resendTimer?.cancel()
                self.resendTimer = nil

                self.resendFrameHandler(newVideoFrame)
            } else {
                // Reset resending schedule if there is an input frame between internals
                let remainingSeconds = self.resendTimeInterval.seconds - delta.seconds
                let deadline = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(remainingSeconds *
                                                           Double(Constants.millisecondsPerSecond)))
                self.resendTimer?.schedule(deadline: deadline, leeway: self.resendScheduleLeewayMs)
            }

        })

        let deadline = DispatchTime.now()
            + DispatchTimeInterval.milliseconds(Constants.millisecondsPerSecond / Int(minFrameRate))
        timer.schedule(deadline: deadline, leeway: resendScheduleLeewayMs)
        timer.activate()
    }
}


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

import AmazonChimeSDKMedia
import CoreMedia
import Foundation

/// `VideoSourceAdapter` provides two classes to adapt `VideoSource` to `VideoSourceInternal`.
class VideoSourceAdapter: NSObject, VideoSink, VideoSourceInternal {
    var contentHint = VideoContentHintInternal.none

    private let sinks = ConcurrentMutableSet()

    private var currentSource: VideoSource?

    var source: VideoSource? {
        get { return currentSource }
        set(newSource) {
            currentSource?.removeVideoSink(sink: self)
            newSource?.addVideoSink(sink: self)
            currentSource = newSource
            contentHint = newSource?.videoContentHint.toInternal ?? VideoContentHintInternal.none
        }
    }

    override init() {
        super.init()
    }

    func onVideoFrameReceived(frame: VideoFrame) {
        guard let buffer = frame.buffer as? VideoFramePixelBuffer else {
            return
        }
        sinks.forEach { item in
            guard let sink = item as? VideoSinkInternal else { return }
            sink.didReceive(buffer.pixelBuffer,
                            timestampNs: Int64(frame.timestampNs),
                            rotation: frame.rotation.toInternal)
        }
    }

    func addVideoSink(_ sink: VideoSinkInternal?) {
        if let sink = sink {
            sinks.add(sink)
        }
    }

    func removeVideoSink(_ sink: VideoSinkInternal?) {
        if let sink = sink {
            sinks.remove(sink)
        }
    }
}


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

import Foundation

/// `DataMessasgeObserver` handles data message event,
@objc public protocol DataMessageObserver {
    /// Handles data message receive event
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter dataMessage: The data message received
    func dataMessageDidReceived(dataMessage: DataMessage)
}


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

import Foundation

@objcMembers public class DefaultRealtimeController: NSObject, RealtimeControllerFacade {
    private let audioClientController: AudioClientController
    private let audioClientObserver: AudioClientObserver
    private let videoClientController: VideoClientController

    public init(audioClientController: AudioClientController,
                audioClientObserver: AudioClientObserver,
                videoClientController: VideoClientController) {
        self.audioClientController = audioClientController
        self.audioClientObserver = audioClientObserver
        self.videoClientController = videoClientController
    }

    public func realtimeLocalMute() -> Bool {
        return audioClientController.setMute(mute: true)
    }

    public func realtimeLocalUnmute() -> Bool {
        return audioClientController.setMute(mute: false)
    }

    public func addRealtimeObserver(observer: RealtimeObserver) {
        audioClientObserver.subscribeToRealTimeEvents(observer: observer)
    }

    public func removeRealtimeObserver(observer: RealtimeObserver) {
        audioClientObserver.unsubscribeFromRealTimeEvents(observer: observer)
    }

    public func addRealtimeDataMessageObserver(topic: String, observer: DataMessageObserver) {
        videoClientController.subscribeToReceiveDataMessage(topic: topic, observer: observer)
    }

    public func removeRealtimeDataMessageObserverFromTopic(topic: String) {
        videoClientController.unsubscribeFromReceiveDataMessageFromTopic(topic: topic)
    }

    public func realtimeSendDataMessage(topic: String, data: Any, lifetimeMs: Int32 = 0) throws {
        try videoClientController.sendDataMessage(topic: topic, data: data, lifetimeMs: lifetimeMs)
    }

    public func realtimeSetVoiceFocusEnabled(enabled: Bool) -> Bool {
        return audioClientController.setVoiceFocusEnabled(enabled: enabled)
    }

    public func realtimeIsVoiceFocusEnabled() -> Bool {
        return audioClientController.isVoiceFocusEnabled()
    }

    public func addRealtimeTranscriptEventObserver(observer: TranscriptEventObserver) {
        audioClientObserver.subscribeToTranscriptEvent(observer: observer)
    }

    public func removeRealtimeTranscriptEventObserver(observer: TranscriptEventObserver) {
        audioClientObserver.unsubscribeFromTranscriptEvent(observer: observer)
    }
}


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

import Foundation

/// `RealtimeControllerFacade` controls aspects meetings concerning realtime UX
/// that for performance, privacy, or other reasons should be implemented using
/// the most direct path. Callbacks generated by this interface should be
/// consumed synchronously and without business logic dependent on the UI state
/// where possible.
///
/// Events will be passed through `RealtimeObserver`, which in turn provides consumers the
/// volume/mute/signal/attendee callbacks that can be used to render in the UI.
@objc public protocol RealtimeControllerFacade {
    /// Mutes the audio input.
    ///
    /// - Returns: Whether mute was successful
    func realtimeLocalMute() -> Bool

    /// Unmutes the audio input if currently allowed
    ///
    /// - Returns: Whether unmute was successful
    func realtimeLocalUnmute() -> Bool

    /// Subscribes to real time events with an observer
    ///
    /// - Parameter observer: Observer that handles real time events
    func addRealtimeObserver(observer: RealtimeObserver)

    /// Unsubscribes from real time events by removing the specified observer
    ///
    /// - Parameter observer: Observer that handles real time events
    func removeRealtimeObserver(observer: RealtimeObserver)

    /// Subscribes to data meesage event with an observer
    ///
    /// - Parameter topic: Topic to handle
    /// - Parameter observer: Observer that handles data message event with given topic
    func addRealtimeDataMessageObserver(topic: String, observer: DataMessageObserver)

    /// Unsubscribes from data meesage event by removing the specified observer by topic
    ///
    /// - Parameter topic: Topic to remove
    func removeRealtimeDataMessageObserverFromTopic(topic: String)

    /// Send arbitrary data to given topic with given lifetime ms (5 mins max)
    ///
    /// - Parameter topic: Topic to send
    /// - Parameter data: Data to send, data can be a String, a ByteArray, or a JSON serializable object
    /// - Parameter lifetimeMs: Message lifetime in milisecond, 5 mins max, default 0
    /// - Throws: SendDataMessageError
    func realtimeSendDataMessage(topic: String, data: Any, lifetimeMs: Int32) throws

    /// Enable or disable Amazon Voice Focus (ML-based noise suppression) on the audio input
    ///
    /// Note: Only call this API after audioClient starts. Calling it before that results in a no-op. Amazon Voice Focus is disabled by default when audioClient starts.
    ///
    /// - Parameter enabled: A `Bool` value, where `true` to enable; `false` to disable
    /// - Returns: Whether the enable/disable action was successful
    func realtimeSetVoiceFocusEnabled(enabled: Bool) -> Bool

    /// Check if Amazon Voice Focus (ML-based noise suppression) is enabled or not
    ///
    /// - Returns: `true` if Amazon Voice Focus is enabled; `false` if Amazon Voice Focus is not enabled, or the audio session was not started yet
    func realtimeIsVoiceFocusEnabled() -> Bool

    /// Subscribe to live transcription events with an observer
    ///
    /// - Parameter observer: Observer that handles live transcription events
    @objc optional func addRealtimeTranscriptEventObserver(observer: TranscriptEventObserver)

    /// Unsubscribes from live transcription events by removing the specified observer
    ///
    /// - Parameter observer: Observer that handles live transcription events
    @objc optional func removeRealtimeTranscriptEventObserver(observer: TranscriptEventObserver)
}


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

import Foundation

/// `RealtimeObserver` handles event that happens in realtime,
///  such as delta in attendees join or leave, volume/signal status.
@objc public protocol RealtimeObserver {
    /// Handles volume changes for attendees
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter volumeUpdates: An array of VolumeUpdates
    func volumeDidChange(volumeUpdates: [VolumeUpdate])

    /// Handles signal strength changes for attendees
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter signalUpdates: An array of SignalUpdates
    func signalStrengthDidChange(signalUpdates: [SignalUpdate])

    /// List attendees that are newly added to the meeting
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter attendeeInfo: an array of AttendeeInfo added
    func attendeesDidJoin(attendeeInfo: [AttendeeInfo])

    /// List attendees that left the meeting
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter attendeeInfo: an array of AttendeeInfo who left
    func attendeesDidLeave(attendeeInfo: [AttendeeInfo])

    /// List attendees that got dropped from the meeting due to network
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter attendeeInfo: an array of AttendeeInfo who are dropped
    func attendeesDidDrop(attendeeInfo: [AttendeeInfo])

    /// List attendees that are newly muted in the meeting
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter attendeeInfo: an array of AttendeeInfo newly muted
    func attendeesDidMute(attendeeInfo: [AttendeeInfo])

    /// List attendees that newly unmuted from the meeting
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter attendeeInfo: an array of AttendeeInfo newly unmuted
    func attendeesDidUnmute(attendeeInfo: [AttendeeInfo])
}


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

import Foundation

/// `TranscriptEventObserver` provides a callback to handle transcript event
@objc public protocol TranscriptEventObserver {
    /// Gets triggered when a transcript event is received
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter transcriptEvent: The transcript event received
    func transcriptEventDidReceive(transcriptEvent: TranscriptEvent)
}


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

import Foundation

@objcMembers public class CreateAttendeeResponse: NSObject {
    let attendee: Attendee

    public init(attendee: Attendee) {
        self.attendee = attendee
    }
}

@objcMembers public class Attendee: NSObject {
    let attendeeId: String
    let externalUserId: String
    let joinToken: String

    public init(attendeeId: String, externalUserId: String, joinToken: String) {
        self.attendeeId = attendeeId
        self.externalUserId = externalUserId
        self.joinToken = joinToken
    }
}


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

import Foundation

@objcMembers public class CreateMeetingResponse: NSObject {
    let meeting: Meeting

    public init(meeting: Meeting) {
        self.meeting = meeting
    }
}

@objcMembers public class Meeting: NSObject {
    let externalMeetingId: String?
    let mediaPlacement: MediaPlacement
    let mediaRegion: String
    let meetingId: String
    let primaryMeetingId: String?

    public convenience init(externalMeetingId: String?,
                            mediaPlacement: MediaPlacement,
                            mediaRegion: String,
                            meetingId: String) {
        self.init(
            externalMeetingId: externalMeetingId,
            mediaPlacement: mediaPlacement,
            mediaRegion: mediaRegion,
            meetingId: meetingId,
            primaryMeetingId: nil)
    }

    public init(externalMeetingId: String?,
                 mediaPlacement: MediaPlacement,
                 mediaRegion: String,
                 meetingId: String,
                 primaryMeetingId: String?) {
        self.externalMeetingId = externalMeetingId
        self.mediaPlacement = mediaPlacement
        self.mediaRegion = mediaRegion
        self.meetingId = meetingId
        self.primaryMeetingId = primaryMeetingId
    }
}

@objcMembers public class MediaPlacement: NSObject {
    let audioFallbackUrl: String
    let audioHostUrl: String
    let signalingUrl: String
    let turnControlUrl: String
    let eventIngestionUrl: String?

    public convenience init(audioFallbackUrl: String, audioHostUrl: String, signalingUrl: String, turnControlUrl: String) {
        self.init(audioFallbackUrl: audioFallbackUrl,
                  audioHostUrl: audioHostUrl,
                  signalingUrl: signalingUrl,
                  turnControlUrl: turnControlUrl,
                  eventIngestionUrl: nil)
    }

    public init(audioFallbackUrl: String,
                audioHostUrl: String,
                signalingUrl: String,
                turnControlUrl: String,
                eventIngestionUrl: String?) {
        self.audioFallbackUrl = audioFallbackUrl
        self.audioHostUrl = audioHostUrl
        self.signalingUrl = signalingUrl
        self.turnControlUrl = turnControlUrl
        self.eventIngestionUrl = eventIngestionUrl
    }
}


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

import AmazonChimeSDKMedia
import AVFoundation
import Foundation

@objcMembers public class DefaultMeetingSession: NSObject, MeetingSession {
    public let audioVideo: AudioVideoFacade
    public let configuration: MeetingSessionConfiguration
    public let logger: Logger
    public let eventAnalyticsController: EventAnalyticsController

    private let audioSession = AVAudioSession.sharedInstance()

    public init(configuration: MeetingSessionConfiguration,
                logger: Logger,
                eventReporterFactory: EventReporterFactory) {
        self.configuration = configuration
        self.logger = logger
        VideoClient.globalInitialize()
        let audioClient: DefaultAudioClient = DefaultAudioClient.shared(logger: logger)
        let videoClient: VideoClient = DefaultVideoClient(logger: logger)
        let clientMetricsCollector = DefaultClientMetricsCollector()
        let audioClientLock = NSLock()

        let meetingStatsCollector =  DefaultMeetingStatsCollector(logger: logger)

        self.eventAnalyticsController = DefaultEventAnalyticsController(meetingSessionConfig: configuration,
                                                                        meetingStatsCollector: meetingStatsCollector,
                                                                        logger: logger,
                                                                        eventReporter: eventReporterFactory.createEventReporter())
        let audioClientObserver = DefaultAudioClientObserver(audioClient: audioClient,
                                                             clientMetricsCollector: clientMetricsCollector,
                                                             audioClientLock: audioClientLock,
                                                             configuration: configuration,
                                                             logger: logger,
                                                             eventAnalyticsController: self.eventAnalyticsController,
                                                             meetingStatsCollector: meetingStatsCollector)
        let activeSpeakerDetector =
            DefaultActiveSpeakerDetector(selfAttendeeId: configuration.credentials.attendeeId)
        let audioClientController = DefaultAudioClientController(audioClient: audioClient,
                                                                 audioClientObserver: audioClientObserver,
                                                                 audioSession: audioSession,
                                                                 audioClientLock: audioClientLock,
                                                                 eventAnalyticsController: self.eventAnalyticsController,
                                                                 meetingStatsCollector: meetingStatsCollector,
                                                                 activeSpeakerDetector: activeSpeakerDetector,
                                                                 logger: logger)
        let videoClientController = DefaultVideoClientController(videoClient: videoClient,
                                                                 clientMetricsCollector: clientMetricsCollector,
                                                                 configuration: configuration,
                                                                 logger: logger,
                                                                 eventAnalyticsController: self.eventAnalyticsController)
        let videoTileController =
            DefaultVideoTileController(videoClientController: videoClientController,
                                       logger: logger,
                                       meetingStatsCollector: meetingStatsCollector)
        let realtimeController = DefaultRealtimeController(audioClientController: audioClientController,
                                                           audioClientObserver: audioClientObserver,
                                                           videoClientController: videoClientController)

        let contentModality = String(DefaultModality.separator) + String(describing: ModalityType.content)
        let contentShareCredentials = MeetingSessionCredentials(
            attendeeId: configuration.credentials.attendeeId + contentModality,
            externalUserId: configuration.credentials.externalUserId,
            joinToken: configuration.credentials.joinToken + contentModality)
        let contentShareConfiguration = MeetingSessionConfiguration(meetingId: configuration.meetingId,
                                                                    externalMeetingId: configuration.externalMeetingId,
                                                                    credentials: contentShareCredentials,
                                                                    urls: configuration.urls,
                                                                    urlRewriter: configuration.urlRewriter)
        let contentShareVideoClient = DefaultVideoClient(logger: logger)
        let contentShareVideoClientController = DefaultContentShareVideoClientController(videoClient: contentShareVideoClient,
                                                                                configuration: contentShareConfiguration,
                                                                                logger: logger,
                                                                                clientMetricsCollector: clientMetricsCollector)
        let contentShareController = DefaultContentShareController(contentShareVideoClientController: contentShareVideoClientController)

        self.audioVideo =
            DefaultAudioVideoFacade(audioVideoController:
                DefaultAudioVideoController(audioClientController: audioClientController,
                                            audioClientObserver: audioClientObserver,
                                            clientMetricsCollector: clientMetricsCollector,
                                            videoClientController: videoClientController,
                                            videoTileController: videoTileController,
                                            configuration: configuration,
                                            logger: logger),
                                    realtimeController: realtimeController,
                                    deviceController:
                DefaultDeviceController(audioSession: audioSession,
                                        videoClientController: videoClientController,
                                        eventAnalyticsController: eventAnalyticsController,
                                        logger: logger),
                                    videoTileController: videoTileController,
                                    activeSpeakerDetector: activeSpeakerDetector,
                                    contentShareController: contentShareController,
                                    eventAnalyticsController: self.eventAnalyticsController,
                                    meetingStatsCollector: meetingStatsCollector)
    }

    public convenience init(configuration: MeetingSessionConfiguration,
                            logger: Logger) {
        let ingestionConfiguration = IngestionConfigurationBuilder().build(disabled: configuration.urls.ingestionUrl?.isEmpty != false,
                                                                           ingestionUrl: configuration.urls.ingestionUrl ?? "",
                                                                           clientConiguration: MeetingEventClientConfiguration(eventClientJoinToken: configuration.credentials.joinToken,
                                                                                meetingId: configuration.meetingId,
                                                                                attendeeId: configuration.credentials.attendeeId)
        )
        self.init(configuration: configuration,
                  logger: logger,
                  eventReporterFactory: DefaultMeetingEventReporterFactory(ingestionConfiguration: ingestionConfiguration,
                                                                    logger: logger))
    }
}


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

import Foundation

@objc public protocol MeetingSession {
    var configuration: MeetingSessionConfiguration { get }
    var logger: Logger { get }
    var audioVideo: AudioVideoFacade { get }
    var eventAnalyticsController: EventAnalyticsController { get }
}


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

import Foundation

/// `MeetingSessionConfiguration` contains the information necessary to start a session.
/// Constructs a MeetingSessionConfiguration with a chime:`CreateMeetingResponse` and
/// chime:`CreateAttendeeResponse` response and optional custom `URLRewriter` that will
/// rewrite urls given to new urls.
@objcMembers public class MeetingSessionConfiguration: NSObject {
    /// The id of the meeting the session is joining.
    public let meetingId: String

    /// The external id of the meeting the session is joining. See https://docs.aws.amazon.com/chime/latest/APIReference/API_CreateMeeting.html#API_CreateMeeting_RequestSyntax for more details
    public let externalMeetingId: String?

    /// The credentials used to authenticate the session.
    public let credentials: MeetingSessionCredentials

    /// The URLs the session uses to reach the meeting service.
    public let urls: MeetingSessionURLs

    public let urlRewriter: URLRewriter

    /// The id of the primary meeting that this session is joining a replica to
    public let primaryMeetingId: String?

    public convenience init(createMeetingResponse: CreateMeetingResponse,
                            createAttendeeResponse: CreateAttendeeResponse) {
        self.init(createMeetingResponse: createMeetingResponse,
                  createAttendeeResponse: createAttendeeResponse,
                  urlRewriter: URLRewriterUtils.defaultUrlRewriter)
    }

    public convenience init(meetingId: String,
                            credentials: MeetingSessionCredentials,
                            urls: MeetingSessionURLs,
                            urlRewriter: @escaping URLRewriter) {
        self.init(meetingId: meetingId,
                  externalMeetingId: nil,
                  credentials: credentials,
                  urls: urls,
                  urlRewriter: urlRewriter)
    }

    public convenience init(meetingId: String,
                            externalMeetingId: String?,
                            credentials: MeetingSessionCredentials,
                            urls: MeetingSessionURLs,
                            urlRewriter: @escaping URLRewriter) {
        self.init(meetingId: meetingId,
                  externalMeetingId: externalMeetingId,
                  credentials: credentials,
                  urls: urls,
                  urlRewriter: urlRewriter,
                  primaryMeetingId: nil)
    }

    public init(meetingId: String,
                externalMeetingId: String?,
                credentials: MeetingSessionCredentials,
                urls: MeetingSessionURLs,
                urlRewriter: @escaping URLRewriter,
                primaryMeetingId: String?) {
        self.meetingId = meetingId
        self.externalMeetingId = externalMeetingId
        self.credentials = credentials
        self.urls = urls
        self.urlRewriter = urlRewriter
        self.primaryMeetingId = primaryMeetingId
    }

    public init(createMeetingResponse: CreateMeetingResponse,
                createAttendeeResponse: CreateAttendeeResponse,
                urlRewriter: @escaping URLRewriter) {
        self.meetingId = createMeetingResponse.meeting.meetingId
        self.externalMeetingId = createMeetingResponse.meeting.externalMeetingId
        self.primaryMeetingId = createMeetingResponse.meeting.primaryMeetingId
        self.credentials = MeetingSessionCredentials(attendeeId: createAttendeeResponse.attendee.attendeeId,
                                                     externalUserId: createAttendeeResponse.attendee.externalUserId,
                                                     joinToken: createAttendeeResponse.attendee.joinToken)
        self.urls = MeetingSessionURLs(audioFallbackUrl: createMeetingResponse.meeting.mediaPlacement.audioFallbackUrl,
                                       audioHostUrl: createMeetingResponse.meeting.mediaPlacement.audioHostUrl,
                                       turnControlUrl: createMeetingResponse.meeting.mediaPlacement.turnControlUrl,
                                       signalingUrl: createMeetingResponse.meeting.mediaPlacement.signalingUrl,
                                       urlRewriter: urlRewriter,
                                       ingestionUrl: createMeetingResponse.meeting.mediaPlacement.eventIngestionUrl)
        self.urlRewriter = urlRewriter
    }
}


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

import Foundation

/// `MeetingSessionCredentials` includes the credentials used to authenticate.
/// the attendee on the meeting
@objcMembers public class MeetingSessionCredentials: NSObject, Codable {
    /// The attendee id for these credentials.
    public let attendeeId: String

    /// The external user Id associated with the attendee.
    public let externalUserId: String

    /// The token that the session will be authenticated with.
    public let joinToken: String

    public init(attendeeId: String, externalUserId: String, joinToken: String) {
        self.attendeeId = attendeeId
        self.externalUserId = externalUserId
        self.joinToken = joinToken
    }
}


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

import Foundation

/// `MeetingSessionStatus` indicates a status received regarding the session.
@objcMembers public class MeetingSessionStatus: NSObject {
    public let statusCode: MeetingSessionStatusCode

    public init(statusCode: MeetingSessionStatusCode) {
        self.statusCode = statusCode
    }
}


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

import Foundation

@objc public enum MeetingSessionStatusCode: UInt32, CustomStringConvertible {
    /// Everything is OK so far.
    case ok = 0

    /// The audio leg failed.
    case audioDisconnected = 9

    /// Due to connection health a reconnect has been triggered.
    case connectionHealthReconnect = 10

    /// Network is not good enough for VoIP.
    case networkBecomePoor = 59

    /// Server hung up.
    case audioServerHungup = 60

    /// The attendee joined from another device.
    case audioJoinedFromAnotherDevice = 61

    /// There was an internal server error with the audio leg.
    case audioInternalServerError = 62

    /// Authentication was rejected. The client is not allowed on this call.
    case audioAuthenticationRejected = 63

    /// The client can not join because the call is at capacity.
    case audioCallAtCapacity = 64

    /// Could not connect the audio leg due to the service being unavailable.
    case audioServiceUnavailable = 65

    /// The attendee should explicitly switch itself from joined with audio to checked-in.
    case audioDisconnectAudio = 69

    /// The call was ended.
    case audioCallEnded = 75

    /// video service is unavailable.
    case videoServiceUnavailable = 12

    /// If State cannot be parsed, then use this state.
    case unknown = 78

    /// When maximum concurrent video channel reached
    case videoAtCapacityViewOnly = 206

    /// Designated input device is not responding and timed out.
    case audioInputDeviceNotResponding = 82

    /// Designated output device is not responding and timed out.
    case audioOutputDeviceNotResponding = 83

    public var description: String {
        switch self {
        case .ok:
            return "ok"
        case .audioDisconnected:
            return "audioDisconnected"
        case .connectionHealthReconnect:
            return "connectionHealthReconnect"
        case .networkBecomePoor:
            return "networkBecomePoor"
        case .audioServerHungup:
            return "audioServerHungup"
        case .audioJoinedFromAnotherDevice:
            return "audioJoinedFromAnotherDevice"
        case .audioInternalServerError:
            return "audioInternalServerError"
        case .audioAuthenticationRejected:
            return "audioAuthenticationRejected"
        case .audioCallAtCapacity:
            return "audioCallAtCapacity"
        case .audioServiceUnavailable:
            return "audioServiceUnavailable"
        case .audioDisconnectAudio:
            return "audioDisconnectAudio"
        case .audioCallEnded:
            return "audioCallEnded"
        case .videoServiceUnavailable:
            return "videoServiceUnavailable"
        case .unknown:
            return "unknown"
        case .videoAtCapacityViewOnly:
            return "videoAtCapacityViewOnly"
        case .audioInputDeviceNotResponding:
            return "audioInputDeviceNotResponding"
        case .audioOutputDeviceNotResponding:
            return "audioOutputDeviceNotResponding"
        }
    }
}


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

import Foundation

/// `MeetingSessionURLs` contains the URLs that will be used to reach the meeting service.
@objcMembers public class MeetingSessionURLs: NSObject, Codable {
    /// The audio fallback URL of the session
    public let audioFallbackUrl: String

    /// The audio host URL of the session
    public let audioHostUrl: String

    /// The TURN control URL of the session
    public let turnControlUrl: String

    /// The signaling URL of the session
    public let signalingUrl: String

    /// The event ingestion URL of the session
    public let ingestionUrl: String?

    public convenience init(audioFallbackUrl: String,
                            audioHostUrl: String,
                            turnControlUrl: String,
                            signalingUrl: String,
                            urlRewriter: URLRewriter) {
        self.init(audioFallbackUrl: audioFallbackUrl,
                  audioHostUrl: audioHostUrl,
                  turnControlUrl: turnControlUrl,
                  signalingUrl: signalingUrl,
                  urlRewriter: urlRewriter,
                  ingestionUrl: nil)
    }

    public init(audioFallbackUrl: String,
                audioHostUrl: String,
                turnControlUrl: String,
                signalingUrl: String,
                urlRewriter: URLRewriter,
                ingestionUrl: String?) {
        self.audioFallbackUrl = urlRewriter(audioFallbackUrl)
        self.audioHostUrl = urlRewriter(audioHostUrl)
        self.turnControlUrl = urlRewriter(turnControlUrl)
        self.signalingUrl = urlRewriter(signalingUrl)
        if let ingestionUrl = ingestionUrl {
            self.ingestionUrl = urlRewriter(ingestionUrl)
        } else {
            self.ingestionUrl = ingestionUrl
        }
    }
}


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

import Foundation

/// `URLRewriter` Function to transform URLs.
/// Use this to rewrite URLs to traverse proxies.
/// - Parameter url: Url string
/// - Returns: A new url string manipulated
public typealias URLRewriter = (_ url: String) -> String

/// `URLRewriterUtils` is class that defines default Url rewrite behavior
@objcMembers public class URLRewriterUtils: NSObject {
    /// The default implementation returns the original URL unchanged.
    public static let defaultUrlRewriter: URLRewriter = { url in
        url
    }
}


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

import Foundation

/// `DefaultModality` is a backwards compatible extension of the
/// attendee id (UUID string) and session token schemas (base 64 string).
/// It appends #<modality> to either string, which indicates the modality
/// of the participant.
///
/// For example,
/// `attendeeId`: "abcdefg"
/// `contentAttendeeId`: "abcdefg#content"
/// `DefaultModality(id: contentAttendeeId).base`: "abcdefg"
/// `DefaultModality(id: contentAttendeeId).modality`: "content"
/// `DefaultModality(id: contentAttendeeId).isOfType(type: .content)`: true
@objcMembers public class DefaultModality: NSObject {
    public let id: String
    public let base: String
    public let modality: String?
    public static let separator: Character = "#"

    public init(id: String) {
        self.id = id
        let substrings = id.split(separator: DefaultModality.separator)
        base = String(substrings[0])
        if substrings.count == 2 {
            modality = String(substrings[1])
        } else {
            modality = nil
        }
    }

    public func isOfType(type: ModalityType) -> Bool {
        return modality == type.description
    }
}


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

import Foundation

/// This is helper function to convert dictionary into JSON string that can be used in
/// `EventAnalyticsObserver.eventDidReceive` callback to convert attributes
/// into JSON string. Calling `attributes.text()`  will returns string
public extension Dictionary where Key == AnyHashable, Value: Any {
    func toJsonString() -> String {
        var jsonDict = [String: String]()
        self.forEach { (key, value) in
            jsonDict[String(describing: key)] = String(describing: value)
        }
        let data = try? JSONSerialization.data(withJSONObject: jsonDict, options: [])
        if let jsonData = data {
            return String(data: jsonData, encoding: .utf8) ?? ""
        }
        return ""
    }
}

/// This is helper function to convert dictionary into JSON string that can be used in
/// `EventAnalyticsObserver.eventDidReceive` callback to convert attributes
/// into JSON string. Calling `attributes.text()`  will returns string
@objc public extension NSDictionary {
    func toJsonString() -> String {
        if let dict = self as? [AnyHashable: Any] {
            return dict.toJsonString()
        }
        return ""
    }
}

extension Dictionary where Key: Comparable, Value: Equatable {
    func subtracting(dict: [Key: Value]) -> [Key: Value] {
        let selfEntries = filter {
            dict[$0.0] != self[$0.0]
        }
        return selfEntries.reduce([Key: Value]()) { (result, entry) -> [Key: Value] in
            var map = result
            map[entry.0] = entry.1
            return map
        }
    }
}

extension Dictionary where Key: Any, Value: Any {
    func toStringDict() -> [String: String] {
        var jsonDict = [String: String]()
        self.forEach { (key, value) in
            jsonDict[String(describing: key)] = String(describing: value)
        }
        return jsonDict
    }
}


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

import Foundation

@objc public enum MediaError: Int, Error, CustomStringConvertible {
    case illegalState
    case audioFailedToStart

    public var description: String {
        switch self {
        case .illegalState:
            return "illegalState"
        case .audioFailedToStart:
            return "audioFailedToStart"
        }
    }
}


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

import Foundation

@objc public enum ModalityType: Int, Error, CustomStringConvertible {
    case content

    public var description: String {
        switch self {
        case .content:
            return "content"
        }
    }
}


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

import Foundation

@objc public enum PermissionError: Int, Error, CustomStringConvertible {
    case audioPermissionError
    case videoPermissionError

    public var description: String {
        switch self {
        case .audioPermissionError:
            return "audioPermissionError"
        case .videoPermissionError:
            return "videoPermissionError"
        }
    }
}


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

import Foundation

@objc public enum ResourceError: Int, Error, CustomStringConvertible {
    case notFound

    public var description: String {
        switch self {
        case .notFound:
            return "notFound"
        }
    }
}


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

import Foundation

@objc public enum SendDataMessageError: Int, Error, CustomStringConvertible {
    // Data message payload is too large
    case invalidDataLength

    // Topic string is not passing regex check
    case invalidTopic

    // Lifetime parameter should be positive
    case negativeLifetimeParameter

    // Data can only be string or JSON serializable (JSONSerialization.isValidJSONObject() == true)
    case invalidData

    public var description: String {
        switch self {
        case .invalidDataLength:
            return "invalidDataLength"
        case .invalidTopic:
            return "invalidTopic"
        case .negativeLifetimeParameter:
            return "negativeLifetimeParameter"
        case .invalidData:
            return "invalidData"
        }
    }
}


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

@objcMembers public class Versioning: NSObject {
    /// Returns the current version of Amazon Chime SDK in the format of string.
    /// If there is an error with the version, empty string will be returned.
    public static func sdkVersion() -> String {
        let sdkVersion = Bundle(for: Versioning.self).infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
        return sdkVersion
    }
}


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

import Foundation
import os

/// ConsoleLogger writes logs with console.
///
/// ```
/// // working with the ConsoleLogger
/// let logger = new ConsoleLogger("demo"); //default level is LogLevel.INFO
/// logger.info("info");
/// logger.debug("debug");
/// logger.fault("fault");
/// logger.error("error");
///
/// // setting logging levels
/// let logger = new ConsoleLogger("demo", .INFO);
/// logger.debug("debug"); // does not print
/// logger.setLogLevel(LogLevel.DEBUG)
/// logger.debug("debug"); // print
/// ```
@objcMembers public class ConsoleLogger: NSObject, Logger {
    let name: String
    var level: LogLevel

    public init(name: String, level: LogLevel = .INFO) {
        self.name = name
        self.level = level
    }

    public func `default`(msg: String) {
        log(type: .DEFAULT, msg: msg)
    }

    public func debug(debugFunction: () -> String) {
        log(type: .DEBUG, msg: debugFunction())
    }

    public func info(msg: String) {
        log(type: .INFO, msg: msg)
    }

    public func fault(msg: String) {
        log(type: .FAULT, msg: msg)
    }

    public func error(msg: String) {
        log(type: .ERROR, msg: msg)
    }

    public func setLogLevel(level: LogLevel) {
        self.level = level
    }

    public func getLogLevel() -> LogLevel {
        return level
    }

    private func log(type: LogLevel, msg: String) {
        if type.rawValue < level.rawValue {
            return
        }

        let logMessage = "[\(LogLevel.allCases[type.rawValue])] \(self.name) - " + msg

        switch type {
        case .DEFAULT:
            os_log("%@", type: .default, logMessage)
        case .DEBUG:
            os_log("%@", type: .debug, logMessage)
        case .INFO:
            os_log("%@", type: .info, logMessage)
        case .FAULT:
            os_log("%@", type: .fault, logMessage)
        case .ERROR:
            os_log("%@", type: .error, logMessage)
        default:
            return
        }
    }
}


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

import Foundation

/// `Logger` defines how to write logs for different logging level.
@objc public protocol Logger {
    /// Emits any message if the log level is equal to or lower than default level.
    func `default`(msg: String)

    /// Calls `debugFunction` only if the log level is debug and emits the
    /// resulting string. Use the debug level to dump large or verbose messages
    /// that could slow down performance.
    func debug(debugFunction: () -> String)

    /// Emits an info message if the log level is equal to or lower than info level.
    func info(msg: String)

    /// Emits a fault message if the log level is equal to or lower than fault level.
    func fault(msg: String)

    /// Emits an error message if the log level is equal to or lower than error level.
    func error(msg: String)

    /// Sets the log level.
    func setLogLevel(level: LogLevel)

    /// Gets the current log level.
    func getLogLevel() -> LogLevel
}


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

import Foundation

@objc public enum LogLevel: Int, CaseIterable, CustomStringConvertible {
    case DEFAULT = 0
    case DEBUG = 1
    case INFO = 2
    case FAULT = 3
    case ERROR = 4
    case OFF = 5

    public var description: String {
        switch self {
        case .DEFAULT:
            return "DEFAULT"
        case .DEBUG:
            return "DEBUG"
        case .INFO:
            return "INFO"
        case .FAULT:
            return "FAULT"
        case .ERROR:
            return "ERROR"
        case .OFF:
            return "OFF"
        }
    }
}


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

import Foundation
@testable import AmazonChimeSDK
import XCTest

class EventAttributeNameTests: XCTestCase {
    func testDescriptionShouldMatch() {
        XCTAssertEqual(EventAttributeName.deviceName.description, "deviceName")
        XCTAssertEqual(EventAttributeName.deviceManufacturer.description, "deviceManufacturer")
        XCTAssertEqual(EventAttributeName.deviceModel.description, "deviceModel")
        XCTAssertEqual(EventAttributeName.osName.description, "osName")
        XCTAssertEqual(EventAttributeName.osVersion.description, "osVersion")
        XCTAssertEqual(EventAttributeName.sdkName.description, "sdkName")
        XCTAssertEqual(EventAttributeName.sdkVersion.description, "sdkVersion")
        XCTAssertEqual(EventAttributeName.timestampMs.description, "timestampMs")
        XCTAssertEqual(EventAttributeName.mediaSdkVersion.description, "mediaSdkVersion")
        XCTAssertEqual(EventAttributeName.attendeeId.description, "attendeeId")
        XCTAssertEqual(EventAttributeName.externalMeetingId.description, "externalMeetingId")
        XCTAssertEqual(EventAttributeName.externalUserId.description, "externalUserId")
        XCTAssertEqual(EventAttributeName.meetingId.description, "meetingId")
        XCTAssertEqual(EventAttributeName.meetingHistory.description, "meetingHistory")
        XCTAssertEqual(EventAttributeName.maxVideoTileCount.description, "maxVideoTileCount")
        XCTAssertEqual(EventAttributeName.meetingStartDurationMs.description, "meetingStartDurationMs")
        XCTAssertEqual(EventAttributeName.meetingDurationMs.description, "meetingDurationMs")
        XCTAssertEqual(EventAttributeName.meetingErrorMessage.description, "meetingErrorMessage")
        XCTAssertEqual(EventAttributeName.meetingStatus.description, "meetingStatus")
        XCTAssertEqual(EventAttributeName.poorConnectionCount.description, "poorConnectionCount")
        XCTAssertEqual(EventAttributeName.retryCount.description, "retryCount")
        XCTAssertEqual(EventAttributeName.videoInputError.description, "videoInputError")
    }
}


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

import Foundation
@testable import AmazonChimeSDK
import XCTest

class EventNameTests: XCTestCase {
    func testDescriptionShouldMatch() {
        XCTAssertEqual(EventName.videoInputFailed.description, "videoInputFailed")
        XCTAssertEqual(EventName.meetingStartRequested.description, "meetingStartRequested")
        XCTAssertEqual(EventName.meetingStartSucceeded.description, "meetingStartSucceeded")
        XCTAssertEqual(EventName.meetingStartFailed.description, "meetingStartFailed")
        XCTAssertEqual(EventName.meetingFailed.description, "meetingFailed")
        XCTAssertEqual(EventName.meetingEnded.description, "meetingEnded")
    }
}


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

import Foundation
@testable import AmazonChimeSDK
import XCTest

class MeetingHistoryEventNameTests: XCTestCase {
    func testDescriptionShouldMatch() {
        XCTAssertEqual(MeetingHistoryEventName.audioInputSelected.description, "audioInputSelected")
        XCTAssertEqual(MeetingHistoryEventName.videoInputSelected.description, "videoInputSelected")
        XCTAssertEqual(MeetingHistoryEventName.videoInputFailed.description, "videoInputFailed")
        XCTAssertEqual(MeetingHistoryEventName.meetingStartRequested.description, "meetingStartRequested")
        XCTAssertEqual(MeetingHistoryEventName.meetingStartSucceeded.description, "meetingStartSucceeded")
        XCTAssertEqual(MeetingHistoryEventName.meetingEnded.description, "meetingEnded")
        XCTAssertEqual(MeetingHistoryEventName.meetingFailed.description, "meetingFailed")
        XCTAssertEqual(MeetingHistoryEventName.meetingReconnected.description, "meetingReconnected")
    }
}