//
//  MeetingViewController.swift
//  AmazonChimeSDKDemo
//
//  Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
//  SPDX-License-Identifier: Apache-2.0
//
// swiftlint:disable type_body_length

import AmazonChimeSDK
import AVFoundation
import CallKit
import Foundation
import ReplayKit
import UIKit

class MeetingViewController: UIViewController {
    // Controls
    @IBOutlet var controlView: UIView!
    @IBOutlet var cameraButton: UIButton!
    @IBOutlet var deviceButton: UIButton!
    @IBOutlet var broadcastButton: UIButton!
    @IBOutlet var broadcastPickerContainerView: UIView!
    @IBOutlet var additionalOptionsButton: UIButton!
    @IBOutlet var endButton: UIButton!
    @IBOutlet var muteButton: UIButton!
    @IBOutlet var resumeCallKitMeetingButton: UIButton!
    @IBOutlet var segmentedControl: UISegmentedControl!

    // Accessory views
    @IBOutlet var containerView: UIView!
    @IBOutlet var titleView: UIView!
    @IBOutlet var titleLabel: UILabel!

    // Screen share
    @IBOutlet var screenView: UIView!
    @IBOutlet var noScreenViewLabel: UILabel!
    @IBOutlet var screenRenderView: DefaultVideoRenderView!

    // Roster
    @IBOutlet var rosterTable: UITableView!

    // Video
    @IBOutlet var videoCollection: UICollectionView!

    // Video Pagination Control
    @IBOutlet var videoPaginationControlView: UIView!
    @IBOutlet var prevVideoPageButton: UIButton!
    @IBOutlet var nextVideoPageButton: UIButton!

    // Metrics
    @IBOutlet var metricsTable: UITableView!

    // Chat View
    @IBOutlet var chatView: UIView!
    @IBOutlet var chatMessageTable: UITableView!
    @IBOutlet var inputBox: UIView!
    @IBOutlet var inputText: UITextField!
    @IBOutlet var sendMessageButton: UIButton!
    @IBOutlet var inputBoxBottomConstrain: NSLayoutConstraint!

    // Captions
    @IBOutlet var captionsTableView: UITableView!

    // Model
    var meetingModel: MeetingModel?

    // Local var
    private let logger = ConsoleLogger(name: "MeetingViewController")

    // MARK: Override functions

    override func viewDidLoad() {
        guard let meetingModel = meetingModel else {
            logger.error(msg: "MeetingModel not set")
            dismiss(animated: true, completion: nil)
            return
        }
        configure(meetingModel: meetingModel)
        super.viewDidLoad()
        setupUI()

        meetingModel.startMeeting()
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        guard let layout = videoCollection.collectionViewLayout as? UICollectionViewFlowLayout else {
            return
        }
        if UIDevice.current.orientation.isLandscape {
            layout.scrollDirection = .horizontal
        } else {
            layout.scrollDirection = .vertical
        }
    }

    private func registerForKeyboardNotifications() {
        // Adding notifies on keyboard appearing
        NotificationCenter
            .default
            .addObserver(self,
                         selector: #selector(keyboardShowHandler),
                         name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter
            .default
            .addObserver(self,
                         selector: #selector(keyboardHideHandler),
                         name: UIResponder.keyboardWillHideNotification,
                         object: nil)
    }

    private func deregisterFromKeyboardNotifications() {
        // Removing notifies on keyboard appearing
        NotificationCenter
            .default
            .removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter
            .default
            .removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
    }

    private func configure(meetingModel: MeetingModel) {
        meetingModel.activeModeDidSetHandler = { [weak self] activeMode in
            self?.switchSubview(mode: activeMode)
        }
        meetingModel.notifyHandler = { [weak self] message in
            self?.view?.makeToast(message, duration: 2.0, position: .top)
        }
        meetingModel.isMutedHandler = { [weak self] isMuted in
            self?.muteButton.isSelected = isMuted
        }
        meetingModel.isEndedHandler = { [weak meetingModel] in
            DispatchQueue.main.async {
                guard let meetingModel = meetingModel else { return }
                MeetingModule.shared().dismissMeeting(meetingModel)
            }
        }
        meetingModel.rosterModel.rosterUpdatedHandler = { [weak self] in
            self?.rosterTable.reloadData()
        }
        meetingModel.metricsModel.metricsUpdatedHandler = { [weak self] in
            self?.metricsTable.reloadData()
        }
        meetingModel.videoModel.videoUpdatedHandler = { [weak self, weak meetingModel] in
            guard let strongSelf = self, let meetingModel = meetingModel else { return }
            meetingModel.videoModel.resumeAllRemoteVideosInCurrentPageExceptUserPausedVideos()
            strongSelf.prevVideoPageButton.isEnabled = meetingModel.videoModel.canGoToPrevRemoteVideoPage
            strongSelf.nextVideoPageButton.isEnabled = meetingModel.videoModel.canGoToNextRemoteVideoPage
            strongSelf.videoCollection.reloadData()
        }
        meetingModel.videoModel.videoSubscriptionUpdatedHandler = { [weak self, weak meetingModel] in
            guard let strongSelf = self, let meetingModel = meetingModel else { return }
            meetingModel.videoModel.removeRemoteVideosNotInCurrentPage()
            meetingModel.videoModel.updateAllRemoteVideosInCurrentPageExceptUserPausedVideos()
            strongSelf.prevVideoPageButton.isEnabled = meetingModel.videoModel.canGoToPrevRemoteVideoPage
            strongSelf.nextVideoPageButton.isEnabled = meetingModel.videoModel.canGoToNextRemoteVideoPage
            strongSelf.videoCollection.reloadData()
        }
        meetingModel.videoModel.localVideoUpdatedHandler = { [weak self] in
            self?.videoCollection?.reloadItems(at: [IndexPath(item: 0, section: 0)])
        }
        meetingModel.screenShareModel.tileIdDidSetHandler = { [weak self, weak meetingModel] tileId in
            if let tileId = tileId,
               let screenRenderView = self?.screenRenderView,
               let meetingModel = meetingModel {
                meetingModel.bind(videoRenderView: screenRenderView, tileId: tileId)
            }
        }
        meetingModel.screenShareModel.viewUpdateHandler = { [weak self] shouldShow in
            self?.screenRenderView.isHidden = !shouldShow
            self?.noScreenViewLabel.isHidden = shouldShow
        }

        meetingModel.chatModel.refreshChatTableHandler = { [weak self] in
            self?.chatMessageTable.reloadData()
        }

        meetingModel.captionsModel.refreshCaptionsTableHandler = { [weak self] in
            self?.captionsTableView.reloadData()
            // auto scroll to bottom when new captions come in
            let indexPath = IndexPath(row: meetingModel.captionsModel.captions.count - 1, section: 0)
            self?.captionsTableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
        }
    }

    // MARK: UI functions

    private func setupUI() {
        // Labels
        titleLabel.text = meetingModel?.meetingId
        titleLabel.accessibilityLabel = "Meeting ID \(meetingModel?.meetingId ?? "")"

        // Buttons
        let buttonStack = [muteButton,
                           deviceButton,
                           cameraButton,
                           additionalOptionsButton,
                           endButton,
                           sendMessageButton]
        for button in buttonStack {
            let normalButtonImage = button?.image(for: .normal)?.withRenderingMode(.alwaysTemplate)
            let selectedButtonImage = button?.image(for: .selected)?.withRenderingMode(.alwaysTemplate)
            button?.setImage(normalButtonImage, for: .normal)
            button?.setImage(selectedButtonImage, for: .selected)
            button?.imageView?.contentMode = UIView.ContentMode.scaleAspectFit
            button?.tintColor = .systemBlue
        }
        endButton.tintColor = .red
        resumeCallKitMeetingButton.isHidden = true
        prevVideoPageButton.isEnabled = false
        nextVideoPageButton.isEnabled = false

        if let model = meetingModel, !model.primaryExternalMeetingId.isEmpty {
            meetingModel?.setMute(isMuted: true)
            self.enableOrDisableButtonsForReplicatedMeeting(enabled: false)
        }

        // Segmented Controler
        segmentedControl.selectedSegmentIndex = SegmentedControlIndex.attendees.rawValue

        // Roster table view
        rosterTable.delegate = meetingModel?.rosterModel
        rosterTable.dataSource = meetingModel?.rosterModel

        // Video collection view
        videoCollection.delegate = self
        videoCollection.dataSource = self

        // Metrics table view
        metricsTable.delegate = meetingModel?.metricsModel
        metricsTable.dataSource = meetingModel?.metricsModel

        // Chat table
        chatMessageTable.delegate = meetingModel?.chatModel
        chatMessageTable.dataSource = meetingModel?.chatModel
        registerForKeyboardNotifications()
        sendMessageButton.isEnabled = false
        chatMessageTable.separatorStyle = .none
        sendMessageButton.imageView?.contentMode = UIView.ContentMode.scaleAspectFit
        setupHideKeyboardOnTap()

        // Captions table
        captionsTableView.delegate = meetingModel?.captionsModel
        captionsTableView.dataSource = meetingModel?.captionsModel
        captionsTableView.separatorStyle = .none

        #if !targetEnvironment(simulator)
            if #available(iOS 12.0, *) {
                setupBroadcastPickerView()
            }
        #endif
    }

