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

import AmazonChimeSDK
import AVFoundation
import UIKit

class JoiningViewController: UIViewController, UITextFieldDelegate {
    @IBOutlet var meetingIdTextField: UITextField!
    @IBOutlet var nameTextField: UITextField!
    @IBOutlet var versionLabel: UILabel!
    @IBOutlet var callKitOptionsPicker: UIPickerView!
    @IBOutlet var audioModeOptionsPicker: UIPickerView!
    @IBOutlet var joinButton: UIButton!
    @IBOutlet var debugSettingsButton: UIButton!

    var callKitOptions = ["Don't use CallKit", "CallKit as Incoming in 10s", "CallKit as Outgoing"]
    var audioModeOptions = ["Stereo/48KHz Audio", "Mono/48KHz Audio", "Mono/16KHz Audio"]

    private let toastDisplayDuration = 2.0
    private let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
    var debugSettingsModel: DebugSettingsModel = DebugSettingsModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        meetingIdTextField.delegate = self
        nameTextField.delegate = self

        callKitOptionsPicker.delegate = self
        callKitOptionsPicker.dataSource = self

        audioModeOptionsPicker.delegate = self
        audioModeOptionsPicker.dataSource = self

        setupHideKeyboardOnTap()
        versionLabel.text = "amazon-chime-sdk-ios@\(Versioning.sdkVersion())"
    }

    override func viewWillAppear(_: Bool) {
        callKitOptionsPicker.selectRow(0, inComponent: 0, animated: false)
        joinButton.isEnabled = true
    }

    @IBAction func joinButton(_: UIButton) {
        // CallKit Option
        let callKitOption = getSelectedCallKitOption()
        if (callKitOption == .incoming) {
            view.makeToast("You can background the app or lock screen while waiting",
                           duration: incomingCallKitDelayInSeconds)
        }

        // Audio Mode
        let audioMode = getSelectedAudioMode()

        joinMeeting(audioVideoConfig: AudioVideoConfiguration(audioMode: audioMode, callKitEnabled: callKitOption != .disabled),
                    callKitOption: callKitOption
        )
    }

    @IBAction func debugSettingsButtonClicked (_: UIButton) {
        guard let debugSettingsVC = mainStoryboard.instantiateViewController(withIdentifier: "debugSettings")
            as? DebugSettingsViewController else {
            return
        }
        debugSettingsVC.modalPresentationStyle = .fullScreen
        debugSettingsVC.model = debugSettingsModel
        self.present(debugSettingsVC, animated: true, completion: nil)
    }

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }

    func getSelectedCallKitOption() -> CallKitOption {
        switch callKitOptionsPicker.selectedRow(inComponent: 0) {
        case 1:
            return .incoming
        case 2:
            return .outgoing
        default:
            return .disabled
        }
    }

    func getSelectedAudioMode() -> AudioMode {
        switch audioModeOptionsPicker.selectedRow(inComponent: 0) {
        case 1:
            return .mono48K
        case 2:
            return .mono16K
        default:
            return .stereo48K
        }
    }

    func joinMeeting(audioVideoConfig: AudioVideoConfiguration, callKitOption: CallKitOption) {
        view.endEditing(true)
        let meetingId = meetingIdTextField.text ?? ""
        let name = nameTextField.text ?? ""

        if meetingId.isEmpty || name.isEmpty {
            DispatchQueue.main.async {
                self.view.makeToast("Meeting ID or name is invalid",
                                    duration: self.toastDisplayDuration)
            }
            return
        }

        MeetingModule.shared().prepareMeeting(meetingId: meetingId,
                                              selfName: name,
                                              audioVideoConfig: audioVideoConfig,
                                              option: callKitOption,
                                              overriddenEndpoint: debugSettingsModel.endpointUrl,
                                              primaryExternalMeetingId: debugSettingsModel.primaryExternalMeetingId) { success in
            DispatchQueue.main.async {
                if !success {
                    self.view.hideToast()
                    self.view.makeToast("Unable to join meeting please try different meeting ID",
                                        duration: self.toastDisplayDuration)
                }
            }
        }
    }
}

extension JoiningViewController: UIPickerViewDelegate {
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent _: Int) -> String? {
        if pickerView == callKitOptionsPicker {
            if row >= callKitOptions.count {
                return nil
            }
            return callKitOptions[row]
        } else if pickerView == audioModeOptionsPicker {
            if row >= audioModeOptions.count {
                return nil
            }
            return audioModeOptions[row]
        } else {
            return nil
        }
    }
}

extension JoiningViewController: UIPickerViewDataSource {
    func numberOfComponents(in _: UIPickerView) -> Int {
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent _: Int) -> Int {
        if pickerView == callKitOptionsPicker {
            return callKitOptions.count
        } else if pickerView == audioModeOptionsPicker {
            return audioModeOptions.count
        } else {
            return 0
        }
    }
}


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

import Foundation
import AmazonChimeSDK
import UIKit

let transcribeContentIdetificationType = "PII"
let transcribeMedicalContentIdentificationType = "PHI"
let transcribeMedicalEngine = "transcribe_medical"

class LiveTranscriptionOptionsViewController: UIViewController, UITextFieldDelegate {
    var model: MeetingModel?
    var storyBoard: UIStoryboard?
    var languageOptionsViewController: AutomaticLanguageOptionsViewController?
    var meetingId = ""
    var engineSelected = ""
    var languageSelected = ""
    var regionSelected = ""
    var partialResultsStabilizationOption = 0
    var piiContentIdentificationSelected = ""
    var piiContentRedactionSelected = ""
    var selectedLanguageOptions = ""
    var preferredLanguageOptions = ""
    var meetingEndpointUrl = ""