    // RPSystemBroadcastPickerView
    @available(iOS 12.0, *)
    private func setupBroadcastPickerView() {
        let pickerViewDiameter: CGFloat = 35
        let pickerView = RPSystemBroadcastPickerView(frame: CGRect(x: 0,
                                                                   y: 0,
                                                                   width: pickerViewDiameter,
                                                                   height: pickerViewDiameter))
        pickerView.translatesAutoresizingMaskIntoConstraints = false
        pickerView.preferredExtension = AppConfiguration.broadcastBundleId

        // Microphone audio is passed through AudioVideoControllerFacade instead of ContentShareController
        pickerView.showsMicrophoneButton = false

        // We add an action to turn off in app content sharing when user attemp to use broadcast
        for subview in pickerView.subviews {
            if let button = subview as? UIButton {
                button.imageView?.tintColor = .systemBlue
                button.addTarget(self, action: #selector(broadcastButtonTapped), for: .touchUpInside)
            }
        }
        broadcastPickerContainerView.addSubview(pickerView)

        let centerX = NSLayoutConstraint(item: pickerView,
                                         attribute: .centerX,
                                         relatedBy: .equal,
                                         toItem: broadcastPickerContainerView,
                                         attribute: .centerX,
                                         multiplier: 1,
                                         constant: 0)
        broadcastPickerContainerView.addConstraint(centerX)
        let centerY = NSLayoutConstraint(item: pickerView,
                                         attribute: .centerY,
                                         relatedBy: .equal,
                                         toItem: broadcastPickerContainerView,
                                         attribute: .centerY,
                                         multiplier: 1,
                                         constant: 0)
        broadcastPickerContainerView.addConstraint(centerY)
        let width = NSLayoutConstraint(item: pickerView,
                                       attribute: .width,
                                       relatedBy: .equal,
                                       toItem: nil,
                                       attribute: .notAnAttribute,
                                       multiplier: 1,
                                       constant: pickerViewDiameter)
        broadcastPickerContainerView.addConstraint(width)
        let height = NSLayoutConstraint(item: pickerView,
                                        attribute: .height,
                                        relatedBy: .equal,
                                        toItem: nil,
                                        attribute: .notAnAttribute,
                                        multiplier: 1,
                                        constant: pickerViewDiameter)
        broadcastPickerContainerView.addConstraint(height)
        broadcastPickerContainerView.bringSubviewToFront(pickerView)
    }

    private func switchSubview(mode: MeetingModel.ActiveMode) {
        rosterTable.isHidden = true
        videoCollection.isHidden = true
        videoPaginationControlView.isHidden = true
        screenView.isHidden = true
        screenRenderView.isHidden = true
        noScreenViewLabel.isHidden = true
        metricsTable.isHidden = true
        resumeCallKitMeetingButton.isHidden = true
        segmentedControl.isHidden = false
        containerView.isHidden = false
        chatView.isHidden = true
        captionsTableView.isHidden = true

        guard let meetingModel = meetingModel else {
            return
        }

        switch mode {
        case .roster:
            rosterTable.reloadData()
            rosterTable.isHidden = false
        case .chat:
            chatMessageTable.reloadData()
            chatView.isHidden = false
        case .video:
            videoCollection.reloadData()
            videoCollection.isHidden = false
            videoPaginationControlView.isHidden = false
        case .screenShare:
            screenView.isHidden = false
            if meetingModel.screenShareModel.isAvailable {
                screenRenderView.isHidden = false
            } else {
                noScreenViewLabel.isHidden = false
            }
        case .metrics:
            metricsTable.reloadData()
            metricsTable.isHidden = false
        case .callKitOnHold:
            resumeCallKitMeetingButton.isHidden = false
            segmentedControl.isHidden = true
            containerView.isHidden = true
            muteButton.isEnabled = false
            additionalOptionsButton.isEnabled = false
            deviceButton.isEnabled = false
            cameraButton.isEnabled = false
            additionalOptionsButton.isEnabled = false
        case .captions:
            captionsTableView.reloadData()
            captionsTableView.isHidden = false
        }
    }

    // MARK: IBAction functions

    @IBAction func segmentedControlClicked(_: Any) {
        switch segmentedControl.selectedSegmentIndex {
        case SegmentedControlIndex.attendees.rawValue:
            meetingModel?.activeMode = .roster
        case SegmentedControlIndex.chat.rawValue:
            meetingModel?.activeMode = .chat
        case SegmentedControlIndex.video.rawValue:
            meetingModel?.activeMode = .video
        case SegmentedControlIndex.screen.rawValue:
            meetingModel?.activeMode = .screenShare
        case SegmentedControlIndex.captions.rawValue:
            meetingModel?.activeMode = .captions
        case SegmentedControlIndex.metrics.rawValue:
            meetingModel?.activeMode = .metrics
        default:
            return
        }
    }

    @IBAction func muteButtonClicked(_: UIButton) {
        meetingModel?.setMute(isMuted: !muteButton.isSelected)
    }

    @IBAction func additionalOptionsButtonClicked(_ sender: UIButton) {
        guard let meetingModel = meetingModel else {
            return
        }
        let optionMenu = UIAlertController(title: nil, message: "Additional Options", preferredStyle: .actionSheet)

        if !meetingModel.primaryExternalMeetingId.isEmpty {
            let primaryMeetingPromotionTitle = meetingModel.isPromotedToPrimaryMeeting
                ? "Demote from Primary meeting" : "Promote to Primary meeting"
            let primaryMeetingPromotionAction = UIAlertAction(title: primaryMeetingPromotionTitle,
                                                              style: .default,
                                                              handler: { _ in self.togglePrimaryMeetingPromotion() })
            optionMenu.addAction(primaryMeetingPromotionAction)
        }

        if meetingModel.primaryExternalMeetingId.isEmpty || meetingModel.isPromotedToPrimaryMeeting {
            let isVoiceFocusEnabled = meetingModel.isVoiceFocusEnabled()
            let nextVoiceFocusStatus = nextOnOrOff(current: isVoiceFocusEnabled)
            let voiceFocusAction = UIAlertAction(title: "Turn \(nextVoiceFocusStatus) Voice Focus",
                                                 style: .default,
                                                 handler: { _ in
                                                    meetingModel.setVoiceFocusEnabled(enabled: !isVoiceFocusEnabled)
                                                 })
            optionMenu.addAction(voiceFocusAction)

            // We can only access torch and apply filter on external video source
            if meetingModel.videoModel.isUsingExternalVideoSource {
                let isTorchOn = meetingModel.videoModel.customSource.torchEnabled
                let nextTorchStatus = nextOnOrOff(current: isTorchOn)
                let torchAction = UIAlertAction(title: "Turn \(nextTorchStatus) flashlight",
                                                style: .default,
                                                handler: { _ in
                                                    self.toggleTorch(nextStatus: nextTorchStatus)
                                                })
                optionMenu.addAction(torchAction)

                let isGpuFilterOn = meetingModel.videoModel.isUsingCoreImageVideoProcessor
                let nextGpuFilterStatus = nextOnOrOff(current: isGpuFilterOn)
                let gpuFilterAction = UIAlertAction(title: "Turn \(nextGpuFilterStatus) Core Image video filter",
                                                    style: .default,
                                                    handler: { _ in
                                                        self.toggleCoreImageFilter(nextStatus: nextGpuFilterStatus)
                                                    })
                optionMenu.addAction(gpuFilterAction)

                let isCpuFilterOn = meetingModel.videoModel.isUsingMetalVideoProcessor
                let nextCpuFilterStatus = nextOnOrOff(current: isCpuFilterOn)
                let cpuFilterAction = UIAlertAction(title: "Turn \(nextCpuFilterStatus) Metal video filter",
                                                    style: .default,
                                                    handler: { _ in
                                                        self.toggleMetalFilter(nextStatus: nextCpuFilterStatus)
                                                    })
                optionMenu.addAction(cpuFilterAction)
            }

            let nextSourceType = meetingModel.videoModel.isUsingExternalVideoSource ? "internal" : "external"
            let customSourceAction = UIAlertAction(title: "Use \(nextSourceType) camera source",
                                                   style: .default,
                                                   handler: { _ in
                                                        self.toggleCustomCameraSource(nextSourceType: nextSourceType)
                                                   })
            optionMenu.addAction(customSourceAction)

            let videoConfigAction = UIAlertAction(title: "Video Configuration", style: .default) { _ in
                self.presentVideoConfigAlertController()
            }
            optionMenu.addAction(videoConfigAction)

            #if !targetEnvironment(simulator)
                let inAppContentShareTitle = meetingModel.screenShareModel.inAppCaptureModel.isSharing ?
                    "Stop sharing content" : "Share in app content"
                let inAppContentShareAction = UIAlertAction(title: inAppContentShareTitle,
                                                            style: .default,
                                                            handler: { _ in self.toggleInAppContentShare() })
                optionMenu.addAction(inAppContentShareAction)
            #endif
        }

        if meetingModel.primaryExternalMeetingId.isEmpty {
            // Only show for normal or primary meeting attendees
            let isLiveTranscriptionEnabled = meetingModel.captionsModel.isLiveTranscriptionEnabled
            let nextLiveTranscriptionStatus = nextOnOrOff(current: isLiveTranscriptionEnabled)
            let liveTranscriptionAction = UIAlertAction(title: "Turn \(nextLiveTranscriptionStatus) Live Transcription",
                                                 style: .default,
                                                 handler: { _ in
                                                    meetingModel.setLiveTranscriptionEnabled(enabled: !isLiveTranscriptionEnabled)
                                                 })
            optionMenu.addAction(liveTranscriptionAction)
        }

        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
        optionMenu.addAction(cancelAction)

        if let popover = optionMenu.popoverPresentationController {
            popover.sourceView = sender
            popover.sourceRect = sender.bounds
        }

        present(optionMenu, animated: true, completion: nil)
    }

    @IBAction func deviceButtonClicked(_ sender: UIButton) {
        guard let meetingModel = meetingModel else {
            return
        }

        let optionMenu = UIAlertController(title: nil, message: "Choose Audio Device", preferredStyle: .actionSheet)
        let currentAudioDevice = meetingModel.currentAudioDevice
        for inputDevice in meetingModel.audioDevices {
            var label = inputDevice.label
            if let selectedAudioDevice = currentAudioDevice {
                if selectedAudioDevice.label == inputDevice.label {
                    label = "\(label) âœ”ï¸Ž"
                }
            }
            let deviceAction = UIAlertAction(title: label,
                                             style: .default,
                                             handler: { _ in meetingModel.chooseAudioDevice(inputDevice)
            })
            optionMenu.addAction(deviceAction)
        }

        let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
        optionMenu.addAction(cancelAction)

        if let popover = optionMenu.popoverPresentationController {
            popover.sourceView = sender
            popover.sourceRect = sender.bounds
        }

        present(optionMenu, animated: true, completion: nil)
    }

    @IBAction func cameraButtonClicked(_: UIButton) {
        if meetingModel?.videoModel.cameraSendIsAvailable == true || cameraButton.isSelected {
            cameraButton.isSelected.toggle()
            meetingModel?.videoModel.isLocalVideoActive = cameraButton.isSelected
        } else {
            meetingModel?.notifyHandler?("Cannot Enable Video. Meeting At Capacity.")
        }
    }

    @IBAction func leaveButtonClicked(_: UIButton) {
        meetingModel?.endMeeting()
        deregisterFromKeyboardNotifications()
    }

    @IBAction func inputTextChanged(_: Any, forEvent _: UIEvent) {
        guard let text = inputText.text else {
            return
        }
        if let model = meetingModel, !model.primaryExternalMeetingId.isEmpty && !model.isPromotedToPrimaryMeeting {
            return
        }
        sendMessageButton.isEnabled = !text.isEmpty
    }

    @IBAction func sendMessageButtonClicked(_: Any) {
        guard let text = inputText.text else {
            return
        }

        meetingModel?.sendDataMessage(text)
        self.inputText.text = ""
        sendMessageButton.isEnabled = false
    }

    @IBAction func resumeCallKitMeetingButtonClicked(_: UIButton) {
        meetingModel?.resumeCallKitMeeting()
    }

    @IBAction func prevPageButtonClicked(_: UIButton) {
        meetingModel?.videoModel.getPreviousRemoteVideoPage()
        meetingModel?.videoModel.videoSubscriptionUpdatedHandler?()
        meetingModel?.videoModel.updateVideoSourceSubscription()
    }

    @IBAction func nextPageButtonClicked(_: UIButton) {
        meetingModel?.videoModel.getNextRemoteVideoPage()
        meetingModel?.videoModel.videoSubscriptionUpdatedHandler?()
        meetingModel?.videoModel.updateVideoSourceSubscription()
    }

    @objc private func keyboardShowHandler(notification: NSNotification) {
        // Need to calculate keyboard exact size due to Apple suggestions
        guard let info: NSDictionary = notification.userInfo as NSDictionary? else {
            return
        }
        guard let keyboardSize = (info[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size else {
            return
        }

        let viewHeight = view.frame.size.height
        let realOrigin = chatView.convert(inputBox.frame.origin, to: view)
        let inputBoxDistanceToBottom = viewHeight - realOrigin.y - inputBox.frame.height
        self.inputBoxBottomConstrain.constant = keyboardSize.height - inputBoxDistanceToBottom
    }

    @objc private func keyboardHideHandler(notification _: NSNotification) {
        self.inputBoxBottomConstrain.constant = 0
    }

    @objc private func broadcastButtonTapped() {
        meetingModel?.screenShareModel.broadcastCaptureModel.isBlocked = false
        meetingModel?.screenShareModel.inAppCaptureModel.isSharing = false
    }

    @objc private func toggleInAppContentShare() {
        let isOn = meetingModel?.screenShareModel.inAppCaptureModel.isSharing ?? false
        let nextStateString = isOn ? "off" : "on"
        logger.info(msg: "Turning \(nextStateString) in app content share")
        guard let meetingModel = meetingModel else {
            return
        }
        if #available(iOS 11.0, *) {
            meetingModel.screenShareModel.inAppCaptureModel.isSharing.toggle()
        } else {
            meetingModel.notifyHandler?("In App Content Share is only available on iOS 11+")
        }
    }

    @objc private func toggleTorch(nextStatus: String) {
        logger.info(msg: "Turning \(nextStatus) torch")
        guard let meetingModel = meetingModel else {
            return
        }
        if !meetingModel.videoModel.toggleTorch() {
            meetingModel.notifyHandler?("Failed to turn \(nextStatus) torch on current camera; torch may not be available")
        }
    }

    @objc private func toggleCoreImageFilter(nextStatus: String) {
        logger.info(msg: "Turning \(nextStatus) CoreImage filter")
        guard let meetingModel = meetingModel else {
            return
        }
        if (meetingModel.videoModel.isUsingMetalVideoProcessor ||
            meetingModel.videoModel.isUsingBackgroundBlur ||
            meetingModel.videoModel.isUsingBackgroundReplacement) {
            meetingModel.notifyHandler?("Cannot toggle more than one filter at same time")
            return
        }

        meetingModel.videoModel.isUsingCoreImageVideoProcessor.toggle()
    }

    @objc private func toggleMetalFilter(nextStatus: String) {
        logger.info(msg: "Turning \(nextStatus) Metal filter")
        guard let meetingModel = meetingModel else {
            return
        }
        // See comments in MetalVideoProcessor
        guard let device = MTLCreateSystemDefaultDevice(), device.supportsFeatureSet(.iOS_GPUFamily2_v1) else {
            meetingModel.notifyHandler?("Cannot turn \(nextStatus) Metal filter because it's not available on this device")
            return
        }
        if (meetingModel.videoModel.isUsingCoreImageVideoProcessor ||
            meetingModel.videoModel.isUsingBackgroundBlur ||
            meetingModel.videoModel.isUsingBackgroundReplacement) {
            meetingModel.notifyHandler?("Cannot toggle more than one filter at same time")
            return
        }
        meetingModel.videoModel.isUsingMetalVideoProcessor.toggle()
    }

    @objc private func toggleCustomCameraSource(nextSourceType: String) {
        logger.info(msg: "Selecting \(nextSourceType) camera source")
        guard let meetingModel = meetingModel else {
            return
        }
        meetingModel.videoModel.isUsingExternalVideoSource.toggle()
    }

    @objc private func togglePrimaryMeetingPromotion() {
        guard let meetingModel = meetingModel else {
            return
        }
        if !meetingModel.isPromotedToPrimaryMeeting {
            if let credentials = meetingModel.primaryMeetingMeetingSessionCredentials {
                // Reuse previously retrieved crednetials
                meetingModel.videoModel.promoteToPrimaryMeeting(
                    credentials: credentials, observer: self)
            } else {
                JoinRequestService.postJoinRequest(meetingId: meetingModel.primaryExternalMeetingId,
                                                   name: "promoted-\(meetingModel.selfName)",
                                                   overriddenEndpoint: MeetingModule.shared().cachedOverriddenEndpoint,
                                                   primaryExternalMeetingId: "") { joinMeetingResponse in
                    if let joinMeetingResponse = joinMeetingResponse {
                        self.logger.info(msg: "Attempting to promote to primary meeting")
                        let meetingResp = JoinRequestService.getCreateMeetingResponse(from: joinMeetingResponse)
                        let attendeeResp = JoinRequestService.getCreateAttendeeResponse(from: joinMeetingResponse)
                        let meetingSessionConfiguration = MeetingSessionConfiguration(
                            createMeetingResponse: meetingResp,
                            createAttendeeResponse: attendeeResp,
                            urlRewriter: { (url: String) -> String in return url })
                        meetingModel.primaryMeetingMeetingSessionCredentials = meetingSessionConfiguration.credentials
                        meetingModel.videoModel.promoteToPrimaryMeeting(
                            credentials: meetingSessionConfiguration.credentials, observer: self)
                    }
                }
            }
        } else {
            meetingModel.videoModel.demoteFromPrimaryMeeting()
        }
    }

    @objc private func presentVideoConfigAlertController() {
        let videoConfigAlertController = UIAlertController(title: "Video Configuration",
                                                           message: nil,
                                                           preferredStyle: .alert)
        videoConfigAlertController.addTextField { textField in
            textField.keyboardType = .numberPad
            textField.placeholder = "Local Video Max Bitrate in kbps"
        }
        let doneAction = UIAlertAction(title: "Done",
                                       style: .default) { [weak videoConfigAlertController] _ in
            guard let textFields = videoConfigAlertController?.textFields else {
                return
            }
            let maxBitRateInKbps = UInt32(textFields[0].text ?? "") ?? 0
            if let videoModel = self.meetingModel?.videoModel {
                videoModel.localVideoMaxBitRateKbps = maxBitRateInKbps
                // if local video is started, restart
                if videoModel.isLocalVideoActive {
                    videoModel.isLocalVideoActive = true
                }
            }
        }
        videoConfigAlertController.addAction(doneAction)
        present(videoConfigAlertController, animated: true)
    }
    private func enableOrDisableButtonsForReplicatedMeeting(enabled: Bool) {
        muteButton.isEnabled = enabled
        cameraButton.isEnabled = enabled
        sendMessageButton.isEnabled = enabled
        muteButton.isEnabled = enabled
        broadcastButton.isEnabled = enabled
    }

    private func nextOnOrOff(current: Bool) -> String {
        return current ? "off" : "on"
    }
}

// MARK: UICollectionViewDelegateFlowLayout

extension MeetingViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_: UICollectionView,
                        layout _: UICollectionViewLayout,
                        sizeForItemAt _: IndexPath) -> CGSize {
        var width = view.frame.width
        var height = view.frame.height
        if UIApplication.shared.statusBarOrientation.isLandscape {
            height /= 2.0
            width = height / 9.0 * 16.0
        } else {
            height = width / 16.0 * 9.0
        }
        return CGSize(width: width, height: height)
    }