    @IBOutlet var startTranscriptionButton: UIButton!
    @IBOutlet var engineTextField: UITextField!
    @IBOutlet var languageTextField: UITextField!
    @IBOutlet var regionTextField: UITextField!
    @IBOutlet var partialResultsStabilizationTextField: UITextField!
    @IBOutlet var piiContentIdentificationTextField: UITextField!
    @IBOutlet var piiContentRedactionTextField: UITextField!
    @IBOutlet var customLanguageModelTextField: UITextField!
    @IBOutlet var enableCustomLangugeModelTextFieldSwitch: UISwitch!
    @IBOutlet var enableCustomLangugeModelLabel: UILabel!
    @IBOutlet var enablePHIIdentificaitonSwitch: UISwitch!
    @IBOutlet var enablePHIIdentificationLabel: UILabel!
    @IBOutlet var enableAutomaticLanguageIdentificationLabel: UILabel!
    @IBOutlet var enableAutomaticLanguageIdentificationSwitch: UISwitch!
    @IBOutlet var liveTranscriptionScrollView: UIScrollView!

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        return false
    }

    var engines: [String] = []
    var languages: [String] = []
    var regions: [String] = []
    var partialResultsStabilizations: [String] = []
    var piiContentIdentifications: [String] = []
    var piiContentRedactions: [String] = []
    public var languageHandler: (([String]?) -> Void)?
    public var languagesDictHandler: (([String:String]?)->Void)?

    let transcribeLanguages = [
                                "en-US",
                                "es-US",
                                "en-GB",
                                "en-AU",
                                "fr-CA",
                                "fr-FR",
                                "it-IT",
                                "de-DE",
                                "pt-BR",
                                "ja-JP",
                                "ko-KR",
                                "zh-CN"
                               ]
    let transcribeRegions = [
                                "auto",
                                "",
                                "ap-northeast-1",
                                "ap-northeast-2",
                                "ap-southeast-2",
                                "ca-central-1",
                                "eu-central-1",
                                "eu-west-1",
                                "eu-west-2",
                                "sa-east-1",
                                "us-east-1",
                                "us-east-2",
                                "us-west-2"
                            ]

    let transcribePiiContents = [
                                    "",
                                    "ALL",
                                    "BANK_ROUTING",
                                    "CREDIT_DEBIT_NUMBER",
                                    "CREDIT_DEBIT_CVV",
                                    "CREDIT_DEBIT_EXPIRY",
                                    "PIN",
                                    "EMAIL",
                                    "ADDRESS",
                                    "NAME",
                                    "PHONE",
                                    "SSN"
                               ]

    let transcribeMedicalLanguages = ["en-US"]
    let transcribeMedicalRegions = [
                                        "auto",
                                        "",
                                        "ap-southeast-2",
                                        "ca-central-1",
                                        "eu-west-1",
                                        "us-east-1",
                                        "us-east-2",
                                        "us-west-2"
                                    ]

    var enginesDict = [
        "transcribe": "Amazon Transcribe",
        "transcribe_medical": "Amazon Transcribe Medical"
    ]

    var languagesDict = [
        "en-US": "US English (en-US)",
        "es-US": "US Spanish (es-US)",
        "en-GB": "British English (en-GB)",
        "en-AU": "Australian English (en-AU)",
        "fr-CA": "Canadian French (fr-CA)",
        "fr-FR": "French (fr-FR)",
        "it-IT": "Italian (it-IT)",
        "de-DE": "German (de-DE)",
        "pt-BR": "Brazilian Portuguese (pt-BR)",
        "ja-JP": "Japanese (ja-JP)",
        "ko-KR": "Korean (ko-KR)",
        "zh-CN": "Mandarin Chinese - Mainland (zh-CN)"
    ]

    let regionsDict = [
        "auto": "Auto",
        "": "Not specified",
        "ap-northeast-1": "Japan (Tokyo)",
        "ap-northeast-2": "South Korea (Seoul)",
        "ap-southeast-2": "Australia (Sydney)",
        "ca-central-1": "Canada",
        "eu-central-1": "Germany (Frankfurt)",
        "eu-west-1": "Ireland",
        "eu-west-2": "United Kingdom (London)",
        "sa-east-1": "Brazil (SÃ£o Paulo)",
        "us-east-1": "United States (N. Virginia)",
        "us-east-2": "United States (Ohio)",
        "us-west-2": "United States (Oregon)"
    ]

    let partialResultsStabilizationsList = [
        "Partial Results Stabilization",
        "-- DEFAULT (HIGH) --",
        "Low",
        "Medium",
        "High"
    ]

    var piiContentIdentificationDict = [
        "": "PII Content Identification",
        "ALL": "ALL",
        "BANK_ROUTING": "BANK ROUTING",
        "CREDIT_DEBIT_NUMBER": "CREDIT/DEBIT NUMBER",
        "CREDIT_DEBIT_CVV": "CREDIT/DEBIT CVV",
        "CREDIT_DEBIT_EXPIRY": "CREDIT/DEBIT EXPIRY",
        "PIN": "PIN",
        "EMAIL": "EMAIL",
        "ADDRESS": "ADDRESS",
        "NAME": "NAME",
        "PHONE": "PHONE NUMBER",
        "SSN": "SSN"
    ]

    var piiContentRedactionDict = [
        "": "PII Content Redaction",
        "ALL": "ALL",
        "BANK_ROUTING": "BANK ROUTING",
        "CREDIT_DEBIT_NUMBER": "CREDIT/DEBIT NUMBER",
        "CREDIT_DEBIT_CVV": "CREDIT/DEBIT CVV",
        "CREDIT_DEBIT_EXPIRY": "CREDIT/DEBIT EXPIRY",
        "PIN": "PIN",
        "EMAIL": "EMAIL",
        "ADDRESS": "ADDRESS",
        "NAME": "NAME",
        "PHONE": "PHONE NUMBER",
        "SSN": "SSN"
    ]

    var enginePickerView = UIPickerView()
    var languagePickerView = UIPickerView()
    var regionPickerView = UIPickerView()
    var partialResultsStabilizationPickerView = UIPickerView()
    var piiContentIdentificationPickerView = UIPickerView()
    var piiContentRedactionPickerView = UIPickerView()

    private func notify(msg: String) {
        DispatchQueue.main.async {
            self.view?.makeToast(msg, duration: 2.0, position: .top)
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        liveTranscriptionScrollView.contentSize = CGSize(width: self.view.frame.width, height: self.view.frame.height+100)

        self.engines = Array(enginesDict.keys)
        self.languages = Array(languagesDict.keys)
        self.regions = Array(regionsDict.keys)
        self.partialResultsStabilizations = Array(partialResultsStabilizationsList)
        self.piiContentIdentifications = transcribePiiContents
        self.piiContentRedactions = transcribePiiContents

        self.engineTextField.delegate = self
        self.languageTextField.delegate = self
        self.regionTextField.delegate = self
        self.partialResultsStabilizationTextField.delegate = self
        self.piiContentIdentificationTextField.delegate = self
        self.piiContentRedactionTextField.delegate = self

        guard let meetingId = self.model?.meetingId, let meetingEndpointUrl = self.model?.meetingEndpointUrl else {
            return MeetingModule.shared().dismissTranscription(self)
        }
        self.meetingId = meetingId
        self.meetingEndpointUrl = meetingEndpointUrl
        engineTextField.inputView = enginePickerView
        languageTextField.inputView = languagePickerView
        regionTextField.inputView = regionPickerView
        partialResultsStabilizationTextField.inputView = partialResultsStabilizationPickerView
        piiContentIdentificationTextField.inputView = piiContentIdentificationPickerView
        piiContentRedactionTextField.inputView = piiContentRedactionPickerView

        engineSelected = "transcribe"
        languageSelected = "en-US"
        regionSelected = "auto"
        partialResultsStabilizationOption = 0
        piiContentIdentificationSelected = ""
        piiContentRedactionSelected = ""

        engineTextField.text = enginesDict[engineSelected]
        languageTextField.text = languagesDict[languageSelected]
        regionTextField.text = regionsDict[regionSelected]
        partialResultsStabilizationTextField.text = partialResultsStabilizationsList[partialResultsStabilizationOption]
        piiContentIdentificationTextField.text = piiContentIdentificationDict[piiContentIdentificationSelected]
        piiContentRedactionTextField.text = piiContentRedactionDict[piiContentRedactionSelected]

        enginePickerView.delegate = self
        languagePickerView.delegate = self
        regionPickerView.delegate = self
        enginePickerView.dataSource = self
        languagePickerView.dataSource = self
        regionPickerView.dataSource = self
        partialResultsStabilizationPickerView.delegate = self
        partialResultsStabilizationPickerView.dataSource = self
        piiContentIdentificationPickerView.delegate = self
        piiContentIdentificationPickerView.dataSource = self
        piiContentRedactionPickerView.delegate = self
        piiContentRedactionPickerView.dataSource = self

        enginePickerView.tag = 1
        languagePickerView.tag = 2
        regionPickerView.tag = 3
        partialResultsStabilizationPickerView.tag = 4
        piiContentIdentificationPickerView.tag = 5
        piiContentRedactionPickerView.tag = 6

        storyBoard = UIStoryboard(name: "Main", bundle: nil)

        guard let languageOptionsViewController =
                storyBoard?.instantiateViewController(withIdentifier: "languageOptions")
                as? AutomaticLanguageOptionsViewController else { return }
        self.languageOptionsViewController = languageOptionsViewController

        languageOptionsViewController.cancellationHandler = { [weak self] numberOfselectedLanguages in
            self?.enableAutomaticLanguageIdentificationSwitch.isOn = numberOfselectedLanguages ?? 0 > 1
        }

        languageOptionsViewController.selectedLanguageOptionsHandler = { [weak self] selectedLanguages in
            self?.selectedLanguageOptions = selectedLanguages ?? ""
        }

        languageOptionsViewController.preferredLanguageOptionHandler = { [weak self] preferredLanguage in
            self?.preferredLanguageOptions = preferredLanguage ?? ""
        }

        self.languageHandler = { [weak self] languageList in
            self?.languageOptionsViewController?.languages = languageList ?? [String]()
        }

        self.languagesDictHandler = { [weak self] mappings in
            self?.languageOptionsViewController?.languagesDict = mappings ?? [String:String]()
        }
    }

    @IBAction func startTranscriptionButton(_ sender: UIButton) {
        let url = self.meetingEndpointUrl.hasSuffix("/") ? self.meetingEndpointUrl : "\(self.meetingEndpointUrl)/"

        let contentIdentificationEnabled = !piiContentIdentificationSelected.isEmpty
        let contentRedactionEnabled = !piiContentRedactionSelected.isEmpty

        var identifyLanguage: Bool?
        var enablePartialResultsStabilization: Bool?
        var contentIdentificationType: String?
        var contentRedactionType: String?
        var partialResultsStabilty: String?
        var piiEntityTypes: String?
        var languageModelName: String?
        var languageOptions: String?
        var preferredLanguage: String?

        if contentIdentificationEnabled {
            if engineSelected == transcribeMedicalEngine {
                contentIdentificationType = transcribeMedicalContentIdentificationType
            } else {
                piiEntityTypes = piiContentIdentificationSelected
                contentIdentificationType = transcribeContentIdetificationType
            }
        }

        if contentRedactionEnabled {
            if engineSelected == transcribeMedicalEngine {
                contentRedactionType = nil
            } else {
                piiEntityTypes = piiContentRedactionSelected
                contentRedactionType = transcribeContentIdetificationType
            }
        }

        if partialResultsStabilizationOption > 0 {
            enablePartialResultsStabilization = true
            if partialResultsStabilizationOption == 1 {
                partialResultsStabilty = partialResultsStabilizationsList.last?.lowercased()
            } else {
                partialResultsStabilty = partialResultsStabilizationsList[partialResultsStabilizationOption].lowercased()
            }
        }

        if enableCustomLangugeModelTextFieldSwitch.isOn {
            if (customLanguageModelTextField.text?.isEmpty) == true {
                self.view.makeToast("Custom language model name cannot be empty!")
                return
            } else {
                languageModelName = customLanguageModelTextField.text
            }
        }

        if enableAutomaticLanguageIdentificationSwitch.isOn {
            if !preferredLanguageOptions.isEmpty {
                preferredLanguage = preferredLanguageOptions
            }
            languageOptions = selectedLanguageOptions
            identifyLanguage = true
            languageSelected = ""
        }

        let transcriptionStreamParams = TranscriptionStreamParams(
            contentIdentificationType: contentIdentificationType,
            contentRedactionType: contentRedactionType,
            enablePartialResultsStabilization: enablePartialResultsStabilization,
            partialResultsStability: partialResultsStabilty,
            piiEntityTypes: piiEntityTypes,
            languageModelName: languageModelName,
            identifyLanguage: identifyLanguage,
            languageOptions: languageOptions,
            preferredLanguage: preferredLanguage)

        let encodedData = try? JSONEncoder().encode(transcriptionStreamParams)
        var transcriptionStreamParamsEncoded = "{}"
        if let data = encodedData {
            transcriptionStreamParamsEncoded = String(data: data, encoding: .utf8) ?? "{}"
        }

        let encodedURL = HttpUtils.encodeStrForURL(
            str: "\(url)start_transcription?title=\(meetingId)" +
            "&language=\(languageSelected)" +
            "&region=\(regionSelected)" +
            "&engine=\(engineSelected)" +
            "&transcriptionStreamParams=\(transcriptionStreamParamsEncoded)")
        HttpUtils.postRequest(url: encodedURL, jsonData: nil) {_, error in
            DispatchQueue.main.async {
                if error == nil {
                    MeetingModule.shared().dismissTranscription(self)
                } else {
                    self.notify(msg: "Transcription call failed, please try again")
                }
            }
        }
    }

    @IBAction func toggleCustomLanguageTextField(_ enableCustomLangugeModelTextFieldSwitch: UISwitch) {
        customLanguageModelTextField.isHidden = !enableCustomLangugeModelTextFieldSwitch.isOn
    }

    @IBAction func enablePHIIdentificaiton(_ enablePHIIdentificaitonSwitch: UISwitch) {
        piiContentIdentificationSelected = enablePHIIdentificaitonSwitch.isOn ? "ALL" : ""
    }

    @IBAction func showLanguageOptionsView(_ sender: UISwitch) {

        guard let languageOptionsViewController = languageOptionsViewController else {
            return
        }

        if sender.isOn {
            languageHandler?(transcribeLanguages)
            languagesDictHandler?(languagesDict)
            languageOptionsViewController.modalPresentationStyle = .pageSheet
            self.present(languageOptionsViewController, animated: true, completion: nil)
        }
    }

}