    func collectionView(_: UICollectionView,
                        layout _: UICollectionViewLayout,
                        insetForSectionAt _: Int) -> UIEdgeInsets {
        return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
    }

    func collectionView(_: UICollectionView,
                        layout _: UICollectionViewLayout,
                        minimumLineSpacingForSectionAt _: Int) -> CGFloat {
        return 8
    }
}

// MARK: UICollectionViewDataSource

extension MeetingViewController: UICollectionViewDataSource {
    func numberOfSections(in _: UICollectionView) -> Int {
        // Only one section for all video tiles
        return 1
    }

    func collectionView(_: UICollectionView,
                        numberOfItemsInSection _: Int) -> Int {
        guard let meetingModel = meetingModel else {
            return 0
        }
        return meetingModel.videoModel.videoTileCount
    }

    func collectionView(_ collectionView: UICollectionView,
                        cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let meetingModel = meetingModel, indexPath.item < meetingModel.videoModel.videoTileCount else {
            return UICollectionViewCell()
        }
        let isSelf = indexPath.item == 0
        let videoTileState = meetingModel.videoModel.getVideoTileState(for: indexPath)
        let displayName = meetingModel.getVideoTileDisplayName(for: indexPath)
        let attendeeId = meetingModel.getVideoTileAttendeeId(for: indexPath)

        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: videoTileCellReuseIdentifier,
                                                            for: indexPath) as? VideoTileCell else {
            return VideoTileCell()
        }

        cell.updateCell(id: attendeeId,
                        name: displayName,
                        isSelf: isSelf,
                        videoTileState: videoTileState,
                        tag: indexPath.row)
        cell.delegate = meetingModel.videoModel
        cell.viewController = self

        if let tileState = videoTileState {
            if tileState.isLocalTile, !tileState.isContent, meetingModel.videoModel.isFrontCameraActive {
                cell.videoRenderView.mirror = true
            }
            meetingModel.bind(videoRenderView: cell.videoRenderView, tileId: tileState.tileId)
        }
        return cell
    }
}

// MARK: PrimaryMeetingPromotionObserver

extension MeetingViewController: PrimaryMeetingPromotionObserver {
    func didPromoteToPrimaryMeeting(status: MeetingSessionStatus) {
        guard let meetingModel = meetingModel else {
            return
        }
        self.logger.info(msg: "Primary meeting promotion completed with status \(status.statusCode.description)")
        if status.statusCode == MeetingSessionStatusCode.ok {
            self.view.makeToast("Successfully promote to primary meeting")
            meetingModel.isPromotedToPrimaryMeeting = true
            self.enableOrDisableButtonsForReplicatedMeeting(enabled: true)
        } else {
            self.view.makeToast("Failed to promote to primary meeting")
            meetingModel.isPromotedToPrimaryMeeting = false
            self.enableOrDisableButtonsForReplicatedMeeting(enabled: false)
        }
    }

    func didDemoteFromPrimaryMeeting(status: MeetingSessionStatus) {
        guard let meetingModel = meetingModel else {
            return
        }
        self.logger.info(msg: "Primary meeting demotion completed with status \(status.statusCode.description)")
        meetingModel.isPromotedToPrimaryMeeting = false
        self.enableOrDisableButtonsForReplicatedMeeting(enabled: false)
    }
}


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

import AmazonChimeSDK
import UIKit

let userDefaultsKeyBroadcastMetrics = "demoMeetingBroadcastMetrics"

class MetricsModel: NSObject {
    let appGroupUserDefaults = UserDefaults(suiteName: AppConfiguration.appGroupId)
    var userDefaultsObserver: NSKeyValueObservation?