extension LiveTranscriptionOptionsViewController: UIPickerViewDataSource, UIPickerViewDelegate {
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }

    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        switch pickerView.tag {
        case 1:
            return engines.count
        case 2:
            return languages.count
        case 3:
            return regions.count
        case 4:
            return partialResultsStabilizations.count
        case 5:
            return piiContentIdentifications.count
        case 6:
            return piiContentRedactions.count
        default:
            return 1
        }
    }

    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        switch pickerView.tag {
        case 1:
            return enginesDict[engines[row]]
        case 2:
            return languagesDict[languages[row]]
        case 3:
            return regionsDict[regions[row]]
        case 4:
            return partialResultsStabilizationsList[row]
        case 5:
            return piiContentIdentificationDict[piiContentIdentifications[row]]
        case 6:
            return piiContentRedactionDict[piiContentRedactions[row]]
        default:
            return "Data not found"
        }
    }

    func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        let engineSelectedBefore = engineSelected
        switch pickerView.tag {
        case 1:
            engineTextField.text = enginesDict[engines[row]]
            engineSelected = engines[row]
            engineTextField.resignFirstResponder()
        case 2:
            languageTextField.text = languagesDict[languages[row]]
            languageSelected = languages[row]
            languageTextField.resignFirstResponder()
        case 3:
            regionTextField.text = regionsDict[regions[row]]
            regionSelected = regions[row]
            regionTextField.resignFirstResponder()
        case 4:
            partialResultsStabilizationTextField.text = partialResultsStabilizationsList[row]
            partialResultsStabilizationOption = row
            partialResultsStabilizationTextField.resignFirstResponder()
        case 5:
            piiContentIdentificationTextField.text = piiContentIdentificationDict[piiContentIdentifications[row]]
            piiContentIdentificationSelected = piiContentIdentifications[row]
            piiContentIdentificationTextField.resignFirstResponder()

            piiContentRedactionTextField.text = piiContentRedactionDict[piiContentRedactions[0]]
            piiContentRedactionSelected = piiContentRedactions[0]
            piiContentRedactionTextField.resignFirstResponder()

        case 6:
            piiContentRedactionTextField.text = piiContentRedactionDict[piiContentRedactions[row]]
            piiContentRedactionSelected = piiContentRedactions[row]
            piiContentRedactionTextField.resignFirstResponder()

            piiContentIdentificationTextField.text = piiContentIdentificationDict[piiContentIdentifications[0]]
            piiContentIdentificationSelected = piiContentIdentifications[0]
            piiContentIdentificationTextField.resignFirstResponder()
        default:
            return
        }

        if engineSelected == "transcribe_medical" && engineSelectedBefore == "transcribe" {
            languages = transcribeMedicalLanguages
            regions = transcribeMedicalRegions
            languageSelected = "en-US"
            languageTextField.text = languagesDict[languageSelected]
            regionSelected = "auto"
            regionTextField.text = regionsDict[regionSelected]

            partialResultsStabilizationOption = 0
            partialResultsStabilizationTextField.text = partialResultsStabilizationsList[partialResultsStabilizationOption]
            piiContentIdentificationSelected = ""
            piiContentIdentificationTextField.text = piiContentIdentificationDict[piiContentIdentificationSelected]
            piiContentRedactionSelected = ""
            piiContentIdentificationTextField.text = piiContentRedactionDict[piiContentRedactionSelected]
            customLanguageModelTextField.text = ""
            selectedLanguageOptions = ""
            preferredLanguageOptions = ""

            partialResultsStabilizationTextField.isHidden = true
            piiContentIdentificationTextField.isHidden = true
            piiContentRedactionTextField.isHidden = true
            enableCustomLangugeModelTextFieldSwitch.isOn = false
            enableCustomLangugeModelTextFieldSwitch.isHidden = true
            enableCustomLangugeModelLabel.isHidden = true
            customLanguageModelTextField.isHidden = true
            enablePHIIdentificaitonSwitch.isHidden = false
            enablePHIIdentificationLabel.isHidden = false
            enableAutomaticLanguageIdentificationLabel.isHidden = true
            enableAutomaticLanguageIdentificationSwitch.isHidden = true
            enableAutomaticLanguageIdentificationSwitch.isOn = false

        }
        if engineSelected == "transcribe" {
            languages = transcribeLanguages
            regions = transcribeRegions
            piiContentIdentifications = transcribePiiContents
            piiContentRedactions = transcribePiiContents
            if enablePHIIdentificaitonSwitch.isOn {
                piiContentIdentificationSelected = ""
            }
            partialResultsStabilizationTextField.isHidden = false
            piiContentIdentificationTextField.isHidden = false
            piiContentRedactionTextField.isHidden = false
            enableCustomLangugeModelTextFieldSwitch.isHidden = false
            enableCustomLangugeModelLabel.isHidden = false
            enableAutomaticLanguageIdentificationLabel.isHidden = false
            enableAutomaticLanguageIdentificationSwitch.isHidden = false
            enablePHIIdentificaitonSwitch.isHidden = true
            enablePHIIdentificaitonSwitch.isOn = false
            enablePHIIdentificationLabel.isHidden = true
        }
    }
}


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

import AmazonChimeSDK
import AVFoundation
import UIKit

class MeetingModel: NSObject {
    enum ActiveMode {
        case roster
        case chat
        case video
        case screenShare
        case captions
        case metrics
        case callKitOnHold
    }

    // Dependencies
    let meetingId: String
    let meetingEndpointUrl: String
    let primaryMeetingId: String
    let primaryExternalMeetingId: String
    let selfName: String
    var audioVideoConfig = AudioVideoConfiguration()
    let callKitOption: CallKitOption
    let meetingSessionConfig: MeetingSessionConfiguration
    lazy var currentMeetingSession = DefaultMeetingSession(configuration: meetingSessionConfig,
                                                           logger: logger)

    // Utils
    let logger = ConsoleLogger(name: "MeetingModel")
    let postLogger: PostLogger
    let activeSpeakerObserverId = UUID().uuidString

    // Sub models
    let rosterModel = RosterModel()
    lazy var videoModel = VideoModel(audioVideoFacade: currentMeetingSession.audioVideo,
                                     eventAnalyticsController: currentMeetingSession.eventAnalyticsController)
    let metricsModel = MetricsModel()
    lazy var screenShareModel = ScreenShareModel(meetingSessionConfig: meetingSessionConfig,
                                                 contentShareController: currentMeetingSession.audioVideo)
    let chatModel = ChatModel()
    lazy var deviceSelectionModel = DeviceSelectionModel(deviceController: currentMeetingSession.audioVideo,
                                                         cameraCaptureSource: videoModel.customSource,
                                                         audioVideoConfig: audioVideoConfig)
    let captionsModel = CaptionsModel()
    let uuid = UUID()
    var call: Call?

    // States
    var isAppInBackground: Bool = false {
        didSet {
            if isAppInBackground {
                wasLocalVideoOn = videoModel.isLocalVideoActive
                if wasLocalVideoOn {
                    videoModel.isLocalVideoActive = false
                }
                videoModel.unsubscribeAllRemoteVideos()
            } else {
                if wasLocalVideoOn {
                    videoModel.isLocalVideoActive = true
                }
                updateRemoteVideoSourceSelection()
            }
            videoModel.updateVideoSourceSubscription()
        }
    }
    private var savedModeBeforeOnHold: ActiveMode?
    private var wasLocalVideoOn: Bool = false

    var activeMode: ActiveMode = .roster {
        didSet {
            updateRemoteVideoSourceSelection()
            videoModel.updateVideoSourceSubscription()
            activeModeDidSetHandler?(activeMode)
        }
    }

    private var isMuted = false {
        didSet {
            if isMuted {
                if currentMeetingSession.audioVideo.realtimeLocalMute() {
                    logger.info(msg: "Microphone has been muted")
                }
            } else {
                if currentMeetingSession.audioVideo.realtimeLocalUnmute() {
                    logger.info(msg: "Microphone has been unmuted")
                }
            }
            isMutedHandler?(isMuted)
        }
    }

    private var isEnded = false {
        didSet {
            // This will unbind current tiles.
            videoModel.isEnded = true
            currentMeetingSession.audioVideo.stop()
            screenShareModel.stopLocalSharing()
            isEndedHandler?()
        }
    }

    // State for joining primary meeting will not be true until success,
    // which is achieved asynchronously.  Managed in `MeetingViewController`.
    var isPromotedToPrimaryMeeting = false
    // Store to adjust content takeover behavior
    var primaryMeetingMeetingSessionCredentials: MeetingSessionCredentials? = nil

    var audioDevices: [MediaDevice] {
        return currentMeetingSession.audioVideo.listAudioDevices()
    }

    var currentAudioDevice: MediaDevice? {
        return currentMeetingSession.audioVideo.getActiveAudioDevice()
    }

    // Handlers
    var activeModeDidSetHandler: ((ActiveMode) -> Void)?
    var notifyHandler: ((String) -> Void)?
    var isMutedHandler: ((Bool) -> Void)?
    var isEndedHandler: (() -> Void)?

    init(meetingSessionConfig: MeetingSessionConfiguration,
         meetingId: String,
         primaryMeetingId: String,
         primaryExternalMeetingId: String,
         selfName: String,
         audioVideoConfig: AudioVideoConfiguration,
         callKitOption: CallKitOption,
         meetingEndpointUrl: String) {
        self.meetingId = meetingId
        self.meetingEndpointUrl = meetingEndpointUrl.isEmpty ? AppConfiguration.url : meetingEndpointUrl
        self.primaryMeetingId = primaryMeetingId
        self.primaryExternalMeetingId = primaryExternalMeetingId
        self.selfName = selfName
        self.audioVideoConfig = audioVideoConfig
        self.callKitOption = callKitOption
        self.meetingSessionConfig = meetingSessionConfig
        let url = AppConfiguration.url.hasSuffix("/") ? AppConfiguration.url : "\(AppConfiguration.url)/"
        self.postLogger = PostLogger(name: "SDKEvents", configuration: meetingSessionConfig, url: "\(url)log_meeting_event")
        super.init()

        if callKitOption == .incoming {
            call = createCall(isOutgoing: false)
        } else if callKitOption == .outgoing {
            call = createCall(isOutgoing: true)
        }
    }

    func bind(videoRenderView: VideoRenderView, tileId: Int) {
        currentMeetingSession.audioVideo.bindVideoView(videoView: videoRenderView, tileId: tileId)
    }

    func startMeeting() {
        if self.callKitOption == .disabled {
            self.configureAudioSession()
            self.startAudioVideoConnection()
            self.currentMeetingSession.audioVideo.startRemoteVideo()
        }
        screenShareModel.broadcastCaptureModel.isBlocked = false
    }

    func resumeCallKitMeeting() {
        if let call = call {
            CallKitManager.shared().setHeld(with: call, isOnHold: false)
        }
    }

    func endMeeting() {
        if let call = call, !isEnded {
            CallKitManager.shared().endCallFromLocal(with: call)
        } else {
            isEnded = true
        }
    }

    func setMute(isMuted: Bool) {
        if let call = call {
            CallKitManager.shared().setMuted(for: call, isMuted: isMuted)
        } else {
            self.isMuted = isMuted
        }
    }

    func setVoiceFocusEnabled(enabled: Bool) {
        let action = enabled ? "enable" : "disable"
        let success = currentMeetingSession.audioVideo.realtimeSetVoiceFocusEnabled(enabled: enabled)
        if success {
            notify(msg: "Voice Focus \(action)d")
        } else {
            notify(msg: "Failed to \(action) Voice Focus")
        }
    }

    func isVoiceFocusEnabled() -> Bool {
        return currentMeetingSession.audioVideo.realtimeIsVoiceFocusEnabled()
    }

    func setLiveTranscriptionEnabled(enabled: Bool) {
        if enabled {
            MeetingModule.shared().liveTranscriptionOptionsSelected(self)
        } else {
            postStopTranscriptionRequest()
        }
    }