    private var appMetrics = [String: Double]()
    private var broadcastMetrics = [String: Double]()
    private var combinedMetrics: [String: Double] {
        return appMetrics.merging(broadcastMetrics) { (metric, _) in metric }
    }

    var metricsUpdatedHandler: (() -> Void)?

    override init() {
        super.init()

        // Since Broadcast Extension runs independently from Demo app, Metrics from Broadcasting does not
        // automatically flow into the app. So we are saving these metrics from Broadcast Extension into
        // shared App Groups User Defaults, observe and retrieve them in MetricsModel to display in demo app.
        userDefaultsObserver = appGroupUserDefaults?.observe(\.demoMeetingBroadcastMetrics,
                                                 options: [.new, .old]) { [weak self] (_, _) in
            if let strongSelf = self,
               let userDefaults = strongSelf.appGroupUserDefaults,
               let broadcastMetrics = userDefaults.demoMeetingBroadcastMetrics {
                strongSelf.broadcastMetrics = broadcastMetrics
            } else {
                self?.broadcastMetrics = [:]
            }
            self?.metricsUpdatedHandler?()
        }
    }

    deinit {
        userDefaultsObserver?.invalidate()
    }

    func updateAppMetrics(metrics: [AnyHashable: Any]) {
        self.appMetrics = [:]
        for key in metrics.keys {
            if let key = key as? ObservableMetric {
                self.appMetrics[key.description] = metrics[key] as? Double ?? 0
            }
        }
    }

    private func getMetricsName(index: Int) -> String {
        return Array(combinedMetrics.keys).sorted(by: <)[index]
    }

    private func getMetricsValue(index: Int) -> String {
        let key = getMetricsName(index: index)
        return String(combinedMetrics[key] ?? 0)
    }
}

// MARK: UITableViewDelegate, UITableViewDataSource

extension MetricsModel: UITableViewDelegate, UITableViewDataSource {
    func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
        return combinedMetrics.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if indexPath.item >= combinedMetrics.count {
            return UITableViewCell()
        }
        let metricsName = getMetricsName(index: indexPath.item)
        let metricsValue = getMetricsValue(index: indexPath.item)
        guard let cell = tableView.dequeueReusableCell(withIdentifier: metricsTableCellReuseIdentifier) as? MetricsTableCell else {
            return MetricsTableCell(name: metricsName, value: metricsValue)
        }

        cell.updateCell(name: metricsName, value: metricsValue)

        return cell
    }
}

extension UserDefaults {
    @objc dynamic var demoMeetingBroadcastMetrics: [String: Double]? {
        return object(forKey: userDefaultsKeyBroadcastMetrics) as? [String: Double]
    }
}


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

import UIKit

let metricsTableCellReuseIdentifier = "metricsCell"

class MetricsTableCell: UITableViewCell {
    @IBOutlet var nameLabel: UILabel!
    @IBOutlet var valueLabel: UILabel!

    init(name: String, value: String) {
        super.init(style: .default, reuseIdentifier: metricsTableCellReuseIdentifier)

        updateCell(name: name, value: value)
    }

    func updateCell(name: String, value: String) {
        nameLabel.text = name
        nameLabel.accessibilityIdentifier = "\(name) metric"
        valueLabel.text = value
        valueLabel.accessibilityIdentifier = "\(name) value"
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}


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

import AmazonChimeSDK
import UIKit

class RosterModel: NSObject {
    private static let contentDelimiter = "#content"
    private static let contentSuffix = "<<Content>>"
    private let logger = ConsoleLogger(name: "RosterModel")

    private var activeSpeakerIds: [String] = []
    private var attendees = [RosterAttendee]()
    private var currentRoster = [String: RosterAttendee]()

    var rosterUpdatedHandler: (() -> Void)?

    static func convertAttendeeName(from info: AttendeeInfo) -> String {
        // The JS SDK Serverless demo will prepend a UUID to provided names followed by a hash to help uniqueness
        let externalUserIdArray = info.externalUserId.components(separatedBy: "#")
        if externalUserIdArray.isEmpty {
            return "<UNKNOWN>"
        }
        let rosterName: String = externalUserIdArray.count == 2 ? externalUserIdArray[1] : info.externalUserId
        return info.attendeeId.hasSuffix(contentDelimiter) ? "\(rosterName) \(contentSuffix)" : rosterName
    }

    func addAttendees(_ newAttendees: [RosterAttendee]) {
        if newAttendees.isEmpty {
            return
        }
        for attendee in newAttendees {
            currentRoster[attendee.attendeeId] = attendee
        }
        attendees.append(contentsOf: newAttendees)
        attendees.sort(by: attendeeSortPredicate)
    }

    func removeAttendees(_ attendeeIds: [String]) {
        for attendeeId in attendeeIds {
            currentRoster.removeValue(forKey: attendeeId)
        }
        attendees = currentRoster.values.sorted(by: attendeeSortPredicate)
    }

    func contains(attendeeId: String) -> Bool {
        return currentRoster[attendeeId] != nil
    }

    func getAttendeeName(for attendeeId: String) -> String? {
        if let attendee = currentRoster[attendeeId] {
            return attendee.attendeeName
        } else {
            return nil
        }
    }

    func getAttendee(at index: Int) -> RosterAttendee? {
        if index >= attendees.count {
            return nil
        }
        return attendees[index]
    }

    func updateVolume(attendeeId: String, volume: VolumeLevel) {
        guard let attendee = currentRoster[attendeeId] else {
            logger.error(msg: "Cannot find attendee with attendee id \(attendeeId)")
            return
        }
        if attendee.volume != volume {
            attendee.volume = volume
            if let name = attendee.attendeeName {
                logger.info(msg: "Volume changed for \(name): \(volume)")
            }
        }
    }

    func updateSignal(attendeeId: String, signal: SignalStrength) {
        guard let attendee = currentRoster[attendeeId] else {
            logger.error(msg: "Cannot find attendee with attendee id \(attendeeId)")
            return
        }
        if attendee.signal != signal {
            attendee.signal = signal
        }
    }

    func updateActiveSpeakers(_ activeSpeakerIds: [String]) {
        self.activeSpeakerIds = activeSpeakerIds
    }

    private func isActiveSpeaker(attendeeId: String) -> Bool {
        return activeSpeakerIds.contains(attendeeId)
    }

    private let attendeeSortPredicate: (RosterAttendee, RosterAttendee) -> Bool = {
        if let name0 = $0.attendeeName, let name1 = $1.attendeeName {
            return name0 < name1
        }
        return false
    }
}

// MARK: UITableViewDelegate, UITableViewDataSource

extension RosterModel: UITableViewDelegate, UITableViewDataSource {
    func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
        return attendees.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let currentAttendee = getAttendee(at: indexPath.item) else {
            return UITableViewCell()
        }
        let isActiveSpeaker = self.isActiveSpeaker(attendeeId: currentAttendee.attendeeId)
        guard let cell = tableView.dequeueReusableCell(withIdentifier: rosterTableCellReuseIdentifier) as? RosterTableCell else {
            return RosterTableCell(attendee: currentAttendee, isActiveSpeaker: isActiveSpeaker)
        }
        cell.updateCell(attendee: currentAttendee, isActiveSpeaker: isActiveSpeaker)

        return cell
    }
}


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

import AmazonChimeSDK
import UIKit

let rosterTableCellReuseIdentifier: String = "rosterCell"

class RosterTableCell: UITableViewCell {
    @IBOutlet var attendeeName: UILabel!
    @IBOutlet var indicator: UIView!
    @IBOutlet var speakLevel: UIImageView!

    init(attendee: RosterAttendee, isActiveSpeaker: Bool) {
        super.init(style: .default, reuseIdentifier: rosterTableCellReuseIdentifier)

        updateCell(attendee: attendee, isActiveSpeaker: isActiveSpeaker)
    }

    func updateCell(attendee: RosterAttendee, isActiveSpeaker: Bool) {
        attendeeName.text = attendee.attendeeName
        attendeeName.accessibilityIdentifier = attendee.attendeeName

        if attendee.volume == .notSpeaking {
            accessibilityIdentifier = "\(attendee.attendeeName ?? "") Not Speaking"
        } else if attendee.volume == .muted {
            accessibilityIdentifier = "\(attendee.attendeeName ?? "") Muted"
        } else {
            accessibilityIdentifier = "\(attendee.attendeeName ?? "") Speaking"
        }

        indicator.isHidden = !isActiveSpeaker
        indicator.accessibilityIdentifier = isActiveSpeaker ? "\(attendee.attendeeName ?? "") Active" : ""
        indicator.layer.cornerRadius = indicator.frame.size.width / 2.0
        speakLevel.tintColor = .systemGray

        speakLevel.image = getSpeakLevelImage(signal: attendee.signal, volume: attendee.volume)
    }

    private func getSpeakLevelImage(signal: SignalStrength, volume: VolumeLevel) -> UIImage? {
        var image: UIImage?
        if signal != .high {
            if volume == .muted {
                image = UIImage(named: "signal-poor-muted")
            } else {
                image = UIImage(named: "signal-poor")
            }
        } else {
            switch volume {
            case .muted:
                image = UIImage(named: "volume-muted")
            case .notSpeaking:
                image = UIImage(named: "volume-0")
            case .low:
                image = UIImage(named: "volume-1")
            case .medium:
                image = UIImage(named: "volume-2")
            case .high:
                image = UIImage(named: "volume-3")
            @unknown default:
                break
            }
        }
        return image?.withRenderingMode(.alwaysTemplate)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
}


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

import AmazonChimeSDK
import Foundation

class ScreenShareModel: NSObject {
    let inAppCaptureModel: InAppScreenCaptureModel
    let broadcastCaptureModel: BroadcastScreenCaptureModel

    var tileId: Int? {
        didSet {
            tileIdDidSetHandler?(tileId)
        }
    }

    var isAvailable: Bool {
        return tileId != nil
    }

    var tileIdDidSetHandler: ((Int?) -> Void)?
    var viewUpdateHandler: ((Bool) -> Void)?

    init(meetingSessionConfig: MeetingSessionConfiguration,
         contentShareController: ContentShareController) {
        self.broadcastCaptureModel = BroadcastScreenCaptureModel(meetingSessionConfig: meetingSessionConfig)
        self.inAppCaptureModel = InAppScreenCaptureModel(contentShareController: contentShareController)
        super.init()
        inAppCaptureModel.isSharingHandler = { [weak self] isSharing in
            if isSharing {
                self?.broadcastCaptureModel.isBlocked = true
            } else {
                self?.broadcastCaptureModel.isBlocked = false
            }
        }
    }

    func stopLocalSharing() {
        inAppCaptureModel.isSharing = false
        broadcastCaptureModel.isBlocked = true
    }
}


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

import AmazonChimeSDK
import UIKit

class VideoModel: NSObject {
    private let remoteVideoTileCountPerPage = 6

    private var currentRemoteVideoPageIndex = 0

    private var selfVideoTileState: VideoTileState?
    private var remoteVideoTileStates: [(Int, VideoTileState)] = []
    private var userPausedVideoTileIds: Set<Int> = Set()
    private var remoteVideoSourceConfigurations: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration> = Dictionary()
    private var contentShareRemoteVideoSourceConfigurations: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration> = Dictionary()
    let audioVideoFacade: AudioVideoFacade
    let customSource: DefaultCameraCaptureSource

    var videoUpdatedHandler: (() -> Void)?
    var videoSubscriptionUpdatedHandler: (() -> Void)?
    var localVideoUpdatedHandler: (() -> Void)?
    let logger = ConsoleLogger(name: "VideoModel")

    private let backgroundBlurProcessor: BackgroundBlurVideoFrameProcessor
    private var backgroundReplacementProcessor: BackgroundReplacementVideoFrameProcessor
    private var backgroundImage: UIImage?

    private var videoSourcesToBeSubscribed: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration> = Dictionary()
    private var videoSourcesToBeUnsubscribed: Set<RemoteVideoSource> = Set()

    var cameraSendIsAvailable: Bool = false

    var pendingRemoteVideoSourceConfigurations: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration> = Dictionary()

    init(audioVideoFacade: AudioVideoFacade, eventAnalyticsController: EventAnalyticsController) {
        self.audioVideoFacade = audioVideoFacade
        self.customSource = DefaultCameraCaptureSource(logger: ConsoleLogger(name: "CustomCameraSource"))
        self.customSource.setEventAnalyticsController(eventAnalyticsController: eventAnalyticsController)

        // Create the background replacement image.
        let rect = CGRect(x: 0,
                          y: 0,
                          width: self.customSource.format.width,
                          height: self.customSource.format.height)
        UIGraphicsBeginImageContextWithOptions(CGSize(width: self.customSource.format.width,
                                                      height: self.customSource.format.height),
                                               false, 0)
        UIColor.blue.setFill()
        UIRectFill(rect)
        let backgroundReplacementImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        let backgroundReplacementConfigurations = BackgroundReplacementConfiguration(logger: ConsoleLogger(name: "BackgroundReplacementProcessor"),
                                                                                backgroundReplacementImage: backgroundReplacementImage)
        self.backgroundReplacementProcessor = BackgroundReplacementVideoFrameProcessor(backgroundReplacementConfiguration: backgroundReplacementConfigurations)

        let backgroundBlurConfigurations = BackgroundBlurConfiguration(logger: ConsoleLogger(name: "BackgroundBlurProcessor"),
                                                                       blurStrength: BackgroundBlurStrength.low)
        self.backgroundBlurProcessor = BackgroundBlurVideoFrameProcessor(backgroundBlurConfiguration: backgroundBlurConfigurations)

        super.init()
    }

    var localVideoMaxBitRateKbps: UInt32 = 0

    var videoTileCount: Int {
        return remoteVideoCountInCurrentPage + 1
    }

    var canGoToPrevRemoteVideoPage: Bool {
        return currentRemoteVideoPageIndex > 0
    }

    var canGoToNextRemoteVideoPage: Bool {
        let maxRemoteVideoPageIndex = Int(ceil(Double(currentRemoteVideoCount) / Double(remoteVideoTileCountPerPage))) - 1
        return currentRemoteVideoPageIndex < maxRemoteVideoPageIndex
    }

    var isLocalVideoActive = false {
        willSet(isLocalVideoActive) {
            if isLocalVideoActive {
                customSource.start()
                startLocalVideo()
            } else {
                customSource.stop()
                stopLocalVideo()
            }
        }
    }

    var isEnded = false {
        didSet(isEnded) {
            if isEnded {
                for tile in remoteVideoTileStates {
                    audioVideoFacade.unbindVideoView(tileId: tile.0)
                }
                if isLocalVideoActive, let selfTile = selfVideoTileState {
                    audioVideoFacade.unbindVideoView(tileId: selfTile.tileId)
                }

                if isUsingExternalVideoSource {
                    self.customSource.removeVideoSink(sink: self.coreImageVideoProcessor)
                    self.customSource.removeVideoSink(sink: self.backgroundBlurProcessor)
                    self.customSource.removeVideoSink(sink: self.backgroundReplacementProcessor)
                    if isUsingMetalVideoProcessor, let metalProcessor = self.metalVideoProcessor {
                        self.customSource.removeVideoSink(sink: metalProcessor)
                    }
                }

                isLocalVideoActive = false

                audioVideoFacade.stopRemoteVideo()
                customSource.torchEnabled = false
            }
        }
    }

    var isFrontCameraActive: Bool {
        // See comments above isUsingExternalVideoSource
        if let internalCamera = audioVideoFacade.getActiveCamera() {
            return internalCamera.type == .videoFrontCamera
        }
        if let activeCamera = customSource.device {
            return activeCamera.type == .videoFrontCamera
        }
        return false
    }

    // To facilitate demoing and testing both use cases, we account for both our external
    // camera and the camera managed by the facade. Actual applications should
    // only use one or the other
    var isUsingExternalVideoSource = true {
        didSet {
            if isLocalVideoActive {
                startLocalVideo()
            }
        }
    }

    private let coreImageVideoProcessor = CoreImageVideoProcessor()
    var isUsingCoreImageVideoProcessor = false {
        didSet {
            if isLocalVideoActive {
                startLocalVideo()
            }
        }
    }

    // See comments in MetalVideoProcessor
    private let metalVideoProcessor = MetalVideoProcessor()
    var isUsingMetalVideoProcessor = false {
        didSet {
            if isLocalVideoActive {
                startLocalVideo()
            }
        }
    }

    var isUsingBackgroundBlur = false {
        didSet {
            if isLocalVideoActive{
                startLocalVideo()
            }
        }
    }

    var isUsingBackgroundReplacement = false {
        didSet {
            if isLocalVideoActive {
                startLocalVideo()
            }
        }
    }

    private var currentRemoteVideoCount: Int {
        return remoteVideoTileStates.count
    }

    private var remoteVideoStatesInCurrentPage: [(Int, VideoTileState)] {
        let remoteVideoStartIndex = currentRemoteVideoPageIndex * remoteVideoTileCountPerPage
        let remoteVideoEndIndex = min(currentRemoteVideoCount, remoteVideoStartIndex + remoteVideoTileCountPerPage) - 1

        if remoteVideoEndIndex < remoteVideoStartIndex {
            return []
        }
        return Array(remoteVideoTileStates[remoteVideoStartIndex ... remoteVideoEndIndex])
    }

    private var remoteVideoStatesNotInCurrentPage: [(Int, VideoTileState)] {
        let remoteVideoAttendeeIdsInCurrentPage = Set(remoteVideoStatesInCurrentPage.map { $0.1.attendeeId })
        return remoteVideoTileStates.filter { !remoteVideoAttendeeIdsInCurrentPage.contains($0.1.attendeeId) }
    }

    private var remoteVideoCountInCurrentPage: Int {
        return remoteVideoStatesInCurrentPage.count
    }

    private func startLocalVideo() {
        MeetingModule.shared().requestVideoPermission { success in
            if success {
                // See comments above isUsingExternalVideoSource
                if self.isUsingExternalVideoSource {
                    var customVideoSource: VideoSource = self.customSource
                    customVideoSource.removeVideoSink(sink: self.coreImageVideoProcessor)
                    customVideoSource.removeVideoSink(sink: self.backgroundBlurProcessor)
                    customVideoSource.removeVideoSink(sink: self.backgroundReplacementProcessor)
                    if let metalVideoProcessor = self.metalVideoProcessor {
                        customVideoSource.removeVideoSink(sink: metalVideoProcessor)
                    }

                    if self.isUsingCoreImageVideoProcessor {
                        customVideoSource.addVideoSink(sink: self.coreImageVideoProcessor)
                        customVideoSource = self.coreImageVideoProcessor
                    } else if self.isUsingMetalVideoProcessor, let metalVideoProcessor = self.metalVideoProcessor {
                        customVideoSource.addVideoSink(sink: metalVideoProcessor)
                        customVideoSource = metalVideoProcessor
                    } else if self.isUsingBackgroundBlur {
                        customVideoSource.addVideoSink(sink: self.backgroundBlurProcessor)
                        customVideoSource = self.backgroundBlurProcessor
                    } else if self.isUsingBackgroundReplacement {
                        customVideoSource.addVideoSink(sink: self.backgroundReplacementProcessor)
                        customVideoSource = self.backgroundReplacementProcessor
                    }
                    // customers could set simulcast here
                    let config = LocalVideoConfiguration(maxBitRateKbps: self.localVideoMaxBitRateKbps)
                    self.audioVideoFacade.startLocalVideo(source: customVideoSource,
                                                          config: config)
                } else {
                    do {
                        try self.audioVideoFacade.startLocalVideo()
                    } catch {
                        self.logger.error(msg: "Error starting local video: \(error.localizedDescription)")
                    }
                }
            }
        }
    }

    private func stopLocalVideo() {
        audioVideoFacade.stopLocalVideo()
        // See comments above isUsingExternalVideoSource
        if isUsingExternalVideoSource {
            customSource.stop()
        }
    }

    private func moveVideoSourceFromPendingToNormalByAttendeeId(attendeeId: String) {
        let pendingAttendeeKeyMap = pendingRemoteVideoSourceConfigurations.keys.reduce(into: [String: RemoteVideoSource]()) {
            $0[$1.attendeeId] = $1
        }

        if let source = pendingAttendeeKeyMap[attendeeId] {
            if pendingRemoteVideoSourceConfigurations[source] != nil {
                remoteVideoSourceConfigurations[source] = pendingRemoteVideoSourceConfigurations[source]
                pendingRemoteVideoSourceConfigurations.removeValue(forKey: source)
            }
        }
    }

    private func isAttendeeIdContentShare(attendeeId: String) -> Bool {
        return DefaultModality(id: attendeeId).isOfType(type: .content)
    }

    private func addVideoSourcesToBeSubscribed (toBeAddedOrUpdated: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration>) {
        for (source, config) in toBeAddedOrUpdated {
            videoSourcesToBeSubscribed[source] = config
            videoSourcesToBeUnsubscribed.remove(source)
        }
    }

    private func addVideoSourcesToBeUnsubscribed (toBeRemoved: Array<RemoteVideoSource>) {
        for source in toBeRemoved {
            videoSourcesToBeSubscribed.removeValue(forKey: source)
            videoSourcesToBeUnsubscribed.insert(source)
        }
    }

    func promoteToPrimaryMeeting(credentials: MeetingSessionCredentials, observer: PrimaryMeetingPromotionObserver) {
        audioVideoFacade.promoteToPrimaryMeeting(credentials: credentials, observer: observer)
    }

    func demoteFromPrimaryMeeting() {
        audioVideoFacade.demoteFromPrimaryMeeting()
    }

    func isRemoteVideoDisplaying(tileId: Int) -> Bool {
        return remoteVideoStatesInCurrentPage.contains(where: { $0.0 == tileId })
    }