    func postStopTranscriptionRequest() {
        let url = self.meetingEndpointUrl.hasSuffix("/") ? self.meetingEndpointUrl : "\(self.meetingEndpointUrl)/"
        let encodedURL = HttpUtils.encodeStrForURL(
                str: "\(url)stop_transcription?title=\(meetingId)")
        HttpUtils.postRequest(url: encodedURL, jsonData: nil) { _, error in
            DispatchQueue.main.async {
                if error != nil {
                    self.notify(msg: "Transcription failed to stop, please try again!")
                } else {
                    self.notify(msg: "Live Transcription Disabled")
                }
            }
        }
    }

    func getVideoTileDisplayName(for indexPath: IndexPath) -> String {
        var displayName = ""
        if indexPath.item == 0 {
            if videoModel.isLocalVideoActive {
                displayName = selfName
            } else {
                displayName = "Turn on your video"
            }
        } else {
            if let videoTileState = videoModel.getVideoTileState(for: indexPath) {
                displayName = rosterModel.getAttendeeName(for: videoTileState.attendeeId) ?? ""
            }
        }
        return displayName
    }

    func getVideoTileAttendeeId(for indexPath: IndexPath) -> String {
        if let videoTileState = videoModel.getVideoTileState(for: indexPath) {
            return videoTileState.attendeeId
        }
        return ""
    }

    func chooseAudioDevice(_ audioDevice: MediaDevice) {
        currentMeetingSession.audioVideo.chooseAudioDevice(mediaDevice: audioDevice)
    }

    func sendDataMessage(_ message: String) {
        do {
            try currentMeetingSession
                .audioVideo
                .realtimeSendDataMessage(topic: "chat",
                                         data: message,
                                         lifetimeMs: 1000)
        } catch {
            logger.error(msg: "Failed to send message!")
            return
        }

        let currentTimestamp = NSDate().timeIntervalSince1970
        let timestamp = TimeStampConversion.formatTimestamp(timestamp: Int64(currentTimestamp) * 1000)

        chatModel
            .addChatMessage(chatMessage:
                ChatMessage(
                    senderName: self.selfName,
                    message: message,
                    timestamp: timestamp,
                    isSelf: true
                ))
    }

    private func notify(msg: String) {
        logger.info(msg: msg)
        notifyHandler?(msg)
    }

    private func logWithFunctionName(fnName: String = #function, message: String = "") {
        logger.info(msg: "[Function] \(fnName) -> \(message)")
    }

    private func setupAudioVideoFacadeObservers() {
        let audioVideo = currentMeetingSession.audioVideo
        audioVideo.addVideoTileObserver(observer: self)
        audioVideo.addRealtimeObserver(observer: self)
        audioVideo.addAudioVideoObserver(observer: self)
        audioVideo.addMetricsObserver(observer: self)
        audioVideo.addDeviceChangeObserver(observer: self)
        audioVideo.addActiveSpeakerObserver(policy: DefaultActiveSpeakerPolicy(),
                                            observer: self)
        audioVideo.addRealtimeDataMessageObserver(topic: "chat", observer: self)
        audioVideo.addEventAnalyticsObserver(observer: self)
        audioVideo.addRealtimeTranscriptEventObserver?(observer: self)
    }

    private func removeAudioVideoFacadeObservers() {
        let audioVideo = currentMeetingSession.audioVideo
        audioVideo.removeVideoTileObserver(observer: self)
        audioVideo.removeRealtimeObserver(observer: self)
        audioVideo.removeAudioVideoObserver(observer: self)
        audioVideo.removeMetricsObserver(observer: self)
        audioVideo.removeDeviceChangeObserver(observer: self)
        audioVideo.removeActiveSpeakerObserver(observer: self)
        audioVideo.removeRealtimeDataMessageObserverFromTopic(topic: "chat")
        audioVideo.removeEventAnalyticsObserver(observer: self)
        audioVideo.removeRealtimeTranscriptEventObserver?(observer: self)
    }

    private func configureAudioSession() {
        MeetingModule.shared().configureAudioSession()
    }

    private func startAudioVideoConnection() {
        do {
            setupAudioVideoFacadeObservers()
            try currentMeetingSession.audioVideo.start(audioVideoConfiguration: audioVideoConfig)
        } catch {
            logger.error(msg: "Error starting the Meeting: \(error.localizedDescription)")
            endMeeting()
        }
    }

    private func logAttendee(attendeeInfo: [AttendeeInfo], action: String) {
        for currentAttendeeInfo in attendeeInfo {
            let attendeeId = currentAttendeeInfo.attendeeId
            if !rosterModel.contains(attendeeId: attendeeId) {
                logger.error(msg: "Cannot find attendee with attendee id \(attendeeId)" +
                    " external user id \(currentAttendeeInfo.externalUserId): \(action)")
                continue
            }
            logger.info(msg: "\(rosterModel.getAttendeeName(for: attendeeId) ?? "nil"): \(action)")
        }
    }

    private func createCall(isOutgoing: Bool) -> Call {
        let call = Call(uuid: uuid, handle: meetingId, isOutgoing: isOutgoing)
        call.isReadytoConfigureHandler = { [weak self] in
            self?.configureAudioSession()
        }
        call.isAudioSessionActiveHandler = { [weak self] in
            self?.startAudioVideoConnection()
            self?.currentMeetingSession.audioVideo.startRemoteVideo()
            if self?.isMuted ?? false {
                _ = self?.currentMeetingSession.audioVideo.realtimeLocalMute()
            }
        }
        call.isEndedHandler = { [weak self] in
            self?.isEnded = true
        }
        call.isMutedHandler = { [weak self] isMuted in
            self?.isMuted = isMuted
        }
        call.isOnHoldHandler = { [weak self] isOnHold in
            if isOnHold {
                self?.currentMeetingSession.audioVideo.stop()
                self?.savedModeBeforeOnHold = self?.activeMode
                self?.activeMode = .callKitOnHold
            } else {
                if let savedModeBeforeOnHold = self?.savedModeBeforeOnHold {
                    self?.activeMode = savedModeBeforeOnHold
                    self?.savedModeBeforeOnHold = nil
                }
            }
        }
        return call
    }

    private func updateRemoteVideoSourceSelection() {
        if activeMode == .video {
            videoModel.removeRemoteVideosNotInCurrentPage()
            videoModel.updateAllRemoteVideosInCurrentPageExceptUserPausedVideos()
        } else if activeMode == .screenShare {
            videoModel.removeNonContentShareVideoSources()
            videoModel.addContentShareVideoSource()
        } else {
            videoModel.unsubscribeAllRemoteVideos()
        }
    }
}

// MARK: AudioVideoObserver

extension MeetingModel: AudioVideoObserver {
    func connectionDidRecover() {
        notifyHandler?("Connection quality has recovered")
        logWithFunctionName()
    }

    func connectionDidBecomePoor() {
        notifyHandler?("Connection quality has become poor")
        logWithFunctionName()
    }

    func videoSessionDidStopWithStatus(sessionStatus: MeetingSessionStatus) {
        logWithFunctionName(message: "\(sessionStatus.statusCode)")
    }

    func audioSessionDidStartConnecting(reconnecting: Bool) {
        notifyHandler?("Audio started connecting. Reconnecting: \(reconnecting)")
        logWithFunctionName(message: "reconnecting \(reconnecting)")
        if !reconnecting {
            call?.isConnectingHandler?()
        }
    }

    func audioSessionDidStart(reconnecting: Bool) {
        notifyHandler?("Audio successfully started. Reconnecting: \(reconnecting)")
        logWithFunctionName(message: "reconnecting \(reconnecting)")
        // Start Amazon Voice Focus as soon as audio session started
        setVoiceFocusEnabled(enabled: true)
        if !reconnecting {
            call?.isConnectedHandler?()
        }

        // This selection has to be here because if there are bluetooth headset connected,
        // selecting non-bluetooth device before audioVideo.start() will get route overwritten by bluetooth
        // after audio session starts
        chooseAudioDevice(deviceSelectionModel.selectedAudioDevice)
    }