    func updateRemoteVideoStatesBasedOnActiveSpeakers(activeSpeakers: [AttendeeInfo], inVideoMode: Bool = false) {
        let activeSpeakerIds = Set(activeSpeakers.map { $0.attendeeId })
        var videoTilesOrderUpdated = false

        // Cast to NSArray to make sure the sorting implementation is stable
        remoteVideoTileStates = (remoteVideoTileStates as NSArray).sortedArray(options: .stable,
                                                                               usingComparator: { (lhs, rhs) -> ComparisonResult in
            let lhsIsActiveSpeaker = activeSpeakerIds.contains((lhs as? (Int, VideoTileState))?.1.attendeeId ?? "")
            let rhsIsActiveSpeaker = activeSpeakerIds.contains((rhs as? (Int, VideoTileState))?.1.attendeeId ?? "")

            if lhsIsActiveSpeaker == rhsIsActiveSpeaker {
                return ComparisonResult.orderedSame
            } else if lhsIsActiveSpeaker && !rhsIsActiveSpeaker {
                return ComparisonResult.orderedAscending
            } else {
                videoTilesOrderUpdated = true
                return ComparisonResult.orderedDescending
            }
        }) as? [(Int, VideoTileState)] ?? []
        for remoteVideoTileState in remoteVideoStatesNotInCurrentPage {
            audioVideoFacade.pauseRemoteVideoTile(tileId: remoteVideoTileState.0)
        }
        if videoTilesOrderUpdated && inVideoMode {
            videoSubscriptionUpdatedHandler?()
            updateVideoSourceSubscription()
        }
    }

    func setSelfVideoTileState(_ videoTileState: VideoTileState?) {
        selfVideoTileState = videoTileState
    }

    func addRemoteVideoTileState(_ videoTileState: VideoTileState, completion: @escaping () -> Void) {
        remoteVideoTileStates.append((videoTileState.tileId, videoTileState))
        moveVideoSourceFromPendingToNormalByAttendeeId(attendeeId: videoTileState.attendeeId)
        completion()
    }

    func removeRemoteVideoTileState(_ videoTileState: VideoTileState, completion: @escaping (Bool) -> Void) {
        if let index = remoteVideoTileStates.firstIndex(where: { $0.0 == videoTileState.tileId }) {
            remoteVideoTileStates.remove(at: index)
            completion(true)
        } else {
            completion(false)
        }
    }

    func updateRemoteVideoTileState(_ videoTileState: VideoTileState) {
        if let index = remoteVideoTileStates.firstIndex(where: { $0.0 == videoTileState.tileId }) {
            remoteVideoTileStates[index] = (videoTileState.tileId, videoTileState)
            videoUpdatedHandler?()
        }
    }

    func getPreviousRemoteVideoPage() {
        let removedList: [RemoteVideoSource] = getRemoteVideoSubscriptionsFromRemoteVideoTileStates(remoteVideoTileStates: remoteVideoStatesInCurrentPage)
        addVideoSourcesToBeUnsubscribed(toBeRemoved: removedList)
        currentRemoteVideoPageIndex -= 1
    }

    func getNextRemoteVideoPage() {
        let removedList: [RemoteVideoSource] = getRemoteVideoSubscriptionsFromRemoteVideoTileStates(remoteVideoTileStates: remoteVideoStatesInCurrentPage)
        addVideoSourcesToBeUnsubscribed(toBeRemoved: removedList)
        currentRemoteVideoPageIndex += 1
    }

    func revalidateRemoteVideoPageIndex() {
        while canGoToPrevRemoteVideoPage, remoteVideoCountInCurrentPage == 0 {
            getPreviousRemoteVideoPage()
        }
    }

    func resumeAllRemoteVideosInCurrentPageExceptUserPausedVideos() {
        for remoteVideoTileState in remoteVideoStatesInCurrentPage {
            if !userPausedVideoTileIds.contains(remoteVideoTileState.0) {
                audioVideoFacade.resumeRemoteVideoTile(tileId: remoteVideoTileState.0)
            }
        }
    }

    func updateAllRemoteVideosInCurrentPageExceptUserPausedVideos() {
        var updatedSources:[RemoteVideoSource: VideoSubscriptionConfiguration] = [:]
        let attendeeKeyMap = remoteVideoSourceConfigurations.keys.reduce(into: [String: RemoteVideoSource]()) {
            $0[$1.attendeeId] = $1
        }

        let attendeeIds = Set(remoteVideoSourceConfigurations.keys.map { $0.attendeeId })
        for remoteVideoTileState in remoteVideoStatesInCurrentPage{
                let attendeeId = String(remoteVideoTileState.1.attendeeId)
            if attendeeIds.contains(attendeeId), let key = attendeeKeyMap[attendeeId]{
                updatedSources[key] = remoteVideoSourceConfigurations[key]
            }
        }

        var videoCount:Int = remoteVideoCountInCurrentPage
        if videoCount < remoteVideoTileCountPerPage {
            for (source, _) in pendingRemoteVideoSourceConfigurations {
                updatedSources[source] = pendingRemoteVideoSourceConfigurations[source]
                videoCount += 1
                if videoCount == remoteVideoTileCountPerPage {
                    break
                }
            }
        }

        addVideoSourcesToBeSubscribed(toBeAddedOrUpdated: updatedSources)
    }

    func addContentShareVideoSource() {
        addVideoSourcesToBeSubscribed(toBeAddedOrUpdated: contentShareRemoteVideoSourceConfigurations)
    }

    func removeContentShareVideoSources() {
        addVideoSourcesToBeUnsubscribed(toBeRemoved: Array(contentShareRemoteVideoSourceConfigurations.keys))
    }

    func removeNonContentShareVideoSources() {
        addVideoSourcesToBeUnsubscribed(toBeRemoved: Array(remoteVideoSourceConfigurations.keys))
    }

    func pauseAllRemoteVideos() {
        for remoteVideoTileState in remoteVideoTileStates {
            audioVideoFacade.pauseRemoteVideoTile(tileId: remoteVideoTileState.0)
        }
    }

    func unsubscribeAllRemoteVideos() {
        addVideoSourcesToBeUnsubscribed(toBeRemoved: Array(contentShareRemoteVideoSourceConfigurations.keys))
        addVideoSourcesToBeUnsubscribed(toBeRemoved: Array(remoteVideoSourceConfigurations.keys))
    }

    func getRemoteVideoSubscriptionsFromRemoteVideoTileStates(remoteVideoTileStates: [(Int, VideoTileState)]) -> [RemoteVideoSource] {
        var remoteVideoSources: [RemoteVideoSource] = []
        let attendeeKeyMap = remoteVideoSourceConfigurations.keys.reduce(into: [String: RemoteVideoSource]()) {
            $0[$1.attendeeId] = $1
        }
        let attendeeIds = Set(remoteVideoSourceConfigurations.keys.map { $0.attendeeId })
        for remoteVideoTileState in remoteVideoTileStates {
                let attendeeId = String(remoteVideoTileState.1.attendeeId)
            if attendeeIds.contains(attendeeId), let key = attendeeKeyMap[attendeeId] {
                remoteVideoSources.append(key)
            }
        }
        return remoteVideoSources
    }

    func removeRemoteVideosNotInCurrentPage() {
        removeContentShareVideoSources()
        let remoteVideoSourcesNotInCurrPage: [RemoteVideoSource] = getRemoteVideoSubscriptionsFromRemoteVideoTileStates(remoteVideoTileStates: remoteVideoStatesNotInCurrentPage)
        addVideoSourcesToBeUnsubscribed(toBeRemoved: remoteVideoSourcesNotInCurrPage)
    }

    func getVideoTileState(for indexPath: IndexPath) -> VideoTileState? {
        if indexPath.item == 0 {
            return selfVideoTileState
        }
        if indexPath.item > remoteVideoTileCountPerPage {
            return nil
        }
        return remoteVideoStatesInCurrentPage[indexPath.item - 1].1
    }

    func toggleTorch() -> Bool {
        let desiredState = !customSource.torchEnabled
        customSource.torchEnabled = desiredState
        return customSource.torchEnabled == desiredState
    }

    func removeVideoSource(source: RemoteVideoSource) {
        remoteVideoSourceConfigurations.removeValue(forKey: source)
        pendingRemoteVideoSourceConfigurations.removeValue(forKey: source)
        contentShareRemoteVideoSourceConfigurations.removeValue(forKey: source)
    }

    func addVideoSource(source: RemoteVideoSource, config: VideoSubscriptionConfiguration) {
        if isAttendeeIdContentShare(attendeeId: source.attendeeId) {
            contentShareRemoteVideoSourceConfigurations[source] = config
        } else {
            if remoteVideoSourceConfigurations[source] == nil {
                pendingRemoteVideoSourceConfigurations[source] = config
            } else {
                remoteVideoSourceConfigurations[source] = config
            }
        }
    }

    func updateVideoSourceSubscription() {
        if videoSourcesToBeSubscribed.isEmpty && videoSourcesToBeUnsubscribed.isEmpty {
            return
        }
        audioVideoFacade.updateVideoSourceSubscriptions(addedOrUpdated: videoSourcesToBeSubscribed, removed: Array(videoSourcesToBeUnsubscribed))
        videoSourcesToBeSubscribed.removeAll()
        videoSourcesToBeUnsubscribed.removeAll()
    }
}

extension VideoModel: VideoTileCellDelegate {
    func onTileButtonClicked(tag: Int, selected: Bool) {
        if tag == 0 {
            // See comments above MeetingModel::isUsingExternalVideoSource
            if audioVideoFacade.getActiveCamera() != nil {
                audioVideoFacade.switchCamera()
            } else {
                customSource.switchCamera()
            }
        } else {
            if let tileState = getVideoTileState(for: IndexPath(item: tag, section: 0)), !tileState.isLocalTile {
                if selected {
                    userPausedVideoTileIds.insert(tileState.tileId)
                    audioVideoFacade.pauseRemoteVideoTile(tileId: tileState.tileId)
                } else {
                    userPausedVideoTileIds.remove(tileState.tileId)
                    audioVideoFacade.resumeRemoteVideoTile(tileId: tileState.tileId)
                }
            }
        }
    }

    func onUpdatePriorityButtonClicked(attendeeId: String, priority: VideoPriority) {
        var updatedSources:[RemoteVideoSource: VideoSubscriptionConfiguration] = [: ]
        for (source, config) in remoteVideoSourceConfigurations {
            if attendeeId == source.attendeeId {
                config.priority = priority
                updatedSources[source] = config
            }

        }
        audioVideoFacade.updateVideoSourceSubscriptions(addedOrUpdated: updatedSources, removed: [])
    }