    func audioSessionDidDrop() {
        notifyHandler?("Audio Session Dropped")
        logWithFunctionName()
    }

    func audioSessionDidStopWithStatus(sessionStatus: MeetingSessionStatus) {
        logWithFunctionName(message: "\(sessionStatus.statusCode)")

        removeAudioVideoFacadeObservers()
        if let call = call {
            switch sessionStatus.statusCode {
            case .ok:
                if call.isOnHold {
                    return
                }
            case .audioCallEnded, .audioServerHungup:
                CallKitManager.shared().reportCallEndedFromRemote(with: call, reason: .remoteEnded)
            case .audioJoinedFromAnotherDevice:
                CallKitManager.shared().reportCallEndedFromRemote(with: call, reason: .answeredElsewhere)
            case .audioDisconnectAudio:
                CallKitManager.shared().reportCallEndedFromRemote(with: call, reason: .declinedElsewhere)
            default:
                CallKitManager.shared().reportCallEndedFromRemote(with: call, reason: .failed)
            }
        }
        endMeeting()
    }

    func audioSessionDidCancelReconnect() {
        notifyHandler?("Audio cancelled reconnecting")
        logWithFunctionName()
    }

    func videoSessionDidStartConnecting() {
        logWithFunctionName()
    }

    func remoteVideoSourcesDidBecomeAvailable(sources: [RemoteVideoSource]) {
        logWithFunctionName()
        sources.forEach { source in
            // Initialize with defaults in case we want to update through UI
            videoModel.addVideoSource(source: source, config: VideoSubscriptionConfiguration())
        }
        updateRemoteVideoSourceSelection()
        videoModel.updateVideoSourceSubscription()
    }

    func remoteVideoSourcesDidBecomeUnavailable(sources: [RemoteVideoSource]) {
        logWithFunctionName()
        sources.forEach { source in
            videoModel.removeVideoSource(source: source)
        }
        videoModel.audioVideoFacade.updateVideoSourceSubscriptions(addedOrUpdated: [:], removed: sources)
    }

    func videoSessionDidStartWithStatus(sessionStatus: MeetingSessionStatus) {
        switch sessionStatus.statusCode {
        case .videoAtCapacityViewOnly:
            notifyHandler?("Local video is no longer possible to be enabled")
            logWithFunctionName(message: "\(sessionStatus.statusCode)")
            videoModel.isLocalVideoActive = false
        default:
            logWithFunctionName(message: "\(sessionStatus.statusCode)")
        }
    }

    func cameraSendAvailabilityDidChange(available : Bool) {
        logWithFunctionName(message: "Camera Send Available: \(available)")
        videoModel.cameraSendIsAvailable = available
    }
}

// MARK: RealtimeObserver

extension MeetingModel: RealtimeObserver {
    private func isSelfAttendee(attendeeId: String) -> Bool {
        return DefaultModality(id: attendeeId).base == meetingSessionConfig.credentials.attendeeId
            || DefaultModality(id: attendeeId).base == primaryMeetingMeetingSessionCredentials?.attendeeId
    }

    private func removeAttendeesAndReload(attendeeInfo: [AttendeeInfo]) {
        let attendeeIds = attendeeInfo.map { $0.attendeeId }
        rosterModel.removeAttendees(attendeeIds)
        if activeMode == .roster {
            rosterModel.rosterUpdatedHandler?()
        }
    }

    private func attendeesDidJoinWithStatus(attendeeInfo: [AttendeeInfo], status: AttendeeStatus) {
        var newAttendees = [RosterAttendee]()
        for currentAttendeeInfo in attendeeInfo {
            let attendeeId = currentAttendeeInfo.attendeeId
            if !rosterModel.contains(attendeeId: attendeeId) {
                let attendeeName = RosterModel.convertAttendeeName(from: currentAttendeeInfo)
                let newAttendee = RosterAttendee(attendeeId: attendeeId,
                                                 attendeeName: attendeeName,
                                                 volume: .notSpeaking,
                                                 signal: .high)
                newAttendees.append(newAttendee)
                let action = "Joined"
                logger.info(msg: "attendeeId:\(currentAttendeeInfo.attendeeId) externalUserId:\(currentAttendeeInfo.externalUserId) \(action)")

                // if other attendee starts sharing content, stop content sharing from current device
                let modality = DefaultModality(id: attendeeId)
                if modality.isOfType(type: .content),
                   !isSelfAttendee(attendeeId: attendeeId) {
                    screenShareModel.stopLocalSharing()
                    notifyHandler?("\(rosterModel.getAttendeeName(for: modality.base) ?? "") took over the screen share")
                }
            }
        }
        rosterModel.addAttendees(newAttendees)
        if activeMode == .roster {
            rosterModel.rosterUpdatedHandler?()
        }
    }

    func attendeesDidLeave(attendeeInfo: [AttendeeInfo]) {
        logAttendee(attendeeInfo: attendeeInfo, action: "Left")
        removeAttendeesAndReload(attendeeInfo: attendeeInfo)
    }

    func attendeesDidDrop(attendeeInfo: [AttendeeInfo]) {
        for attendee in attendeeInfo {
            notify(msg: "\(attendee.externalUserId) dropped")
        }

        removeAttendeesAndReload(attendeeInfo: attendeeInfo)
    }

    func attendeesDidMute(attendeeInfo: [AttendeeInfo]) {
        logAttendee(attendeeInfo: attendeeInfo, action: "Muted")
    }

    func attendeesDidUnmute(attendeeInfo: [AttendeeInfo]) {
        logAttendee(attendeeInfo: attendeeInfo, action: "Unmuted")
    }

    func volumeDidChange(volumeUpdates: [VolumeUpdate]) {
        for currentVolumeUpdate in volumeUpdates {
            let attendeeId = currentVolumeUpdate.attendeeInfo.attendeeId
            rosterModel.updateVolume(attendeeId: attendeeId, volume: currentVolumeUpdate.volumeLevel)
        }
        if activeMode == .roster {
            rosterModel.rosterUpdatedHandler?()
        }
    }

    func signalStrengthDidChange(signalUpdates: [SignalUpdate]) {
        for currentSignalUpdate in signalUpdates {
            logWithFunctionName(message: "\(currentSignalUpdate.attendeeInfo.externalUserId) \(currentSignalUpdate.signalStrength)")
            let attendeeId = currentSignalUpdate.attendeeInfo.attendeeId
            rosterModel.updateSignal(attendeeId: attendeeId, signal: currentSignalUpdate.signalStrength)
        }
        if activeMode == .roster {
            rosterModel.rosterUpdatedHandler?()
        }
    }

    func attendeesDidJoin(attendeeInfo: [AttendeeInfo]) {
        attendeesDidJoinWithStatus(attendeeInfo: attendeeInfo, status: AttendeeStatus.joined)
    }
}

// MARK: MetricsObserver

extension MeetingModel: MetricsObserver {
    func metricsDidReceive(metrics: [AnyHashable: Any]) {
        guard let observableMetrics = metrics as? [ObservableMetric: Any] else {
            logger.error(msg: "The received metrics \(metrics) is not of type [ObservableMetric: Any].")
            return
        }
        metricsModel.updateAppMetrics(metrics: metrics)
        logger.info(msg: "Media metrics have been received: \(observableMetrics)")
        if activeMode == .metrics {
            metricsModel.metricsUpdatedHandler?()
        }
    }
}

// MARK: DeviceChangeObserver

extension MeetingModel: DeviceChangeObserver {
    func audioDeviceDidChange(freshAudioDeviceList: [MediaDevice]) {
        let deviceLabels: [String] = freshAudioDeviceList.map { device in "* \(device.label) (\(device.type))" }
        logger.info(msg: deviceLabels.joined(separator: "\n"))
        notifyHandler?("Device availability changed:\nAvailable Devices:\n\(deviceLabels.joined(separator: "\n"))")
    }
}

// MARK: VideoTileObserver

extension MeetingModel: VideoTileObserver {
    func videoTileDidAdd(tileState: VideoTileState) {
        logger.info(msg: "Attempting to add video tile tileId: \(tileState.tileId)" +
            " attendeeId: \(tileState.attendeeId) with size \(tileState.videoStreamContentWidth)*\(tileState.videoStreamContentHeight)")
        if tileState.isContent {
            screenShareModel.tileId = tileState.tileId
            if activeMode == .screenShare {
                screenShareModel.viewUpdateHandler?(true)
                videoModel.addContentShareVideoSource()
            } else {
                videoModel.removeContentShareVideoSources()
            }
        } else {
            if tileState.isLocalTile {
                videoModel.setSelfVideoTileState(tileState)
                if activeMode == .video {
                    videoModel.localVideoUpdatedHandler?()
                }
            } else {
                videoModel.addRemoteVideoTileState(tileState, completion: {
                    if self.activeMode == .video {
                        self.videoModel.videoSubscriptionUpdatedHandler?()
                    } else {
                        // Currently not in the video view, no need to render non content share video tiles
                        self.videoModel.removeNonContentShareVideoSources()
                    }
                })
            }
        }
        videoModel.updateVideoSourceSubscription()
    }

    func videoTileDidRemove(tileState: VideoTileState) {
        logger.info(msg: "Attempting to remove video tile tileId: \(tileState.tileId)" +
            " attendeeId: \(tileState.attendeeId)")
        currentMeetingSession.audioVideo.unbindVideoView(tileId: tileState.tileId)

        if tileState.isContent {
            screenShareModel.tileId = nil
            if activeMode == .screenShare {
                screenShareModel.viewUpdateHandler?(false)
            }
        } else if tileState.isLocalTile {
            videoModel.setSelfVideoTileState(nil)
            if activeMode == .video {
                videoModel.localVideoUpdatedHandler?()
            }
        } else {
            videoModel.removeRemoteVideoTileState(tileState, completion: { success in
                if success {
                    self.videoModel.revalidateRemoteVideoPageIndex()
                    if self.activeMode == .video {
                        self.videoModel.videoUpdatedHandler?()
                    }
                } else {
                    self.logger.error(msg: "Cannot remove unexisting remote video tile for tileId: \(tileState.tileId)")
                }
            })
        }
    }

    func videoTileDidPause(tileState: VideoTileState) {
        if tileState.pauseState == .pausedForPoorConnection {
            videoModel.updateRemoteVideoTileState(tileState)
        } else {
            let attendeeId = tileState.attendeeId
            let attendeeName = rosterModel.getAttendeeName(for: attendeeId) ?? ""
            notifyHandler?("Video for attendee \(attendeeName) " +
                " has been paused")
        }
    }

    func videoTileDidResume(tileState: VideoTileState) {
        let attendeeId = tileState.attendeeId
        let attendeeName = rosterModel.getAttendeeName(for: attendeeId) ?? ""
        notifyHandler?("Video for attendee \(attendeeName) has been unpaused")
        videoModel.updateRemoteVideoTileState(tileState)
    }

    func videoTileSizeDidChange(tileState: VideoTileState) {
        logger.info(msg: "Video stream content size changed to \(tileState.videoStreamContentWidth)*\(tileState.videoStreamContentHeight) for tileId: \(tileState.tileId)")
    }
}

// MARK: ActiveSpeakerObserver

extension MeetingModel: ActiveSpeakerObserver {
    var observerId: String {
        return activeSpeakerObserverId
    }

    var scoresCallbackIntervalMs: Int {
        return 5000 // 5 second
    }

    func activeSpeakerDidDetect(attendeeInfo: [AttendeeInfo]) {
        videoModel.updateRemoteVideoStatesBasedOnActiveSpeakers(activeSpeakers: attendeeInfo, inVideoMode: activeMode == .video)

        rosterModel.updateActiveSpeakers(attendeeInfo.map { $0.attendeeId })
        if activeMode == .roster {
            rosterModel.rosterUpdatedHandler?()
        }
    }

    func activeSpeakerScoreDidChange(scores: [AttendeeInfo: Double]) {
        let scoresInString = scores.map { (score) -> String in
            let (key, value) = score
            return "\(key.externalUserId): \(value)"
        }.joined(separator: ",")
        logWithFunctionName(message: "\(scoresInString)")
    }
}

// MARK: DataMessageObserver

extension MeetingModel: DataMessageObserver {
    func dataMessageDidReceived(dataMessage: DataMessage) {
        if dataMessage.topic == "chat" {
            chatModel.addDataMessage(dataMessage: dataMessage)
        }
    }
}

extension MeetingModel: EventAnalyticsObserver {
    func eventDidReceive(name: EventName, attributes: [AnyHashable: Any]) {
        let jsonData = try? JSONSerialization.data(withJSONObject: [
            "name": "\(name)",
            "attributes": toStringKeyDict(attributes.merging(currentMeetingSession.audioVideo.getCommonEventAttributes(),
                                                             uniquingKeysWith: { (_, newVal) -> Any in
                newVal
            }))
        ] as [String : Any], options: [])

        guard let data = jsonData, let msg = String(data: data, encoding: .utf8)  else {
            logger.info(msg: "Dictionary is not in correct format to be serialized")
            return
        }
        postLogger.info(msg: msg)

        switch name {
        case .meetingStartSucceeded:
            logger.info(msg: "Meeting stared on : \(currentMeetingSession.audioVideo.getCommonEventAttributes().toJsonString())")
        case .meetingEnded, .meetingFailed:
            logger.info(msg: "\(currentMeetingSession.audioVideo.getMeetingHistory())")
            postLogger.publishLog()
        default:
            break
        }
    }

    func toStringKeyDict(_ attributes: [AnyHashable: Any]) -> [String: Any] {
        var jsonDict = [String: Any]()
        attributes.forEach { (key, value) in
            jsonDict[String(describing: key)] = String(describing: value)
        }
        return jsonDict
    }
}

// MARK: TranscriptEventObserver

extension MeetingModel: TranscriptEventObserver {
    func transcriptEventDidReceive(transcriptEvent: TranscriptEvent) {
        captionsModel.addTranscriptEvent(transcriptEvent: transcriptEvent)
    }
}


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

import AmazonChimeSDK
import AVFoundation
import UIKit

let incomingCallKitDelayInSeconds = 10.0

class MeetingModule {
    private static var sharedInstance: MeetingModule?
    private(set) var activeMeeting: MeetingModel?
    private let meetingPresenter = MeetingPresenter()
    private var meetings: [UUID: MeetingModel] = [:]
    private let logger = ConsoleLogger(name: "MeetingModule")

    // These need to be cached in case of primary meeting joins in the future
    var cachedOverriddenEndpoint = ""
    var cachedPrimaryExternalMeetingId = ""

    static func shared() -> MeetingModule {
        if sharedInstance == nil {
            sharedInstance = MeetingModule()

            // This is to initialize CallKit properly before requesting first outgoing/incoming call
            _ = CallKitManager.shared()
        }
        return sharedInstance!
    }