    func onVideoFilterButtonClicked(videoFilter: BackgroundFilter, uiView: UIViewController) {
        switch videoFilter {
        case .none:
            if isUsingBackgroundBlur {
                isUsingBackgroundBlur.toggle()
                uiView.view.makeToast("Turning background blur off.")
            } else if isUsingBackgroundReplacement {
                isUsingBackgroundReplacement.toggle()
                uiView.view.makeToast("Turning background replacement off.")
            } else {
                uiView.view.makeToast("No video filers are on.")
            }
        case .blur:
            if isUsingMetalVideoProcessor ||
               isUsingCoreImageVideoProcessor ||
               isUsingBackgroundReplacement {
                uiView.view.makeToast("Cannot toggle more than one filter at a time.")
                return
            }
            let nextStatus = isUsingBackgroundBlur ? "off" : "on"
            uiView.view.makeToast("Turning background \(videoFilter.description) \(nextStatus).")
            isUsingBackgroundBlur.toggle()
        case .replacement:
            if isUsingMetalVideoProcessor ||
               isUsingCoreImageVideoProcessor ||
               isUsingBackgroundBlur {
                uiView.view.makeToast("Cannot toggle more than one filter at a time.")
                return
            }
            let nextStatus = isUsingBackgroundReplacement ? "off" : "on"
            uiView.view.makeToast("Turning background \(videoFilter.description) \(nextStatus).")
            isUsingBackgroundReplacement.toggle()
        @unknown default:
            self.logger.info(msg: "Unknown background filter.")
        }
    }
    func onUpdateResolutionButtonClicked(attendeeId: String, resolution: VideoResolution) {
        var updatedSources:[RemoteVideoSource: VideoSubscriptionConfiguration] = [:]
        for(source, config) in remoteVideoSourceConfigurations {
            if attendeeId == source.attendeeId {
                config.targetResolution = resolution
                updatedSources[source] = config
            }
        }
        audioVideoFacade.updateVideoSourceSubscriptions(addedOrUpdated: updatedSources, removed: [])
    }

}


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

import AmazonChimeSDK
import UIKit

let videoTileCellReuseIdentifier = "VideoTileCell"

protocol VideoTileCellDelegate: AnyObject {
    func onTileButtonClicked(tag: Int, selected: Bool)
    func onUpdatePriorityButtonClicked(attendeeId: String, priority: VideoPriority)
    func onVideoFilterButtonClicked(videoFilter: BackgroundFilter, uiView: UIViewController)
    func onUpdateResolutionButtonClicked(attendeeId: String, resolution: VideoResolution)
}

class VideoTileCell: UICollectionViewCell {
    @IBOutlet var attendeeName: UILabel!
    @IBOutlet var shadedView: UIView!
    @IBOutlet var onTileButton: UIButton!
    @IBOutlet var videoFiltersButton: UIButton!
    @IBOutlet var updateVideoPriorityButton: UIButton!
    @IBOutlet var updateVideoResolutionButton: UIButton!
    @IBOutlet var videoDisabledImage: UIImageView!
    @IBOutlet var poorConnectionBackground: UIView!
    @IBOutlet var poorConnectionImage: UIImageView!
    @IBOutlet var poorConnectionLabel: UILabel!
    @IBOutlet var videoRenderView: DefaultVideoRenderView!

    weak var delegate: VideoTileCellDelegate?
    weak var viewController: UIViewController?
    var attendeeId: String = ""