    func prepareMeeting(meetingId: String,
                        selfName: String,
                        audioVideoConfig: AudioVideoConfiguration,
                        option: CallKitOption,
                        overriddenEndpoint: String,
                        primaryExternalMeetingId: String,
                        completion: @escaping (Bool) -> Void) {
        requestRecordPermission { success in
            guard success else {
                completion(false)
                return
            }
            self.cachedOverriddenEndpoint = overriddenEndpoint
            self.cachedPrimaryExternalMeetingId = primaryExternalMeetingId
            JoinRequestService.postJoinRequest(meetingId: meetingId,
                                               name: selfName,
                                               overriddenEndpoint: overriddenEndpoint,
                                               primaryExternalMeetingId: primaryExternalMeetingId) { joinMeetingResponse in
                guard let joinMeetingResponse = joinMeetingResponse else {
                    DispatchQueue.main.async {
                        completion(false)
                    }
                    return
                }
                let meetingResp = JoinRequestService.getCreateMeetingResponse(from: joinMeetingResponse)
                let attendeeResp = JoinRequestService.getCreateAttendeeResponse(from: joinMeetingResponse)
                let meetingSessionConfiguration = MeetingSessionConfiguration(createMeetingResponse: meetingResp,
                                                   createAttendeeResponse: attendeeResp,
                                                   urlRewriter: self.urlRewriter)
                let meetingModel = MeetingModel(meetingSessionConfig: meetingSessionConfiguration,
                                                meetingId: meetingId,
                                                primaryMeetingId: meetingSessionConfiguration.primaryMeetingId ?? "",
                                                primaryExternalMeetingId: joinMeetingResponse.joinInfo.primaryExternalMeetingId ?? "",
                                                selfName: selfName,
                                                audioVideoConfig: audioVideoConfig,
                                                callKitOption: option,
                                                meetingEndpointUrl: overriddenEndpoint)
                self.meetings[meetingModel.uuid] = meetingModel

                switch option {
                case .incoming:
                    guard let call = meetingModel.call else {
                        completion(false)
                        return
                    }
                    let backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
                    DispatchQueue.main.asyncAfter(deadline: .now() + incomingCallKitDelayInSeconds) {
                        CallKitManager.shared().reportNewIncomingCall(with: call)
                        UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
                    }
                case .outgoing:
                    guard let call = meetingModel.call else {
                        completion(false)
                        return
                    }
                    CallKitManager.shared().startOutgoingCall(with: call)
                case .disabled:
                    DispatchQueue.main.async { [weak self] in
                        self?.selectDevice(meetingModel, completion: completion)
                    }
                }
                completion(true)
            }
        }
    }

    func urlRewriter(url: String) -> String {
        // changing url
        // return url.replacingOccurrences(of: "example.com", with: "my.example.com")
        return url
    }

    func selectDevice(_ meeting: MeetingModel, completion: @escaping (Bool) -> Void) {
        // This is needed to discover bluetooth devices
        configureAudioSession()
        self.meetingPresenter.showDeviceSelectionView(meetingModel: meeting) { success in
            if success {
                self.activeMeeting = meeting
            }
            completion(success)
        }
    }

    func deviceSelected(_ deviceSelectionModel: DeviceSelectionModel) {
        guard let activeMeeting = activeMeeting else {
            return
        }
        activeMeeting.deviceSelectionModel = deviceSelectionModel
        meetingPresenter.dismissActiveMeetingView {
            self.meetingPresenter.showMeetingView(meetingModel: activeMeeting) { _ in }
        }
    }

    func liveTranscriptionOptionsSelected(_ meetingModel: MeetingModel) {
        guard let activeMeeting = activeMeeting else {
            return
        }
        self.meetingPresenter.showLiveTranscriptionView(meetingModel: activeMeeting) { _ in }
    }

    func dismissTranscription(_ liveTranscriptionVC: LiveTranscriptionOptionsViewController) {
        self.meetingPresenter.dismissLiveTranscriptionView(liveTranscriptionVC) { _ in }
    }

    func joinMeeting(_ meeting: MeetingModel, completion: @escaping (Bool) -> Void) {
        endActiveMeeting {
            self.meetingPresenter.showMeetingView(meetingModel: meeting) { success in
                if success {
                    self.activeMeeting = meeting
                }
                completion(success)
            }
        }
    }

    func getMeeting(with uuid: UUID) -> MeetingModel? {
        return meetings[uuid]
    }

    func endActiveMeeting(completion: @escaping () -> Void) {
        if let activeMeeting = activeMeeting {
            activeMeeting.endMeeting()
            meetingPresenter.dismissActiveMeetingView {
                self.meetings[activeMeeting.uuid] = nil
                self.activeMeeting = nil
                completion()
            }
        } else {
            completion()
        }
    }

    func dismissMeeting(_ meeting: MeetingModel) {
        if let activeMeeting = activeMeeting, meeting.uuid == activeMeeting.uuid {
            meetingPresenter.dismissActiveMeetingView(completion: {})
            meetings[meeting.uuid] = nil
            self.activeMeeting = nil
        } else {
            meetings[meeting.uuid] = nil
        }
    }

    func requestRecordPermission(completion: @escaping (Bool) -> Void) {
        let audioSession = AVAudioSession.sharedInstance()
        switch audioSession.recordPermission {
        case .denied:
            logger.error(msg: "User did not grant audio permission, it should redirect to Settings")
            completion(false)
        case .undetermined:
            audioSession.requestRecordPermission { granted in
                if granted {
                    completion(true)
                } else {
                    self.logger.error(msg: "User did not grant audio permission")
                    completion(false)
                }
            }
        case .granted:
            completion(true)
        @unknown default:
            logger.error(msg: "Audio session record permission unknown case detected")
            completion(false)
        }
    }

    func requestVideoPermission(completion: @escaping (Bool) -> Void) {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .denied, .restricted:
            logger.error(msg: "User did not grant video permission, it should redirect to Settings")
            completion(false)
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { authorized in
                if authorized {
                    completion(true)
                } else {
                    self.logger.error(msg: "User did not grant video permission")
                    completion(false)
                }
            }
        case .authorized:
            completion(true)
        @unknown default:
            logger.error(msg: "AVCaptureDevice authorizationStatus unknown case detected")
            completion(false)
        }
    }

    func configureAudioSession() {
        let audioSession = AVAudioSession.sharedInstance()
        do {
            if audioSession.category != .playAndRecord {
                try audioSession.setCategory(AVAudioSession.Category.playAndRecord,
                                             options: AVAudioSession.CategoryOptions.allowBluetooth)
                try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
            }
            if audioSession.mode != .voiceChat {
                try audioSession.setMode(.voiceChat)
            }
        } catch {
            logger.error(msg: "Error configuring AVAudioSession: \(error.localizedDescription)")
        }
    }
}


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

import UIKit

class MeetingPresenter {
    private let mainStoryboard = UIStoryboard(name: "Main", bundle: nil)
    private var activeMeetingViewController: UIViewController?

    var rootViewController: UIViewController? {
        return UIApplication.shared.keyWindow?.rootViewController
    }

    func showMeetingView(meetingModel: MeetingModel, completion: @escaping (Bool) -> Void) {
        guard let meetingViewController = mainStoryboard.instantiateViewController(withIdentifier: "meeting")
            as? MeetingViewController, let rootViewController = self.rootViewController else {
            completion(false)
            return
        }
        meetingViewController.modalPresentationStyle = .fullScreen
        meetingViewController.meetingModel = meetingModel
        rootViewController.present(meetingViewController, animated: true) {
            self.activeMeetingViewController = meetingViewController
            completion(true)
        }
    }

    func dismissActiveMeetingView(completion: @escaping () -> Void) {
        guard let activeMeetingViewController = activeMeetingViewController else {
            completion()
            return
        }
        activeMeetingViewController.dismiss(animated: true) {
            self.activeMeetingViewController = nil
            completion()
        }
    }

    func showDeviceSelectionView(meetingModel: MeetingModel, completion: @escaping (Bool) -> Void) {
        guard let deviceSelectionVC = mainStoryboard.instantiateViewController(withIdentifier: "deviceSelection")
            as? DeviceSelectionViewController, let rootViewController = self.rootViewController else {
            completion(false)
            return
        }
        deviceSelectionVC.modalPresentationStyle = .fullScreen
        deviceSelectionVC.model = meetingModel.deviceSelectionModel
        rootViewController.present(deviceSelectionVC, animated: true) {
            self.activeMeetingViewController = deviceSelectionVC
            completion(true)
        }
    }

    func showLiveTranscriptionView(meetingModel: MeetingModel, completion: @escaping (Bool) -> Void) {
        guard let liveTranscriptionVC = mainStoryboard.instantiateViewController(withIdentifier: "liveTranscription")
                as? LiveTranscriptionOptionsViewController, let rootViewController = self.activeMeetingViewController else {
            completion(false)
            return
        }
        liveTranscriptionVC.modalPresentationStyle = .pageSheet
        liveTranscriptionVC.model = meetingModel
        rootViewController.present(liveTranscriptionVC, animated: true) {
            completion(true)
        }
    }

    func dismissLiveTranscriptionView(_ liveTranscriptionVC: LiveTranscriptionOptionsViewController, completion: @escaping (Bool) -> Void) {
        liveTranscriptionVC.dismiss(animated: true) {}
    }
}