    func updateCell(id: String, name: String, isSelf: Bool, videoTileState: VideoTileState?, tag: Int) {
        let isVideoActive = videoTileState != nil
        let isVideoPausedByUser = isVideoActive && videoTileState?.pauseState == .pausedByUserRequest

        attendeeId = id
        attendeeName.text = name
        backgroundColor = .systemGray
        isHidden = false

        // Self video cell not active
        if isSelf, !isVideoActive {
            onTileButton.isHidden = true
            updateVideoPriorityButton.isHidden = true
            updateVideoResolutionButton.isHidden = true
            videoDisabledImage.image = UIImage(named: "meeting-video")?.withRenderingMode(.alwaysTemplate)
            videoDisabledImage.tintColor = .white
            videoDisabledImage.isHidden = false
            return
        }

        videoDisabledImage.isHidden = true
        videoRenderView.isHidden = false
        videoRenderView.accessibilityIdentifier = "\(name) VideoTile"

        onTileButton.tintColor = .white
        onTileButton.isHidden = false
        onTileButton.tag = tag
        onTileButton.addTarget(self, action: #selector(onTileButtonClicked), for: .touchUpInside)
        onTileButton.isSelected = isVideoPausedByUser
        videoFiltersButton.isHidden = true
        updateVideoPriorityButton.isHidden = false
        updateVideoResolutionButton.isHidden = false

        if isSelf {
            onTileButton.setImage(UIImage(named: "switch-camera")?.withRenderingMode(.alwaysTemplate),
                                  for: .normal)

            updateVideoPriorityButton.isHidden = true
            updateVideoResolutionButton.isHidden = true
            // If AmazonChimeSDKMachineLearning is not available then hide the video filter button.
            if BackgroundFilterProcessor.isAvailable() {
                videoFiltersButton.isHidden = false
            } else {
                self.viewController?.view.makeToast("AmazonChimeSDKMachineLearning is not available. " +
                                                    "See README for more information.")
            }
            videoFiltersButton.setImage(UIImage(named: "more")?.withRenderingMode(.alwaysTemplate), for: .normal)
            videoFiltersButton.tintColor = .white
            videoFiltersButton.addTarget(self, action: #selector(showVideoFiltersMenu), for: .touchUpInside)
        } else {
            onTileButton.setImage(UIImage(named: "pause-video")?.withRenderingMode(.alwaysTemplate),
                                  for: .normal)
            onTileButton.setImage(UIImage(named: "resume-video")?.withRenderingMode(.alwaysTemplate),
                                  for: .selected)
            let shouldShowPoorConnection = videoTileState?.pauseState == .pausedForPoorConnection
            renderPoorConnection(isHidden: !shouldShowPoorConnection)

            updateVideoPriorityButton.setImage(UIImage(named: "more")?.withRenderingMode(.alwaysTemplate), for: .normal)
            updateVideoPriorityButton.tintColor = .white
            updateVideoPriorityButton.addTarget(self, action: #selector(showUpdateVideoSubscriptionsMenu), for: .touchUpInside)

            updateVideoResolutionButton.setImage(UIImage(named: "up")?.withRenderingMode(.alwaysTemplate), for: .normal)
            updateVideoResolutionButton.tintColor = .white
            updateVideoResolutionButton.addTarget(self, action: #selector(showUpdateVideoResolutionMenu), for: .touchUpInside)
        }
    }

    @objc func showUpdateVideoSubscriptionsMenu() {
        let alertController = UIAlertController(title: "Set video priority", message: "Choose the display priority order for the selected video", preferredStyle: .alert)
        let priorityList = [VideoPriority.lowest, VideoPriority.low, VideoPriority.medium, VideoPriority.high, VideoPriority.highest]
        let titleList = ["Lowest", "Low", "Medium", "High", "Highest"]

        for index in 0...(priorityList.count-1) {
            let action = UIAlertAction(title: titleList[index], style: UIAlertAction.Style.default) {_ in
                self.delegate?.onUpdatePriorityButtonClicked(attendeeId: self.attendeeId, priority: priorityList[index])
            }
            alertController.addAction(action)
        }

        // Present the controller
        viewController?.present(alertController, animated: true, completion: nil)
    }

    /// Add the background filter options to be presented.
    @objc func showVideoFiltersMenu() {
        let videoFilterAlertController = UIAlertController(title: "Set video filter",
                                                           message: "Choose a video filter for the selected video",
                                                           preferredStyle: .alert)
        var filtersList = [BackgroundFilter.none]
        var titleList = ["None"]
        if BackgroundFilterProcessor.isAvailable() {
            filtersList.append(contentsOf: [BackgroundFilter.blur, BackgroundFilter.replacement])
            titleList.append(contentsOf: ["Background Blur", "Background Replacement"])
        }

        for index in 0...(filtersList.count-1) {
            let action = UIAlertAction(title: titleList[index], style: UIAlertAction.Style.default) {_ in
                self.delegate?.onVideoFilterButtonClicked(videoFilter: filtersList[index], uiView: self.viewController!)
            }
            videoFilterAlertController.addAction(action)
        }

        viewController?.present(videoFilterAlertController, animated: true, completion: {
            videoFilterAlertController.view.superview?.isUserInteractionEnabled = true
            videoFilterAlertController.view.superview?.addGestureRecognizer(UITapGestureRecognizer(target: self,
                                                                                action: #selector(self.dismissOnTapOutside)))
        })
    }

    // Dismiss controller if user tap outside.
    @objc func dismissOnTapOutside() {
        viewController?.dismiss(animated: true, completion: nil)
    }
    @objc func showUpdateVideoResolutionMenu() {
        let alertController = UIAlertController(title: "Set video resolution", message: "Choose the display resolution for the selected video", preferredStyle: .alert)
        let resolutionList = [VideoResolution.low, VideoResolution.medium, VideoResolution.high]
        let titleList = ["Low", "Medium", "High"]

        for index in 0...(resolutionList.count - 1) {
            let action = UIAlertAction(title: titleList[index], style: UIAlertAction.Style.default) {_ in
                self.delegate?.onUpdateResolutionButtonClicked(attendeeId: self.attendeeId, resolution: resolutionList[index])
            }
            alertController.addAction(action)
        }

        // Present the controller
        viewController?.present(alertController, animated: true, completion: nil)
    }

    override func prepareForReuse() {
        accessibilityIdentifier = nil
        attendeeName.isHidden = false
        contentView.isHidden = false
        isHidden = true

        onTileButton.imageView?.contentMode = UIView.ContentMode.scaleAspectFill
        updateVideoPriorityButton.imageView?.contentMode = UIView.ContentMode.scaleAspectFill
        updateVideoResolutionButton.imageView?.contentMode = UIView.ContentMode.scaleAspectFill
        shadedView.isHidden = false
        videoRenderView.backgroundColor = .systemGray
        videoRenderView.isHidden = true
        renderPoorConnection(isHidden: true)
        videoRenderView.mirror = false
        // Clean up old video image to prevent frame flicker
        videoRenderView.resetImage()
    }

    @objc func onTileButtonClicked(_ sender: UIButton) {
        sender.isSelected.toggle()
        delegate?.onTileButtonClicked(tag: sender.tag, selected: sender.isSelected)
        if sender.tag == 0 {
            videoRenderView.mirror.toggle()
        }
    }

    private func renderPoorConnection(isHidden: Bool) {
        if !isHidden {
            poorConnectionImage.image = UIImage(named: "connection-problem")!.withRenderingMode(.alwaysTemplate)
            poorConnectionImage.tintColor = .white
        }
        poorConnectionImage.isHidden = isHidden
        poorConnectionLabel.isHidden = isHidden
        poorConnectionBackground.isHidden = isHidden
    }
}


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

import AmazonChimeSDK
import Foundation

public class Caption {
    let speakerName: String
    var isPartial: Bool
    var content: String
    var entities: [String]?
    var items: [TranscriptItem]?

    init(speakerName: String, isPartial: Bool, content: String, entities: [String]? = nil, items: [TranscriptItem]? = nil) {
        self.speakerName = speakerName
        self.isPartial = isPartial
        self.content = content
        self.entities = entities
        self.items = items
    }
}


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

import Foundation

struct CreateMediaPlacementInfo: Codable {
    var audioFallbackUrl: String?
    var audioHostUrl: String
    var signalingUrl: String
    var turnControlUrl: String?
    var eventIngestionUrl: String?

    enum CodingKeys: String, CodingKey {
        case audioFallbackUrl = "AudioFallbackUrl"
        case audioHostUrl = "AudioHostUrl"
        case signalingUrl = "SignalingUrl"
        case turnControlUrl = "TurnControlUrl"
        case eventIngestionUrl = "EventIngestionUrl"
    }
}

struct CreateMeetingInfo: Codable {
    var externalMeetingId: String?
    var primaryMeetingId: String?
    var mediaPlacement: CreateMediaPlacementInfo
    var mediaRegion: String
    var meetingId: String

    enum CodingKeys: String, CodingKey {
        case externalMeetingId = "ExternalMeetingId"
        case primaryMeetingId = "PrimaryMeetingId"
        case mediaPlacement = "MediaPlacement"
        case mediaRegion = "MediaRegion"
        case meetingId = "MeetingId"
    }
}

struct CreateAttendeeInfo: Codable {
    var attendeeId: String
    var externalUserId: String
    var joinToken: String

    enum CodingKeys: String, CodingKey {
        case attendeeId = "AttendeeId"
        case externalUserId = "ExternalUserId"
        case joinToken = "JoinToken"
    }
}

struct CreateMeeting: Codable {
    var meeting: CreateMeetingInfo

    enum CodingKeys: String, CodingKey {
        case meeting = "Meeting"
    }
}

struct CreateAttendee: Codable {
    var attendee: CreateAttendeeInfo

    enum CodingKeys: String, CodingKey {
        case attendee = "Attendee"
    }
}

struct CreateJoinInfo: Codable {
    var meeting: CreateMeeting
    var attendee: CreateAttendee
    var primaryExternalMeetingId: String?

    enum CodingKeys: String, CodingKey {
        case meeting = "Meeting"
        case attendee = "Attendee"
        case primaryExternalMeetingId = "PrimaryExternalMeetingId"
    }
}

struct JoinMeetingResponse: Codable {
    var joinInfo: CreateJoinInfo

    enum CodingKeys: String, CodingKey {
        case joinInfo = "JoinInfo"
    }
}


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

import AmazonChimeSDK
import Foundation

public class RosterAttendee {
    let attendeeId: String
    let attendeeName: String?
    var volume: VolumeLevel
    var signal: SignalStrength
    var attendeeStatus: AttendeeStatus

    init(attendeeId: String, attendeeName: String, volume: VolumeLevel, signal: SignalStrength) {
        self.attendeeId = attendeeId
        self.attendeeName = attendeeName
        self.volume = volume
        self.signal = signal
        self.attendeeStatus = AttendeeStatus.joined
    }
}


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

import Foundation

enum SegmentedControlIndex: Int {
    case attendees = 0
    case chat = 1
    case video = 2
    case screen = 3
    case captions = 4
    case metrics = 5
}


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

import Foundation

public struct TranscriptionStreamParams: Codable {
    let contentIdentificationType: String?
    let contentRedactionType: String?
    let enablePartialResultsStabilization: Bool?
    let partialResultsStability: String?
    let piiEntityTypes: String?
    let languageModelName: String?
    let identifyLanguage: Bool?
    let languageOptions: String?
    let preferredLanguage: String?

    init(contentIdentificationType: String?, contentRedactionType: String?, enablePartialResultsStabilization: Bool?, partialResultsStability: String?,
         piiEntityTypes: String?, languageModelName: String?, identifyLanguage: Bool?, languageOptions: String?, preferredLanguage: String?) {

        self.contentIdentificationType = contentIdentificationType
        self.contentRedactionType = contentRedactionType
        self.enablePartialResultsStabilization = enablePartialResultsStabilization
        self.partialResultsStability = partialResultsStability
        self.piiEntityTypes = piiEntityTypes
        self.languageModelName = languageModelName
        self.identifyLanguage = identifyLanguage
        self.languageOptions = languageOptions
        self.preferredLanguage = preferredLanguage
    }
}


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

import UIKit

@IBDesignable class PaddingLabel: UILabel {

    @IBInspectable var topInset: CGFloat = 12.0
    @IBInspectable var bottomInset: CGFloat = 12.0
    @IBInspectable var leftInset: CGFloat = 12.0
    @IBInspectable var rightInset: CGFloat = 12.0
    private let cornerRadius = 8.0

    override init(frame: CGRect) {
        super.init(frame: frame)
        initView()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        initView()
    }

    private func initView() {
        self.clipsToBounds = true
        self.layer.cornerRadius = self.cornerRadius
    }

    override func drawText(in rect: CGRect) {
        let insets = UIEdgeInsets(top: topInset,
                                  left: leftInset,
                                  bottom: bottomInset,
                                  right: rightInset)
        super.drawText(in: rect.inset(by: insets))
    }

    override var intrinsicContentSize: CGSize {
        let size = super.intrinsicContentSize
        return CGSize(width: size.width + leftInset + rightInset,
                      height: size.height + topInset + bottomInset)
    }

    override var bounds: CGRect {
        didSet {
            preferredMaxLayoutWidth = bounds.width - (leftInset + rightInset)
        }
    }
}


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

import UIKit

extension UIView {

    private var toasts: [UIView] {
        return self.subviews.filter { $0 is ToastView }
    }

    func makeToast(_ message: String,
                   duration: Double = ToastView.defaultDuration,
                   position: ToastPosition = .bottom) {
        let toast = ToastView(frame: CGRect.zero)
        toast.show(message, inView: self, duration: duration, position: position)
    }

    func hideToast() {
        self.toasts.forEach { toast in
            if let toastView = toast as? ToastView {
                toastView.dismiss()
            }
        }
    }

}


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

public enum ToastPosition {
    case top
    case center
    case bottom
}


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

import UIKit

final class ToastView: PaddingLabel {

    static let defaultDuration = 2.0

    private let autoDismissAnimationDuration = 1.0
    private let dismissAnimationDuration = 0.2
    private let dismissAnimationDelay = 0.0
    private let paddingTop = 32.0
    private let paddingBottom = 32.0
    private let widthMultiplier = 0.78

    override init(frame: CGRect) {
        super.init(frame: CGRect.zero)
        self.numberOfLines = 0
        self.isUserInteractionEnabled = false
        self.textAlignment = .center
        if #available(iOS 12.0, *) {
            if self.traitCollection.userInterfaceStyle == .dark {
                self.backgroundColor = .darkGray
            } else {
                self.backgroundColor = .lightGray
            }
        } else {
            self.backgroundColor = .lightGray
        }
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func show(_ message: String,
              inView parentView: UIView,
              duration: TimeInterval,
              position: ToastPosition) {
        self.text = message
        parentView.addSubview(self)
        self.setupConstraints(position)
        parentView.bringSubviewToFront(self)
        self.dismiss(duration: autoDismissAnimationDuration,
                     delay: duration)
    }

    func dismiss() {
        self.dismiss(duration: dismissAnimationDuration,
                     delay: dismissAnimationDelay)
    }

    private func setupConstraints(_ position: ToastPosition) {
        self.translatesAutoresizingMaskIntoConstraints = false
        if let superview = self.superview {
            self.widthAnchor.constraint(lessThanOrEqualTo: superview.widthAnchor,
                                        multiplier: self.widthMultiplier).isActive = true
            self.centerXAnchor.constraint(equalTo: superview.centerXAnchor).isActive = true
            self.heightAnchor.constraint(greaterThanOrEqualToConstant: 0).isActive = true
            switch position {
            case .top:
                self.topAnchor.constraint(equalTo: superview.topAnchor,
                                          constant: self.paddingTop).isActive = true
            case .center:
                self.centerYAnchor.constraint(equalTo: superview.centerYAnchor).isActive = true
            case .bottom:
                self.bottomAnchor.constraint(equalTo: superview.bottomAnchor,
                                             constant: -self.paddingBottom).isActive = true
            }
        }
        self.setNeedsLayout()
        self.layoutIfNeeded()
    }

    private func dismiss(duration: TimeInterval,
                         delay: TimeInterval) {
        self.layer.removeAllAnimations()
        UIView.animate(withDuration: duration,
                       delay: delay,
                       options: .curveEaseOut) {
            self.alpha = 0
        } completion: { isComplete in
            if isComplete {
                 self.removeFromSuperview()
            }
        }
    }
}


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

enum CallKitOption {
    case disabled
    case incoming
    case outgoing
}


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

import AmazonChimeSDK
import CoreImage
import Foundation

class CoreImageVideoProcessor: VideoSource, VideoSink {
    var videoContentHint = VideoContentHint.motion

    private let context = CIContext()

    private var bufferPool: CVPixelBufferPool?
    private var bufferPoolWidth: Int = 0
    private var bufferPoolHeight: Int = 0

    private let sinks = NSMutableSet()

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

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

    func onVideoFrameReceived(frame: VideoFrame) {
        guard let pixelBuffer = frame.buffer as? VideoFramePixelBuffer else {
            return
        }

        let inputImage = CIImage(cvImageBuffer: pixelBuffer.pixelBuffer)
        let outputImage = inputImage.applyingFilter("CISepiaTone", parameters: [:])

        if bufferPool == nil || frame.width != bufferPoolWidth || frame.height != bufferPoolHeight {
            updateBufferPool(newWidth: frame.width, newHeight: frame.height)
        }

        guard let bufferPool = bufferPool else {
            return
        }
        var outputBuffer: CVPixelBuffer?
        CVPixelBufferPoolCreatePixelBuffer(nil, bufferPool, &outputBuffer)
        guard let validOutputBuffer = outputBuffer else {
            return
        }
        context.render(outputImage, to: validOutputBuffer)

        let processedFrame = VideoFrame(timestampNs: frame.timestampNs,
                                        rotation: frame.rotation,
                                        buffer: VideoFramePixelBuffer(pixelBuffer: validOutputBuffer))

        for sink in sinks {
            (sink as? VideoSink)?.onVideoFrameReceived(frame: processedFrame)
        }
    }

    private func updateBufferPool(newWidth: Int, newHeight: Int) {
        var attributes: [NSString: NSObject] = [:]
        attributes[kCVPixelBufferPixelFormatTypeKey] = NSNumber(value: Int(kCVPixelFormatType_32BGRA))
        attributes[kCVPixelBufferWidthKey] = NSNumber(value: newWidth)
        attributes[kCVPixelBufferHeightKey] = NSNumber(value: newHeight)
        attributes[kCVPixelBufferIOSurfacePropertiesKey] = [AnyHashable: Any]() as NSObject
        CVPixelBufferPoolCreate(nil, nil, attributes as CFDictionary?, &bufferPool)

        bufferPoolWidth = newWidth
        bufferPoolHeight = newHeight
    }
}