import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}


import Foundation

let baseDirectory = "/tmp/swift-example-agent"

// Create a client
let client = GymClient(baseURL: URL(string:"http://localhost:5000")!)

// Make a new environment with the CartPole task
let id = client.create(envID: "CartPole-v0")

// Verify action and observation space
print(client.actionSpace(instanceID: id))
print(client.observationSpace(instanceID: id))
print(client.containsObservation(instanceID: id, observations: ["name":"Box"]))

// Start recording, and wipe out old recordings
client.startMonitor(instanceID: id, directory: baseDirectory, force: true, resume: false, videoCallable: true)

// Refresh to get our first observation
let obs = client.reset(instanceID: id)
print("First observation: \(obs)")

var count = 0
while true {
    // Find a random action purely as a demonstration. Replace with an action chosen by your algorithm.
    let action = client.sampleAction(instanceID: id)

    // Execute the action in the environment
    let result = client.step(instanceID: id, action: action)

    print("Result on iteration \(count). \nReward: \(result.reward). Observation: \(result.observation).")
    count += 1
    if result.done {
// Uncomment the following line to endlessly repeat
//        client.reset(instanceID: id)
        break
    }
}

// Clean up
client.closeMonitor(instanceID: id)
client.close(instanceID: id)

// Get your api key from https://gym.openai.com/users/{your_name}
// client.uploadResults(directory: baseDirectory, apiKey: nil, algorithmID: nil)


//
// GymClient.swift
// GYM-HTTP-API
// Created by Andrew Schreiber on 2/2/17.
//

// Add this file to your iOS/MacOS project to access a Gym HTTP server

import Foundation

open class GymClient {
    /// The URL for the Gym HTTP server
    public let baseURL:URL

    /// Creates an instance for interfacing with the Gym HTTP client.
    public init(baseURL:URL) {
        self.baseURL = baseURL
    }

    /// Create a new environment with a gym environment ID string i.e. "CartPole-v0"
    /// - returns : An instanceID to be used to uniquely identify the environment to be manipulated
    open func create(envID:EnvID) -> InstanceID {
        let json = post(url: baseURL.appendingPathComponent("/v1/envs/"),
                        parameter: ["env_id":envID])
        let instanceID = (json as! [String:InstanceID])["instance_id"]!
        return instanceID
    }

    /// Get all existing environment instances. The list is reset each time the server is reset.
    /// - returns : A dictionary of instanceIDs and the gym environment ID they are made from
    open func listAll() -> [InstanceID:EnvID] {
        let json = get(url: baseURL.appendingPathComponent("/v1/envs/"))
        let map = (json as! [String:[InstanceID:EnvID]])["all_envs"]!
        return map
    }

    /// Reset the state of the environment
    /// The resulting observation type may vary.
    /// For discrete spaces, it is an Int.
    /// For vector spaces, it is a [Double].
    /// - returns : An initial observation
    open func reset(instanceID:InstanceID) -> Observation {
        let json = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/reset/"))
        let obs = (json as! [String:AnyObject])["observation"]!
        return Observation(base: obs)
    }

    /// Run one timestep of the environment's dynamics.
    /// - parameter action : An action to take. For discrete spaces, it should be an Int. For vector spaces, it should be a [Double].
    /// - parameter render : Undocumented functionality XD
    /// - returns : StepResult includes an observation of the current environment, amount of reward after the action, if the simulation is done, and any meta data

    open func step(instanceID:InstanceID, action:Action, render:Bool = false) -> StepResult {
        let parameter = ["action":action.base, "render":render] as [String : Any]
        let json = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/step/"),
                        parameter: parameter)
        let result = StepResult(jsonDict: json as! [String:AnyObject])
        return result
    }

    /// Get information (name and dimensions/bounds) of the env's observation_space
    open func observationSpace(instanceID:InstanceID) -> Space {
        return getSpace(instanceID: instanceID, name: "observation_space")
    }

    /// Checks if observations are all contained in the observation space.
    open func containsObservation(instanceID:InstanceID, observations:[String:Any]) -> Bool {
        let json = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/observation_space/contains"), parameter: observations)
        let isMember = (json as! [String:Bool])["member"]!
        return isMember
    }

    /// Get information (name and dimensions/bounds) of the env's action_space
    open func actionSpace(instanceID:InstanceID) -> Space {
        return getSpace(instanceID: instanceID, name: "action_space")
    }

    /// Sample an action randomly from all possible actions in the environment
    open func sampleAction(instanceID:InstanceID) -> Action {
        let json = get(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/action_space/sample"))
        let action = (json as! [String:AnyObject])["action"]!
        return Action(base:action)
    }

    /// Checks if an action is contained in the action space. Currently, only int action types are supported
    open func containsAction(instanceID:InstanceID, action:Action) -> Bool {
        guard action.discreteValue != nil else { fatalError("Currently only int action types are supported") }
        let json = get(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/action_space/contains/\(action.discreteValue!)"))
        let isMember = (json as! [String:Bool])["member"]!
        return isMember
    }

    /// Close the environment instance. Must be done before upload.
    open func close(instanceID:InstanceID) {
        _ = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/close/"))
    }

    /// Start recording.
    /// - parameter directory : Location to write files. The server will create the directory if it does not exist.
    /// - parameter force : Clear out existing training data from this directory (by deleting every file prefixed with "openaigym.")
    /// - parameter resume : Retain the training data already in this directory, which will be merged with our new data
    open func startMonitor(instanceID:InstanceID, directory:String, force:Bool, resume:Bool, videoCallable:Bool) {

        let parameter = ["directory":directory,
                         "force": force,
                         "resume":resume,
                         "video_callable": videoCallable] as [String : Any]

        _ = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/monitor/start/"), parameter: parameter)
    }

    /// Stop recording and flush all data to disk.
    /// Two files will be created a meta data file like "openaigym.manifest.5.40273.manifest.json" and a performance file like "openaigym.episode_batch.11.40273.stats.json"
    open func closeMonitor(instanceID:InstanceID) {
       _ = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/monitor/close/"))
    }

    /// Shut down the server
    open func shutdown() {
        _ = post(url: baseURL.appendingPathComponent("/v1/shutdown/"))
    }

    /// Upload the results of training (as automatically recorded by your env's monitor) to OpenAI Gym.
    /// - parameter directory : Absolute path of directory containing recorder files i.e. "/tmp/swift-gym-agent"
    /// - parameter apiKey : Unique key from openai.com on your account page. Can be ignored if already contained in the environment.
    /// - parameter algorithmID : A unique identifer for the algorithm. You can safely leave this nil and it will be autogenerated.

    open func uploadResults(directory:String, apiKey:String?, algorithmID:String? = nil) {
        guard let apiKey = apiKey ?? environmentVariable(key:"OPENAI_GYM_API_KEY") else { fatalError("No API Key") }

        var data:[String:String] = ["training_dir": directory, "api_key": apiKey]
        if let algorithmID = algorithmID {
            data["algorithm_id"] = algorithmID
        }

        _ = post(url: baseURL.appendingPathComponent("/v1/upload/"), parameter: data)
    }

    // MARK: Helpers

    private func get(url:URL) -> Any? {
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        request.timeoutInterval = 120

        var json:Any?

        let semaphore = DispatchSemaphore(value: 0)
        let task = URLSession.shared.dataTask(with: request) { (data, res, error) in
            self.httpErrorHandler(data: data, res: res, error: error)
            json = try! JSONSerialization.jsonObject(with: data!, options: [.allowFragments])
            semaphore.signal()
        }
        task.resume()
        _ = semaphore.wait(timeout: .distantFuture)
        return json
    }

    private func post(url:URL, parameter:Any? = nil) -> Any? {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.timeoutInterval = 120

        if let parameter = parameter {
            request.httpBody = try! JSONSerialization.data(withJSONObject: parameter, options: [])
        }
        var json:Any?
        let semaphore = DispatchSemaphore(value: 0)

        let task = URLSession.shared.dataTask(with:request) { (data, res, error) in
            self.httpErrorHandler(data: data, res: res, error: error)

            json = try? JSONSerialization.jsonObject(with: data!, options: [.allowFragments])
            semaphore.signal()

        }
        task.resume()

        _ = semaphore.wait(timeout: .distantFuture)

        return json
    }

    private func httpErrorHandler(data:Data?, res:URLResponse?, error:Error?) {
        if let error = error {
            fatalError(error.localizedDescription)
        } else if let res = res as? HTTPURLResponse, ![200, 204].contains(res.statusCode) {
            let text = String(data:data!, encoding:String.Encoding.utf8)!
            fatalError("Error with request:\(text). Response: \(res)")
        }
    }

    private func environmentVariable(key:String) -> String? {
        guard let rawValue = getenv(key) else { return nil }
        return String(utf8String: rawValue)
    }

    private func getSpace(instanceID:InstanceID, name:String) -> Space {
        let json = get(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/\(name)/"))
        let dict = (json as! [String:AnyObject])["info"] as! [String:AnyObject]
        return Space(jsonDict: dict)
    }
}

// MARK: Models

public struct Space {

    // Name is the name of the space, such as "Box", "HighLow",
    // or "Discrete".
    public let name:String

    // Properties for Box spaces.
    public let shape:[Int]?
    public let low:[Double]?
    public let high:[Double]?

    // Properties for Discrete spaces.
    public let n:Int?

    // Properties for HighLow spaces.
    public let numberOfRows:Int?
    public let matrix:[Double]?

    public init(jsonDict:[String:AnyObject]) {
        name = jsonDict["name"] as! String

        shape = jsonDict["shape"] as! [Int]?
        low = jsonDict["low"] as! [Double]?
        high = jsonDict["high"] as! [Double]?
        n = jsonDict["n"] as! Int?
        numberOfRows = jsonDict["num_rows"] as! Int?
        matrix = jsonDict["matrix"] as! [Double]?
    }
}

public typealias InstanceID = String
public typealias EnvID = String

public typealias Action = MultiValueType
public typealias Observation = MultiValueType

public struct MultiValueType {
    public let base:AnyObject

    public init(base:AnyObject) {
        self.base = base
        if vectorValue == nil && discreteValue == nil {
            print("Unsupported value type: \(base)")
        }
    }
    public var vectorValue:[Double]? {
        return base as? [Double]
    }

    public var discreteValue:Int? {
        return base as? Int
    }
}

public struct StepResult {
    public let observation:Observation
    public let reward:Double
    public let done:Bool
    public let info:[String:AnyObject]

    public init(jsonDict:[String:AnyObject]) {
        self.observation = Observation(base: jsonDict["observation"]!)
        self.reward =  jsonDict["reward"] as! Double
        self.done = jsonDict["done"] as! Bool
        self.info = jsonDict["info"] as! [String:AnyObject]

    }
}


import UIKit
import PlaygroundSupport

let boardSize = CGSize(width: 500, height: 550)
let boardView = BoardView(frame: CGRect(origin: .zero, size: boardSize))

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = boardView


public struct Board {
    // MARK: -- Public variable's
    public var size: Int

    // MARK: -- Private variable's
    private var table: [ [PlayerSymbol?] ]

    // MARK: -- Public function's
    public init(size: Int) {
        self.size = size
        self.table = []
        self.clear()
    }

    public mutating func clear() {
        self.table = Array(repeating: Array(repeating: PlayerSymbol.empty, count: size), count: size)
    }

    public func hasEmptyField() -> Bool {
        for i in 0 ..< self.size {
            for j in 0 ..< self.size {
                if self.table[i][j] == PlayerSymbol.empty {
                    return true
                }
            }
        }
        return false
    }

    public func symbol(forPosition position: Position) -> PlayerSymbol? {
        guard position.row < self.size, position.column < size else { return nil }
        return self.table[position.row][position.column]
    }

    public mutating func makeMove(player: Player, position: Position) {
        guard self.symbol(forPosition: position) == PlayerSymbol.empty else { return }
        guard self.symbol(forPosition: position) != player.symbol else { return }

        self.table[position.row][position.column] = player.symbol
    }

    public func check(player: Player) -> BoardStatus {
        let playerSymbol: PlayerSymbol = player.symbol

        if self.foundWinInRows(playerSymbol) { return BoardStatus.win }
        if self.foundWinInColumns(playerSymbol) { return BoardStatus.win }
        if self.foundWinInSlants(playerSymbol) { return BoardStatus.win }

        if self.hasEmptyField() { return BoardStatus.continues } else { return BoardStatus.draw }
    }

    // MARK: -- Private function's
    private func foundWinInRows(_ playerSymbol: PlayerSymbol) -> Bool {
        for i in 0 ..< self.size {
            var theSameSymbolsInRowCount = 0

            for j in 0 ..< self.size - 1 {
                if self.table[i][j] == self.table[i][j+1] && (self.table[i][j] == playerSymbol) {
                    theSameSymbolsInRowCount += 1
                } else {
                    theSameSymbolsInRowCount = 0
                }
            }

            if theSameSymbolsInRowCount == self.size - 1 {
                return true
            }
        }

        return false
    }

    private func foundWinInColumns(_ playerSymbol: PlayerSymbol) -> Bool {
        for j in 0 ..< self.size {
            var theSameSymbolsInColumnCount = 0

            for i in 0 ..< self.size - 1 {
                if self.table[i][j] == self.table[i+1][j] && (self.table[i][j] == playerSymbol) {
                    theSameSymbolsInColumnCount += 1
                } else {
                    theSameSymbolsInColumnCount = 0
                }
            }

            if theSameSymbolsInColumnCount == self.size - 1 {
                return true
            }
        }

        return false
    }

    private func foundWinInSlants(_ playerSymbol: PlayerSymbol) -> Bool {
        var theSameSymbolsInSlantCount = 0

        for i in 0 ..< self.size {
            for j in -(self.size - 1) ... 0 {
                if(self.table[-j][i] == playerSymbol) {
                    var k: Int = -j
                    var l: Int = i
                    theSameSymbolsInSlantCount = 0

                    while l < self.size && k >= 0 {
                        if self.table[k][l] == playerSymbol {
                            theSameSymbolsInSlantCount += 1
                        } else {
                            theSameSymbolsInSlantCount = 0
                        }
                        k -= 1
                        l += 1

                        if theSameSymbolsInSlantCount == self.size {
                            return true
                        }
                    }
                    theSameSymbolsInSlantCount = 0
                }
                theSameSymbolsInSlantCount = 0
            }
            theSameSymbolsInSlantCount = 0
        }

        theSameSymbolsInSlantCount = 0

        for i in 0 ..< self.size {
            for j in 0 ..< self.size {
                if(self.table[j][i] == playerSymbol) {
                    var k: Int = j
                    var l: Int = i
                    theSameSymbolsInSlantCount = 0

                    while l < self.size && k < self.size {
                        if self.table[k][l] == playerSymbol {
                            theSameSymbolsInSlantCount += 1
                        } else {
                            theSameSymbolsInSlantCount = 0
                        }
                        k += 1
                        l += 1

                        if theSameSymbolsInSlantCount == self.size {
                            return true
                        }
                    }
                    theSameSymbolsInSlantCount = 0
                }
                theSameSymbolsInSlantCount = 0
            }
            theSameSymbolsInSlantCount = 0
        }

        return false
    }
}


public typealias Position = (row: Int, column: Int)


public enum BoardStatus {

    case continues

    case win

    case draw
}


public enum DifficultLevel: Int {

    case easy = 2

    case medium = 3

    case hard = 5
}


import Foundation

public class GameModel {
    // MARK: -- Public variable's
    public var board: Board!

    public var gameStatus: BoardStatus

    // MARK: -- Private variable's
    private var playersList: [Player]!

    private var movementsSequence: [Int]!

    private var actualPlayerIndex: Int!

    private var actualPlayer: Player {
        get {
            return playersList[actualPlayerIndex]
        }
    }

    private var difficultLevel: DifficultLevel = DifficultLevel.hard

    // MARK: -- Public function's
    public init(boardSize: Int, playersList: [Player], difficultLevel: DifficultLevel) {
        self.board = Board.init(size: boardSize)
        self.playersList = playersList
        self.difficultLevel = difficultLevel
        self.gameStatus = BoardStatus.continues

        self.generateMovementsSequence()
        self.changeActualPlayer()
    }

    public func update() {
        self.gameStatus = board.check(player: actualPlayer)

        switch self.gameStatus {
        case BoardStatus.continues:
            changeActualPlayer()
        case BoardStatus.draw:
            changeActualPlayer()
        default: break
        }
    }

    public func playerMakeMove(selectedPosition: (row: Int, column: Int)) {
        guard board.symbol(forPosition: selectedPosition) == PlayerSymbol.empty else { return }
        guard board.hasEmptyField() == true else { return }

        board.makeMove(player: actualPlayer, position: selectedPosition)
        update()
    }

    public func makeMinimaxMove() {
        guard actualPlayer.type == PlayerType.computer else { return }
        guard board.hasEmptyField() == true else { return }

        sleep(1)

        let selectedPosition: Position = minimaxMove(board: board, player: playersList[0], opponent: playersList[1], depth: self.difficultLevel.rawValue)
        board.makeMove(player: actualPlayer, position: selectedPosition)
        update()
    }

    public func newRound() {
        board.clear()
        gameStatus = BoardStatus.continues
        generateMovementsSequence()
        changeActualPlayer()
    }

    // MARK: -- Private function's
    private func generateMovementsSequence() {
        self.movementsSequence = []

        let playersCount = playersList.count
        let movesCount = (board.size * board.size)

        var move = Int.random(in: 0 ..< playersCount)
        movementsSequence.append(move)

        for _ in 0 ..< movesCount - 1 {
            move += 1
            movementsSequence.append(move % playersCount)
        }
    }

    private func changeActualPlayer() {
        if !movementsSequence.isEmpty {
            actualPlayerIndex = movementsSequence.first!
            movementsSequence.removeFirst()
        }
    }
}


public enum GameStateValue: Int {

    case min = -1

    case null = 0

    case max = 1
}


public func minimaxMove(board: Board, player: Player, opponent: Player, depth: Int) -> Position {
    var bestVal = GameStateValue.min
    var bestMoves: [Position] = []

    for i in 0 ..< board.size {
        for j in 0 ..< board.size {
            if board.symbol(forPosition: Position(i, j)) == PlayerSymbol.empty {
                var tempBoard = board
                let move = Position(i, j)

                tempBoard.makeMove(player: opponent, position: (i, j))
                let moveVal = minMax(board: tempBoard, player: opponent, opponent: player,
                                     depth: depth,
                                     alpha: GameStateValue.min.rawValue, beta: GameStateValue.max.rawValue,
                                     maximizingPlayer: false)

                if moveVal.rawValue > bestVal.rawValue {
                    bestVal = moveVal
                    bestMoves = []
                    bestMoves.append(move)
                } else if moveVal == bestVal {
                    bestMoves.append(move)
                }
            }
        }
    }

    return bestMoves[Int.random(in: 0 ..< bestMoves.count)]
}

public func minMax(board: Board, player: Player, opponent: Player, depth: Int, alpha: Int, beta: Int, maximizingPlayer: Bool) -> GameStateValue {
    var alpha = alpha
    var beta = beta

    if let gameResult = evaluateGameState(board: board, player: player, opponent: opponent) {
        guard depth != 0 && gameResult != GameStateValue.min && gameResult != GameStateValue.max && gameResult != GameStateValue.null else {
            return gameResult
        }
    }

    if maximizingPlayer {
        var maxEval = GameStateValue.min

        for i in 0 ..< board.size {
            for j in 0 ..< board.size {
                if board.symbol(forPosition: Position(i, j)) == PlayerSymbol.empty {
                    var tempBoard = board
                    tempBoard.makeMove(player: player, position: Position(i, j))

                    let eval = minMax(board: tempBoard, player: player, opponent: opponent, depth: depth - 1,
                                      alpha: alpha, beta: beta,
                                      maximizingPlayer: !maximizingPlayer)

                    maxEval = GameStateValue(rawValue: max(maxEval.rawValue, eval.rawValue))!
                    alpha = max(alpha, eval.rawValue)

                    if beta <= alpha { break }
                }
            }
        }

        return maxEval
    } else {
        var minEval = GameStateValue.max

        for i in 0 ..< board.size {
            for j in 0 ..< board.size {
                if board.symbol(forPosition: Position(i, j)) == PlayerSymbol.empty {
                    var tempBoard = board
                    tempBoard.makeMove(player: opponent, position: (i, j))

                    let eval = minMax(board: tempBoard, player: player, opponent: opponent, depth: depth - 1,
                                      alpha: alpha, beta: beta,
                                      maximizingPlayer: !maximizingPlayer)

                    minEval =  GameStateValue(rawValue: min(minEval.rawValue, eval.rawValue))!
                    beta = min(beta, eval.rawValue)

                    if beta <= alpha { break }
                }
            }
        }

        return minEval
    }
}

public func evaluateGameState(board: Board, player: Player, opponent: Player) -> GameStateValue? {
    if board.check(player: player) == BoardStatus.win {
        return GameStateValue.max
    } else if board.check(player: opponent) == BoardStatus.win {
        return GameStateValue.min
    } else if board.check(player: player) == BoardStatus.draw || board.check(player: opponent) == BoardStatus.draw {
        return GameStateValue.null
    }

    return nil
}


public struct Player {
    // MARK: -- Public variable's
    public var type: PlayerType

    public var symbol: PlayerSymbol

    // MARK: -- Public function's
    public init(type: PlayerType, symbol: PlayerSymbol) {
        self.type = type
        self.symbol = symbol
    }
}


public enum PlayerType {

    case computer

    case human
}


import UIKit

public class BoardView: UIView {
    // MARK: -- Public
    public var gameModel: GameModel!

    public var players = [Player(type: .human, symbol: .circle),
                          Player(type: .computer, symbol: .cross)]

    // MARK: -- Override's
    public override init(frame: CGRect) {
        super.init(frame: frame)

        self.setupBoard()
        self.setupResetButton()
        self.setupIndicator()
        self.startGame()
    }

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

    // MARK: -- Private
    private var buttons: [UIButton] = []

    private var stackView: UIStackView!

    private var resetButton: UIButton!

    private var indicator: UIActivityIndicatorView!

    private func startGame() {
        self.gameModel = GameModel.init(boardSize: 3, playersList: self.players, difficultLevel: .hard)

        DispatchQueue.global(qos: .userInteractive).async {

            self.blockViewForUser()

            self.gameModel.makeMinimaxMove()

            self.unblockViewForUser()
        }
    }

    private func updateUI() {
        if gameModel.gameStatus != BoardStatus.continues {
            self.resetButton.setTitle("New game", for: .normal)
            blockButtons()
        } else {
            self.resetButton.setTitle("Reset", for: .normal)
        }
        boardToButtons()
    }

    private func boardToButtons() {
        var buttonIndex = 0

        for row in 0 ..< 3 {
            for column in 0 ..< 3 {
                let symbol = gameModel.board.symbol(forPosition: Position(row, column))
                if symbol != PlayerSymbol.empty {
                    self.buttons[buttonIndex].setTitle(symbol?.rawValue, for: .normal)
                    self.buttons[buttonIndex].isUserInteractionEnabled = false
                }
                buttonIndex += 1
            }
        }
    }

    private func setupBoard() {
        self.stackView = UIStackView()
        self.stackView.translatesAutoresizingMaskIntoConstraints = false
        self.stackView.axis = .vertical
        self.stackView.alignment = .fill
        self.stackView.distribution = .fillEqually
        self.stackView.spacing = 10

        self.addSubview(self.stackView)

        for index in 1 ... 3 {
            let boardRow = self.createBoardRow(rowNumber: index)
            self.stackView.addArrangedSubview(boardRow)
        }

        // constraints
        let constraints = [
            self.stackView.topAnchor.constraint(equalTo: self.topAnchor, constant: 10),
            self.stackView.centerXAnchor.constraint(equalTo: self.centerXAnchor),
            self.stackView.widthAnchor.constraint(equalTo: self.widthAnchor, constant: -20),
            self.stackView.heightAnchor.constraint(equalTo: self.stackView.widthAnchor)
        ]
        NSLayoutConstraint.activate(constraints)
    }

    private func createBoardRow(rowNumber: Int) -> UIStackView {
        let stackView = UIStackView()
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .horizontal
        stackView.alignment = .fill
        stackView.distribution = .fillEqually
        stackView.spacing = 10

        for index in 1 ... 3 {
            let button = UIButton()
            let id = String(index + ( (rowNumber - 1) * 3 ) )
            button.restorationIdentifier = id
            button.backgroundColor = .lightGray
            button.titleLabel?.font = UIFont(name: "Helvetica", size: 50)
            button.addTarget(self, action: #selector(buttonPressed(_:)), for: .touchUpInside)

            self.buttons.append(button)
            stackView.addArrangedSubview(button)
        }

        return stackView
    }

    private func blockViewForUser() {
        DispatchQueue.main.async {
            self.resetButton.isHidden = true
            self.indicator.isHidden = false
            self.indicator.startAnimating()

            self.blockButtons()
            self.updateUI()
        }
    }

    private func unblockViewForUser() {
        DispatchQueue.main.async {
            self.unblockButtons()
            self.updateUI()

            self.resetButton.isHidden = false
            self.indicator.isHidden = true
            self.indicator.stopAnimating()
        }
    }

    @objc private func buttonPressed(_ sender: UIButton) {
        let position = buttonIDtoPosition(id: sender.restorationIdentifier!)

        DispatchQueue.global(qos: .userInteractive).async {
            self.gameModel.playerMakeMove(selectedPosition: position)

            self.blockViewForUser()

            self.gameModel.makeMinimaxMove()

            self.unblockViewForUser()
        }
    }

    private func setupResetButton() {
        self.resetButton = UIButton(type: .system)
        self.resetButton.translatesAutoresizingMaskIntoConstraints = false
        self.resetButton.setTitle("Reset", for: .normal)
        self.resetButton.backgroundColor = .lightGray
        self.resetButton.addTarget(self, action: #selector(resetButtonPressed(_:)), for: .touchUpInside)

        self.addSubview(self.resetButton)

        // constraints
        let constraints = [
            self.resetButton.topAnchor.constraint(equalTo: self.stackView.bottomAnchor, constant: 10),
            self.resetButton.bottomAnchor.constraint(equalTo: self.bottomAnchor),
            self.resetButton.widthAnchor.constraint(equalTo: self.widthAnchor)
        ]
        NSLayoutConstraint.activate(constraints)
    }

    @objc private func resetButtonPressed(_ sender: UIButton) {
        self.gameModel.newRound()
        self.clearButtons()
        self.startGame()
    }

    private func setupIndicator() {
        self.indicator = UIActivityIndicatorView()
        self.indicator.translatesAutoresizingMaskIntoConstraints = false
        self.indicator.backgroundColor = .lightGray

        self.addSubview(self.indicator)

        // constraints
        let constraints = [
            self.indicator.topAnchor.constraint(equalTo: self.stackView.bottomAnchor, constant: 10),
            self.indicator.bottomAnchor.constraint(equalTo: self.bottomAnchor),
            self.indicator.widthAnchor.constraint(equalTo: self.widthAnchor)
        ]
        NSLayoutConstraint.activate(constraints)
    }

    private func buttonIDtoPosition(id: String) -> Position {
        switch id {
        case "1":
            return Position(0, 0)
        case "2":
            return Position(0, 1)
        case "3":
            return Position(0, 2)
        case "4":
            return Position(1, 0)
        case "5":
            return Position(1, 1)
        case "6":
            return Position(1, 2)
        case "7":
            return Position(2, 0)
        case "8":
            return Position(2, 1)
        case "9":
            return Position(2, 2)
        default:
            return Position(0, 0)
        }
    }

    private func clearButtons() {
        for button in self.buttons {
            button.setTitle("", for: .normal)
            button.isUserInteractionEnabled = true
        }
    }

    private func unblockButtons() {
        for button in self.buttons {
            button.isUserInteractionEnabled = true
        }
    }

    private func blockButtons() {
        for button in self.buttons {
            button.isUserInteractionEnabled = false
        }
    }
}


import XCTest

class BoardTests: XCTestCase {

    private var sut: Board!

    private var boardSize = 3

    override func setUp() {
        super.setUp()
        sut = Board(size: boardSize)
    }

    override func tearDown() {
        sut = nil
        super.tearDown()
    }

    func testInit() {
        XCTAssertEqual(sut.size, boardSize)
        XCTAssertEqual(allFieldsAreEmpty(), true)
    }

    func testSymbolForPosition() {
        let player = Player(type: .human, symbol: .circle)
        let position = Position(0, 0)

        sut.clear()
        XCTAssertEqual(sut.symbol(forPosition: position), PlayerSymbol.empty)

        sut.makeMove(player: player, position: position)
        XCTAssertEqual(sut.symbol(forPosition: position), player.symbol)
    }

    func testClear() {
        let player = Player(type: .computer, symbol: .circle)
        let position = Position(0, 0)

        sut.makeMove(player: player, position: position)

        XCTAssertEqual(allFieldsAreEmpty(), false)

        sut.clear()

        XCTAssertEqual(allFieldsAreEmpty(), true)
    }

    func testHasEmptyField() {
        let player = Player(type: .computer, symbol: .circle)

        sut.clear()

        XCTAssertEqual(sut.hasEmptyField(), true)

        sut.makeMove(player: player, position: Position(0, 0))
        sut.makeMove(player: player, position: Position(0, 1))
        sut.makeMove(player: player, position: Position(0, 2))

        sut.makeMove(player: player, position: Position(1, 0))
        sut.makeMove(player: player, position: Position(1, 1))
        sut.makeMove(player: player, position: Position(1, 2))

        sut.makeMove(player: player, position: Position(2, 0))
        sut.makeMove(player: player, position: Position(2, 1))
        sut.makeMove(player: player, position: Position(2, 2))

        XCTAssertEqual(sut.hasEmptyField(), false)
    }

    func testMakeMove() {
        let firstPlayer = Player(type: .human, symbol: .circle)
        let secondPlayer = Player(type: .human, symbol: .cross)
        let position = Position(0, 0)

        sut.clear()
        sut.makeMove(player: firstPlayer, position: position)
        sut.makeMove(player: secondPlayer, position: position)

        XCTAssertEqual(sut.symbol(forPosition: position), firstPlayer.symbol)
    }

    func testCheck() {
        let firstPlayer = Player(type: .computer, symbol: .circle)
        let secondPlayer = Player(type: .computer, symbol: .cross)

        sut.clear()

        XCTAssertEqual(sut.check(player: firstPlayer), BoardStatus.continues)
        XCTAssertEqual(sut.check(player: secondPlayer), BoardStatus.continues)

        sut.clear()
        sut.makeMove(player: firstPlayer, position: Position(0, 0))
        sut.makeMove(player: firstPlayer, position: Position(0, 1))
        sut.makeMove(player: firstPlayer, position: Position(0, 2))

        XCTAssertEqual(sut.check(player: firstPlayer), BoardStatus.win)
        XCTAssertEqual(sut.check(player: secondPlayer), BoardStatus.continues)

        sut.clear()
        sut.makeMove(player: firstPlayer, position: Position(0, 0))
        sut.makeMove(player: firstPlayer, position: Position(1, 0))
        sut.makeMove(player: firstPlayer, position: Position(2, 0))

        XCTAssertEqual(sut.check(player: firstPlayer), BoardStatus.win)
        XCTAssertEqual(sut.check(player: secondPlayer), BoardStatus.continues)

        sut.clear()
        sut.makeMove(player: firstPlayer, position: Position(0, 0))
        sut.makeMove(player: firstPlayer, position: Position(1, 1))
        sut.makeMove(player: firstPlayer, position: Position(2, 2))

        XCTAssertEqual(sut.check(player: firstPlayer), BoardStatus.win)
        XCTAssertEqual(sut.check(player: secondPlayer), BoardStatus.continues)

        sut.clear()
        sut.makeMove(player: firstPlayer, position: Position(0, 0))
        sut.makeMove(player: secondPlayer, position: Position(0, 1))
        sut.makeMove(player: secondPlayer, position: Position(0, 2))

        sut.makeMove(player: secondPlayer, position: Position(1, 0))
        sut.makeMove(player: firstPlayer, position: Position(1, 1))
        sut.makeMove(player: firstPlayer, position: Position(1, 2))

        sut.makeMove(player: secondPlayer, position: Position(2, 0))
        sut.makeMove(player: firstPlayer, position: Position(2, 1))
        sut.makeMove(player: secondPlayer, position: Position(2, 2))

        XCTAssertEqual(sut.check(player: firstPlayer), BoardStatus.draw)
        XCTAssertEqual(sut.check(player: secondPlayer), BoardStatus.draw)
    }

    private func allFieldsAreEmpty() -> Bool {
        var allFieldAreEmpty = true

        for row in 0 ..< sut.size {
            for column in 0 ..< sut.size {
                if sut.symbol(forPosition: Position(row, column)) != PlayerSymbol.empty {
                    allFieldAreEmpty = false
                }
            }
        }

        return allFieldAreEmpty
    }
}


import XCTest

class MinimaxTests: XCTestCase {
    override func setUp() {
        super.setUp()
    }

    override func tearDown() {
        super.tearDown()
    }

    func testEvaluateGameState() {
        var board = Board(size: 3)
        let firstPlayer = Player(type: .human, symbol: .cross)
        let secondPlayer = Player(type: .human, symbol: .circle)

        board.clear()

        XCTAssertEqual(evaluateGameState(board: board, player: firstPlayer, opponent: secondPlayer), nil)

        board.makeMove(player: firstPlayer, position: Position(0, 0))

        XCTAssertEqual(evaluateGameState(board: board, player: firstPlayer, opponent: secondPlayer), nil)

        board.makeMove(player: firstPlayer, position: Position(0, 1))
        board.makeMove(player: firstPlayer, position: Position(0, 2))

        XCTAssertEqual(evaluateGameState(board: board, player: firstPlayer, opponent: secondPlayer), .max)
        XCTAssertEqual(evaluateGameState(board: board, player: secondPlayer, opponent: firstPlayer), .min)

        board.clear()
        board.makeMove(player: secondPlayer, position: Position(0, 0))
        board.makeMove(player: secondPlayer, position: Position(0, 1))
        board.makeMove(player: secondPlayer, position: Position(0, 2))
        board.makeMove(player: firstPlayer, position: Position(1, 0))

        XCTAssertEqual(evaluateGameState(board: board, player: firstPlayer, opponent: secondPlayer), .min)
        XCTAssertEqual(evaluateGameState(board: board, player: secondPlayer, opponent: firstPlayer), .max)

        board.clear()
        board.makeMove(player: firstPlayer, position: Position(0, 0))
        board.makeMove(player: secondPlayer, position: Position(0, 1))
        board.makeMove(player: secondPlayer, position: Position(0, 2))

        board.makeMove(player: secondPlayer, position: Position(1, 0))
        board.makeMove(player: firstPlayer, position: Position(1, 1))
        board.makeMove(player: firstPlayer, position: Position(1, 2))

        board.makeMove(player: secondPlayer, position: Position(2, 0))
        board.makeMove(player: firstPlayer, position: Position(2, 1))
        board.makeMove(player: secondPlayer, position: Position(2, 2))

        XCTAssertEqual(evaluateGameState(board: board, player: firstPlayer, opponent: secondPlayer), .null)
        XCTAssertEqual(evaluateGameState(board: board, player: secondPlayer, opponent: firstPlayer), .null)
    }
}


import XCTest

class PlayerTests: XCTestCase {

    private var sut: Player!

    private var playerType: PlayerType = .human

    private var playerSymbol: PlayerSymbol = .circle

    override func setUp() {
        super.setUp()
        sut = Player(type: playerType, symbol: playerSymbol)
    }

    override func tearDown() {
        sut = nil
        super.tearDown()
    }

    func testInit() {
        XCTAssertEqual(sut.type, playerType)
        XCTAssertEqual(sut.symbol, playerSymbol)
    }
}


import Foundation

enum ShuntingYard {
    enum Operator: String, CaseIterable {
        case power = "^"
        case plus = "+"
        case minus = "-"
        case times = "*"
        case divide = "/"
    }

    static func evaluate(_ string: String) -> Double {
        let scanner = Scanner(string: string)
        var numberStack: [Double] = []
        var operatorStack: [Operator] = []

        func applyOperator(_ op: Operator) {
            guard let a = numberStack.popLast(), let b = numberStack.popLast() else {
                return
            }

            numberStack.append(op.apply(a, b))
        }

        while !scanner.isAtEnd {
            if let op = scanner.scanOperator() {
                while let last = operatorStack.last, last.precedence > op.precedence || (op.leftAssociative && last.precedence == op.precedence) {
                    applyOperator(last)
                    operatorStack.removeLast()
                }
                operatorStack.append(op)
            } else if let number = scanner.scanDouble() {
                numberStack.append(number)
            } else {
                break
            }
        }

        while let op = operatorStack.popLast() {
            applyOperator(op)
        }

        return numberStack.first ?? 0
    }
}

extension ShuntingYard.Operator {
    var precedence: Int {
        switch self {
        case .power: return 3
        case .divide, .times: return 2
        case .plus, .minus: return 1
        }
    }

    var leftAssociative: Bool {
        switch self {
        case .power: return false
        case .plus, .minus, .times, .divide: return true
        }
    }

    func apply(_ a: Double, _ b: Double) -> Double {
        switch self {
        case .power: return pow(b, a)
        case .divide: return b / a
        case .times: return a * b
        case .plus: return a + b
        case .minus: return b - a
        }
    }
}

private extension Scanner {
    func scanOperator() -> ShuntingYard.Operator? {
        for op in ShuntingYard.Operator.allCases {
            if scanString(op.rawValue) != nil {
                return op
            }
        }
        return nil
    }
}

func testShuntingYard() {
    func test(_ x: String) {
        print(x,"=", ShuntingYard.evaluate(x))
    }

    test("3 + 4 * 5")
    test("4 * 5 + 3")
    test("2 ^ 3 ^ 4")
    test("10.5 - 4 * 5")
    test("2 + 3 ^ 4")
    test("2 * 3 ^ 4")
    test("3 ^ 4")
}


struct Heap<Element> {
    let compare: (Element, Element) -> Bool
    private var items : [Element]

    init(_ items : [Element], compare: @escaping (Element, Element) -> Bool) {
        self.compare = compare
        self.items = items
        for index in (0 ..< count / 2).reversed() {
            heapify(index)
        }
    }

    /// The minimum item on this heap or nil if the heap is empty
    var min: Element? {
        return items.first
    }

    /// The number of items on this heap
    var count: Int {
        return items.count
    }

    /// true if this heap is empty
    var isEmpty: Bool {
        return items.isEmpty
    }

    /// Removes and returns the minimum item from the heap.
    /// - returns: The minimum item from the heap or nil if the heap is empty.
    mutating func extractMin() -> Element? {
        guard let result = items.first else { return nil }

        items.removeFirst()
        heapify(0)
        return result

    }

    /// Inserts a new item into this heap
    /// - parameter item: The new item to insert
    mutating func insert(item : Element) {
        items.append(item)
        var i = items.count - 1
        while i > 0 && compare(items[i], items[parent(i)]) {
            items.swapAt(i, parent(i))
            i = parent(i)
        }
    }

    /// Restores the heap property starting at a given index
    /// - parameter index: The index to start at
    private mutating func heapify(_ index : Int) {
        var minimumIndex = index
        if left(index) < count && compare(items[left(index)], items[minimumIndex]) {
            minimumIndex = left(index)
        }

        if right(index) < count && compare(items[right(index)], items[minimumIndex]) {
            minimumIndex = right(index)
        }

        if minimumIndex != index {
            items.swapAt(minimumIndex, index)
            heapify(minimumIndex)
        }
    }

    /// Returns the index of the left child of an item
    private func left(_ index : Int) -> Int {
        return 2 * index + 1
    }

    /// Returns the index of the right child of an item
    private func right(_ index: Int) -> Int {
        return 2 * index + 2
    }

    /// Returns the index of the parent of an item
    private func parent(_ index: Int) -> Int {
        return (index - 1) / 2
    }
}

extension Heap: ExpressibleByArrayLiteral where Element: Comparable {
    init(arrayLiteral elements: Element...) {
        self.init(elements, compare: <)
    }

    init(_ elements: [Element]) {
        self.init(elements, compare: <)
    }
}


import Foundation

public class Node<Value> {
    public var value: Value?
    public var next: Node?

    public init(value: Value? = nil, next: Node<Value>? = nil) {
        self.value = value
        self.next = next
    }
}

extension Node: CustomStringConvertible {
    public var description: String {
        guard let next = next else {
            return "\(String(describing: value))"
        }
        return "\(String(describing: value)) -> " + String(describing: next) + " "
    }
}

public struct LinkedList<Value> {

    public var head: Node<Value>?
    public var tail: Node<Value>?

    public init() {}

    public var isEmpty: Bool {
        return head == nil
    }
    public mutating func push(_ value: Value) {
        head = Node(value: value, next: head)

        if tail == nil {
            tail = head
        }
    }

    public mutating func append(_ value: Value) {
        guard !isEmpty else {
            push(value)
            return
        }

        tail!.next = Node(value: value)

        tail = tail!.next
    }

    public func node(at index: Int) -> Node<Value>? {
        var currentNode = head
        var currentIndex = 0

        while currentNode != nil && currentIndex < index {
            currentNode = currentNode!.next
            currentIndex += 1
        }

        return currentNode
    }

    @discardableResult
    public mutating func insert(_ value: Value,
                                after node: Node<Value>) -> Node<Value> {
        guard tail !== node else {
          append(value)
          return tail!
        }
        node.next = Node(value: value, next: node.next)

        return node.next!
    }

    @discardableResult
    public mutating func pop() -> Value? {
        defer {
            head = head?.next
            if isEmpty {
                tail = nil
            }
        }
        return head?.value
    }

    @discardableResult
    public mutating func removeLast() -> Value? {
      guard let head = head else {
    return nil
    }
      guard head.next != nil else {
        return pop()
      }
      var prev = head
      var current = head
      while let next = current.next {
        prev = current
        current = next
      }
      prev.next = nil
      tail = prev
      return current.value
    }

    @discardableResult
    public mutating func remove(after node: Node<Value>) -> Value? {
      defer {
        if node.next === tail {
          tail = node
        }
        node.next = node.next?.next
      }
      return node.next?.value
    }
}

extension LinkedList: CustomStringConvertible {
    public var description: String {
        guard let head = head else {
          return "Empty list"
        }
        return String(describing: head)
      }
}

// Below you can find a testing Scenario for Playground.

/*
 import UIKit

 // Test Linked List

 let node1 = Node(value: 1)
 let node2 = Node(value: 2)
 let node3 = Node(value: 3)

 node1.next = node2
 node2.next = node3

 print(node1)

 var list = LinkedList<Int>()
 list.push(3)
 list.push(2)
 list.push(1)

 print(list)

 var listAppend = LinkedList<Int>()

 listAppend.append(1)
 listAppend.append(2)
 listAppend.append(3)

 print(listAppend)
 */


// Create simple queue
// Tejas Nanaware

struct Queue<T> {
  private var elements: [T] = []

  mutating func push(_ value: T) {
    elements.append(value)
  }

  mutating func pop() -> T? {
    guard !elements.isEmpty else {
      return nil
    }
    return elements.removeFirst()
  }
}

var queue = Queue<String>()

queue.push("One")
queue.push("Two")
queue.push("Three")

print(queue.pop())
print(queue)
print(queue.pop())
print(queue)
print(queue.pop())
print(queue)


import Foundation

public struct Stack<T> {
    private var elements = [T]()

    public mutating func push(_ element: T) {
        elements.append(element)
    }

    public mutating func pop() -> T? {
        return elements.popLast()
    }

    public var isEmpty: Bool {
        return elements.isEmpty
    }

    public var count: Int {
        return elements.count
    }

    public var peek: T? {
        return elements.last
    }
}

// The code below can be used for testing

var stack = Stack<Int>()

stack.push(10)
stack.push(20)
stack.push(30)

print(stack.count)
print(stack.peek)
print(stack.isEmpty)

print(stack.pop())
print(stack.pop())
print(stack.pop())
print(stack.isEmpty)
print(stack.count)


class UnionFindNode {
    var rank = 0

    private var parent: UnionFindNode? = nil

    func findRoot() -> UnionFindNode {
        var x = self
        while let parent = x.parent {
            x.parent = parent.parent ?? parent
            x = parent
        }
        return x
    }

    @discardableResult
    static func union(_ x: UnionFindNode, _ y: UnionFindNode) -> UnionFindNode {
        var x = x.findRoot()
        var y = y.findRoot()

        guard x !== y else { return x }

        if x.rank < y.rank {
            swap(&x, &y)
        }

        y.parent = x
        if x.rank == y.rank {
            x.rank = y.rank + 1
        }

        return x
    }

    static func inSameSet(_ x: UnionFindNode, _ y: UnionFindNode) -> Bool {
        return x.findRoot() === y.findRoot()
    }
}

func testUnionFind() {
    let a = UnionFindNode()
    let b = UnionFindNode()
    let c = UnionFindNode()

    print("a, b", UnionFindNode.inSameSet(a, b))
    print("b, c", UnionFindNode.inSameSet(b, c))
    print("a, c", UnionFindNode.inSameSet(a, c))

    print("Joining a, b")

    UnionFindNode.union(a, b)
    print("a, b", UnionFindNode.inSameSet(a, b))
    print("b, c", UnionFindNode.inSameSet(b, c))
    print("a, c", UnionFindNode.inSameSet(a, c))

    print("Joining b, c")

    UnionFindNode.union(b, c)
    print("a, b", UnionFindNode.inSameSet(a, b))
    print("b, c", UnionFindNode.inSameSet(b, c))
    print("a, c", UnionFindNode.inSameSet(a, c))

    print("New node d")
    let d = UnionFindNode()

    print("a, d", UnionFindNode.inSameSet(a, d))

    print("Joining d, c")
    UnionFindNode.union(d, c)
    print("a, d", UnionFindNode.inSameSet(a, d))

}


enum Kruskal {
    struct Vertex {
        let name: String
        let node = UnionFindNode()

        init(_ name: String) {
            self.name = name
        }
    }

    struct Edge {
        let from: Vertex
        let to: Vertex
        let weight: Int
    }

    typealias Graph = [Edge]

    static func kruskal(_ graph: Graph) -> Graph {
        var edges = Heap(graph) { $0.weight < $1.weight }

        var result: Graph = []
        result.reserveCapacity(edges.count)

        while let edge = edges.extractMin() {
            guard !UnionFindNode.inSameSet(edge.from.node, edge.to.node) else {
                continue
            }
            UnionFindNode.union(edge.from.node, edge.to.node)
            result.append(edge)
        }

        return result
    }
}

extension Kruskal.Vertex: CustomStringConvertible {
    var description: String { name }
}

extension Kruskal.Edge: CustomStringConvertible {
    var description: String { "\(from) --(\(weight))-- \(to)" }
}

func testKruskal() {
    typealias Vertex = Kruskal.Vertex
    typealias Edge = Kruskal.Edge

    let A = Vertex("A")
    let B = Vertex("B")
    let C = Vertex("C")
    let D = Vertex("D")
    let E = Vertex("E")
    let F = Vertex("F")
    let G = Vertex("G")

    let graph = [
        Edge(from: A, to: B, weight: 7),
        Edge(from: A, to: D, weight: 5),
        Edge(from: B, to: C, weight: 8),
        Edge(from: B, to: D, weight: 9),
        Edge(from: B, to: E, weight: 7),
        Edge(from: C, to: E, weight: 5),
        Edge(from: D, to: E, weight: 15),
        Edge(from: D, to: F, weight: 6),
        Edge(from: E, to: F, weight: 8),
        Edge(from: E, to: G, weight: 9),
        Edge(from: F, to: G, weight: 11),
    ]

    print(Kruskal.kruskal(graph).map { String(describing: $0) }.joined(separator: "\n") )
}


// The Fibonacci numbers, commonly denoted F(n) form a sequence,
// called the Fibonacci sequence, such that # each number is the sum
// of the two preceding ones, starting from 0 and 1. That is,
//
// F(0) = 0, F(1) = 1
// F(n) = F(n - 1) + F(n - 2), for n > 1
//
// Given n, calculate F(n).
//
// @leticiafaleia
func fibonacci(_ number: Int) -> Int {
    guard number > 1 else { return number }
    return fibonacci(number - 1) + fibonacci(number - 2)
}

fibonacci(5)


import Foundation

public func binarySearch<T: Comparable>(_ a: [T], key: T) -> Int? {
    var lowerBound = 0
    var upperBound = a.count
    while lowerBound < upperBound {
        let midIndex = lowerBound + (upperBound - lowerBound) / 2
        if a[midIndex] == key {
            return midIndex
        } else if a[midIndex] < key {
            lowerBound = midIndex + 1
        } else {
            upperBound = midIndex
        }
    }
    return nil
}
// The code below can be used for testing

// var numbers = [7, 10, 13, 17, 19, 24, 29, 31, 32, 37, 41, 43, 47, 51, 53, 119, 163, 611, 627]
// if let searchIndex = binarySearch(numbers, key: 10) {
//     print("Element found on index: \(searchIndex)")
// }
// else {
// print("Element not found")
// }


import Foundation

func linearSearch<T: Equatable>(_ array: [T], _ object: T) -> Int? {
  for (index, obj) in array.enumerated() where obj == object {
    return index
  }
  return nil
}

// The code below can be used for testing

// var numbers = [10, 119, 13, 24, 53, 17, 31, 7, 19, 627, 47, 163, 37, 611, 29, 43, 51, 41, 32]
// if let searchIndex = linearSearch(numbers,31) {
//     print("Element found on index: \(searchIndex)")
// }
// else {
// print("Element not found")
// }


import Foundation

extension Array where Element: Comparable {

    func bubbleSort(by areInIncreasingOrder: ((Element, Element) -> Bool) = (<)) -> [Element] {
        var data = self

        for i in 0..<(data.count-1) {
            for j in 0..<(data.count-i-1) where areInIncreasingOrder(data[j+1], data[j]) {
                data.swapAt(j, j + 1)
            }
        }

        return data
    }
}

func swap<T: Comparable>(left: inout T, right: inout T) {
    print("Swapping \(left) and \(right)")
    let temp = right
    right = left
    left = temp
}

// The code below can be used for testing

// let numberList : Array<Int> = [8, 2, 10, 9, 7, 5]
// let results: Array<Int> = numberList.bubbleSort()
// print(results)


import Foundation

func insertionSort<T>(_ array: [T], by comparison: (T, T) -> Bool) -> [T] {
    guard array.count > 1 else { return array }

    var sortedArray = array

    for index in 1..<sortedArray.count {
        var currentIndex = index
        let temp = sortedArray[currentIndex]

        while currentIndex > 0, comparison(temp, sortedArray[currentIndex - 1]) {
            sortedArray[currentIndex] = sortedArray[currentIndex - 1]
            currentIndex -= 1
        }
        sortedArray[currentIndex] = temp
    }

    return sortedArray
}

// The code below can be used for testing

/*
let numbers = [10, 1, 3, 8, 4, 2]

print(insertionSort(numbers, by: >))
print(insertionSort(numbers, by: <))

let names = ["Jack", "Paul", "Olivia", "Emma", "Michael"]

print(insertionSort(names, by: >))
print(insertionSort(names, by: <))
*/


import Foundation

extension Array where Element: Comparable {

	mutating func mergeSort(by comparison: (Element, Element) -> Bool) {
		guard self.count > 1 else {
			return
		}
		_mergeSort(from: 0, to: count - 1, by: comparison)
	}

	mutating private func _mergeSort(
		from left: Int,
		to right: Int,
		by comparison: (Element, Element) -> Bool
	) {
		if left < right {
			let mid = left + (right - left) / 2
			_mergeSort(from: 0, to: mid, by: comparison)
			_mergeSort(from: mid + 1, to: right, by: comparison)
			_merge(from: left, mid: mid, to: right, by: comparison)
		}
	}

	mutating private func _merge(
		from left: Int,
		mid: Int,
		to right: Int,
		by comparison: (Element, Element) -> Bool
	) {
		var copy = [Element](repeating: self[left], count: right - left + 1)
		var (leftStartIndex, rightStartIndex, currentIndex) = (left, mid + 1, 0)
		for _ in left ... right {
			if leftStartIndex > mid {
				copy[currentIndex] = self[rightStartIndex]
				rightStartIndex += 1
			} else if rightStartIndex > right {
				copy[currentIndex] = self[leftStartIndex]
				leftStartIndex += 1
			} else if comparison(self[leftStartIndex], self[rightStartIndex]) {
				copy[currentIndex] = self[leftStartIndex]
				leftStartIndex += 1
			} else {
				copy[currentIndex] = self[rightStartIndex]
				rightStartIndex += 1
			}
			currentIndex += 1
		}
		leftStartIndex = left
		for i in copy.indices {
			self[leftStartIndex] = copy[i]
			leftStartIndex += 1
		}
	}

	func mergeSorted(by comparison: (Element, Element) -> Bool) -> Array {
		var copy = self
		copy.mergeSort(by: comparison)
		return copy
	}

}

// The code below can be used for testing
// var numberList = [15, 2, 23, 11, 3, 9]
// debugPrint(numberList.mergeSorted(by: >))
// numberList.mergeSort(by: <)
// debugPrint(numberList)


import Foundation

extension Array where Element: Comparable {
    /// Sorts the array using the QuickSort algorithm in place.
    ///
    /// The QuickSort algorithm sorts the array by first choosing a pivot. This pivot is used to rearrange
    /// all elements, moving the smaller ones to the left of it. This operation is then recursevely applied
    /// to the subarrays formed around the pivot.
    mutating func quickSort() {
        guard count > 1 else {
            return
        }

        _quickSort(from: 0, to: count - 1)
    }

    mutating private func _quickSort(from left: Int, to right: Int) {
        guard left < right, right - left > 0 else {
            return
        }

        let pivotIndex = partition(from: left, to: right)
        _quickSort(from: left, to: pivotIndex - 1)
        _quickSort(from: pivotIndex + 1, to: right)
    }

    /// This method is where the pivot is chosen, so the smaller elements get moved to the left,
    /// and the bigger ones to the right.
    mutating private func partition(from left: Int, to right: Int) -> Int {
        /// Chooses the pivot, which in this case is always the first element, which is not very efficient.
        let pivotIndex = left
        swapAt(pivotIndex, right)

        let pivot = self[right]
        var i = left

        for j in i ..< right {
            // If the element is smaller than the pivot, move it to the left.
            if self[j] <= pivot {
                swapAt(i, j)
                i += 1
            }
        }

        // Move the pivot to its right sorted position.
        swapAt(i, right)

        return i
    }

    /// Returns a sorted version of this array using the QuickSort algorithm.
    func quickSorted() -> Array {
        var copy = self

        copy.quickSort()

        return copy
    }
}

// Use the following code to test it:
// var numbers = [1002, 42, 55, 124, 205]
// debugPrint(numbers.quickSorted())
//
// numbers.quickSort()
// debugPrint(numbers)
//
// The console should print:
// [42, 55, 124, 205, 1002]
// [42, 55, 124, 205, 1002]


import Foundation

extension Array where Element: Comparable {
func selectionSort() -> Array<Element> {

        guard self.count > 1 else {
            return self
        }

        var output: Array<Element> = self

        for primaryindex in 0..<output.count {

            var minimum = primaryindex
            var secondaryindex = primaryindex + 1

            while secondaryindex < output.count {

                if output[minimum] > output[secondaryindex] {
                    minimum = secondaryindex
                }
                secondaryindex += 1
            }

            if primaryindex != minimum {
            output.swapAt(primaryindex, minimum)
            }
        }

        return output
    }
}

// The code below can be used for testing

// let numberList : Array<Int> = [15, 2, 23, 11, 3, 9]
// let results: Array<Int> = numberList.selectionSort()
// print(results)


import Foundation

public class TreeNode<T> {
    public var value: T

    public weak var parent: TreeNode?
    public var children = [TreeNode<T>]()

    public init(value: T) {
        self.value = value
    }

    public func addChild(_ node: TreeNode<T>) {
        children.append(node)
        node.parent = self
    }
}

/* Checks the node's value property, if there is no match, check the child nodes.
Repeat the same process recursively */
extension TreeNode where T: Equatable {
    func search(_ value: T) -> TreeNode? {
        if value == self.value {
            return self
        }
        for child in children {
            if let found = child.search(value) {
                return found
            }
        }
        return nil
    }
}

// The code below can be used for testing
let tree = TreeNode<String>(value: "animals")

let reptilesNode = TreeNode<String>(value: "reptiles")
let mammalsNode = TreeNode<String>(value: "mammals")

let lizardsNode = TreeNode<String>(value: "lizards")
let snakesNode = TreeNode<String>(value: "snakes")

let dogsNode = TreeNode<String>(value: "dogs")
let humansNode = TreeNode<String>(value: "humans")

tree.addChild(reptilesNode)
tree.addChild(mammalsNode)

reptilesNode.addChild(lizardsNode)
reptilesNode.addChild(snakesNode)

mammalsNode.addChild(dogsNode)
mammalsNode.addChild(humansNode)

print(tree.search("humans")?.value)
print(tree.search("lizards")?.value)
print(tree.search("dragons")?.value)


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

import Foundation

@objcMembers public class DefaultEventAnalyticsController: NSObject, EventAnalyticsController {
    private var eventAnalyticObservers = ConcurrentMutableSet()
    private let meetingStatsCollector: MeetingStatsCollector
    private let meetingSessionConfig: MeetingSessionConfiguration
    private let logger: Logger
    private let eventReporter: EventReporter?

    init(meetingSessionConfig: MeetingSessionConfiguration,
         meetingStatsCollector: MeetingStatsCollector,
         logger: Logger,
         eventReporter: EventReporter?)
    {
        self.meetingSessionConfig = meetingSessionConfig
        self.meetingStatsCollector = meetingStatsCollector
        self.eventReporter = eventReporter
        self.logger = logger
        super.init()
    }

    convenience init(meetingSessionConfig: MeetingSessionConfiguration,
                     meetingStatsCollector: MeetingStatsCollector,
                     logger: Logger)
    {
        self.init(meetingSessionConfig: meetingSessionConfig,
                  meetingStatsCollector: meetingStatsCollector,
                  logger: logger,
                  eventReporter: nil)
    }

    public func publishEvent(name: EventName, attributes: [AnyHashable: Any]) {
        var mutatedAttributes = attributes
        let timestampMs = DateUtils.getCurrentTimeStampMs()
        mutatedAttributes[EventAttributeName.timestampMs] = timestampMs

        meetingStatsCollector.addMeetingHistoryEvent(historyEventName: Converters.MeetingEventName.toMeetingHistoryEventName(name: name),
                                                     timestampMs: timestampMs)

        switch name {
        case .meetingFailed,
             .meetingEnded,
             .meetingStartFailed,
             .meetingStartSucceeded:
            let meetingStats = meetingStatsCollector.getMeetingStats()
            mutatedAttributes.merge(meetingStats) { _, newVal in newVal }
        default:
            break
        }

        eventReporter?.report(event: SDKEvent(eventName: name, eventAttributes: mutatedAttributes))

        ObserverUtils.forEach(observers: eventAnalyticObservers) { (eventAnalyticObserver: EventAnalyticsObserver) in
            eventAnalyticObserver.eventDidReceive(name: name, attributes: mutatedAttributes)
        }
    }

    public func getMeetingHistory() -> [MeetingHistoryEvent] {
        return meetingStatsCollector.getMeetingHistory()
    }

    public func publishEvent(name: EventName) {
        publishEvent(name: name, attributes: [AnyHashable: Any]())
    }

    public func pushHistory(historyEventName: MeetingHistoryEventName) {
        let currentTimeMs = DateUtils.getCurrentTimeStampMs()
        let attributes = [EventAttributeName.timestampMs: currentTimeMs]
        eventReporter?.report(event: SDKEvent(meetingHistoryEventName: historyEventName,
                                              eventAttributes: attributes))

        meetingStatsCollector.addMeetingHistoryEvent(historyEventName: historyEventName,
                                                     timestampMs: currentTimeMs)
    }

    public func addEventAnalyticsObserver(observer: EventAnalyticsObserver) {
        eventAnalyticObservers.add(observer)
    }

    public func removeEventAnalyticsObserver(observer: EventAnalyticsObserver) {
        eventAnalyticObservers.remove(observer)
    }

    public func getCommonEventAttributes() -> [AnyHashable: Any] {
        return EventAttributeUtils.getCommonAttributes(meetingSessionConfig: meetingSessionConfig)
    }
}


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

import Foundation

/// `EventAnalyticsController` keeps track of events and notifies `EventAnalyticsObserver`.
/// An event describes the success and failure conditions for the meeting session.
@objc public protocol EventAnalyticsController {
    func publishEvent(name: EventName)

    /// Publish an event with updated `EventAttributes`
    ///
    /// - Parameters:
    ///   - name: Name of event to publish
    ///   - attributes: Attributes `EventAttributes` for that meeting event
    func publishEvent(name: EventName, attributes: [AnyHashable: Any])

    /// Push `MeetingHistoryEventName` to internal `MeetingStatsCollector` states to later pass to builders
    ///
    /// - Parameter historyEventName: History state to put in the meeting history
    func pushHistory(historyEventName: MeetingHistoryEventName)

    /// Subscribes to meeting event related data with an observer
    /// - Parameter observer: An observer to add to start receiving meeting events
    func addEventAnalyticsObserver(observer: EventAnalyticsObserver)

    /// Unsubscribes from meeting event by removing the specified observer
    /// - Parameter observer: An observer to remove to stop receiving meeting events
    func removeEventAnalyticsObserver(observer: EventAnalyticsObserver)

    /// Retrieve meeting history.
    func getMeetingHistory() -> [MeetingHistoryEvent]

    /// Retrieve common attributes, including deviceName, osName, and more.
    func getCommonEventAttributes() -> [AnyHashable: Any]
}


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

import Foundation

/// `EventAnalyticsFacade` exposes event analytics related function to builders
@objc public protocol EventAnalyticsFacade {
    /// Subscribes to meeting event related data with an observer
    /// - Parameter observer: An observer to add to start receiving meeting events
    func addEventAnalyticsObserver(observer: EventAnalyticsObserver)

    /// Unsubscribes from meeting event by removing the specified observer
    /// - Parameter observer: An observer to remove to stop receiving meeting events
    func removeEventAnalyticsObserver(observer: EventAnalyticsObserver)

    /// Retrieve meeting history.
    func getMeetingHistory() -> [MeetingHistoryEvent]

    /// Retrieve common attributes, including deviceName, osName, and more.
    func getCommonEventAttributes() -> [AnyHashable: Any]
}


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

import Foundation

/// `EventAnalyticsObserver` handles events regarding to analytics.
@objc public protocol EventAnalyticsObserver {
    /// Called when specific events occur during the meeting and includes attributes of the event.
    /// This can be used to create analytics around meeting metric.
    /// - Parameters:
    ///   - name: name of the event
    ///   - attributes: attributes of the event
    func eventDidReceive(name: EventName, attributes: [AnyHashable: Any])
}


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

import Foundation

/// EventAttributeName describes key of attributes that are passed in `EventAnalyticsObserver.eventDidReceive`
@objc public enum EventAttributeName: Int, CustomStringConvertible, CaseIterable {
    /// Name of device = Manufacturer of Device + Device Model
    case deviceName
    /// Manufacturer of Device
    case deviceManufacturer
    /// Model of Device
    case deviceModel
    /// Operating system name, which is "iOS"
    case osName
    /// Operating system version
    case osVersion
    /// Name of SDK, which is "amazon-chime-sdk-ios"
    case sdkName
    /// Version of SDK
    case sdkVersion
    /// Version of media SDK
    case mediaSdkVersion
    /// Timestamp of event occurrence
    case timestampMs
    /// AttendeeId
    case attendeeId
    /// External Meeting Id
    case externalMeetingId
    /// External Attendee Id
    case externalUserId
    /// Meeting Id
    case meetingId
    /// History of the meeting events in chronological order
    case meetingHistory
    // Followings are related to AudioVideo Event Attributes - meetingStartRequested, meetingStartSucceeded, meetingStartFailed, meetingEnded, meetingFailed

    /// Maximum number video tile shared during the meeting, including self video tile
    case maxVideoTileCount
    /// Duration of the meeting start process
    case meetingStartDurationMs
    /// Duration of the meeting
    case meetingDurationMs
    /// Error message of the meeting
    case meetingErrorMessage
    /// Meeting Status `MeetingSessionStatus`
    case meetingStatus
    /// The number of poor connection count during the meeting from start to end
    case poorConnectionCount
    /// The number of meeting retry connection count during the meeting from start to end
    case retryCount
    // Followings are related to Device Event Attributes - videoInputFailed

    /// The error of video input selection such as starting camera
    case videoInputError

    public var description: String {
        switch self {
        case .deviceName:
            return "deviceName"
        case .deviceManufacturer:
            return "deviceManufacturer"
        case .deviceModel:
            return "deviceModel"
        case .osName:
            return "osName"
        case .osVersion:
            return "osVersion"
        case .sdkName:
            return "sdkName"
        case .sdkVersion:
            return "sdkVersion"
        case .timestampMs:
            return "timestampMs"
        case .mediaSdkVersion:
            return "mediaSdkVersion"
        case .attendeeId:
            return "attendeeId"
        case .externalMeetingId:
            return "externalMeetingId"
        case .externalUserId:
            return "externalUserId"
        case .meetingId:
            return "meetingId"
        case .meetingHistory:
            return "meetingHistory"
        case .maxVideoTileCount:
            return "maxVideoTileCount"
        case .meetingStartDurationMs:
            return "meetingStartDurationMs"
        case .meetingDurationMs:
            return "meetingDurationMs"
        case .meetingErrorMessage:
            return "meetingErrorMessage"
        case .meetingStatus:
            return "meetingStatus"
        case .poorConnectionCount:
            return "poorConnectionCount"
        case .retryCount:
            return "retryCount"
        case .videoInputError:
            return "videoInputError"
        default:
            return ""
        }
    }
}


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

import Foundation

/// `EventName` represent some major event that could help builders to analyze the data
@objc public enum EventName: Int, CaseIterable, CustomStringConvertible {
    /// The camera selection failed.
    case videoInputFailed
    /// The meeting will start.
    case meetingStartRequested
    /// The meeting started.
    case meetingStartSucceeded
    /// The meeting failed to start.
    case meetingStartFailed
    /// The meeting ended with failure
    case meetingFailed
    /// The meeting ended.
    case meetingEnded
    // unknown
    case unknown

    public var description: String {
        switch self {
        case .videoInputFailed:
            return "videoInputFailed"
        case .meetingStartRequested:
            return "meetingStartRequested"
        case .meetingStartSucceeded:
            return "meetingStartSucceeded"
        case .meetingStartFailed:
            return "meetingStartFailed"
        case .meetingFailed:
            return "meetingFailed"
        case .meetingEnded:
            return "meetingEnded"
        case .unknown:
            return "unknown"
        }
    }

    static func toEventName(name: String) -> EventName {
        switch name {
        case "videoInputFailed":
            return .videoInputFailed
        case "meetingStartRequested":
            return .meetingStartRequested
        case "meetingStartSucceeded":
            return .meetingStartSucceeded
        case "meetingStartFailed":
            return .meetingStartFailed
        case "meetingFailed":
            return .meetingFailed
        case "meetingEnded":
            return .meetingEnded
        default:
            return .unknown
        }
    }
}


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

import Foundation

@objcMembers public class MeetingHistoryEvent: NSObject {
    public let meetingHistoryEventName: MeetingHistoryEventName
    public let timestampMs: Int64

    public init(meetingHistoryEventName: MeetingHistoryEventName, timestampMs: Int64) {
        self.meetingHistoryEventName = meetingHistoryEventName
        self.timestampMs = timestampMs
    }

    public override var description: String {
        return "\(String(describing: meetingHistoryEventName)): \(timestampMs)"
    }
}


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

import Foundation

/// `MeetingHistoryEventName` is a meeting history state which are important events to note in the history.
/// Thus, this also includes events in `EventName`
@objc public enum MeetingHistoryEventName: Int, CaseIterable, CustomStringConvertible {

    /// The microphone was selected.
    case audioInputSelected
    /// The camera was selected.
    case videoInputSelected
    /// The camera selection failed.
    case videoInputFailed
    /// The meeting failed to start.
    case meetingStartFailed
    /// The meeting will start.
    case meetingStartRequested
    /// The meeting started.
    case meetingStartSucceeded
    /// The meeting ended.
    case meetingEnded
    /// The meeting failed.
    case meetingFailed
    /// The meeting reconnected.
    case meetingReconnected
    /// unknown
    case unknown

    public var description: String {
        switch self {
        case .audioInputSelected:
            return "audioInputSelected"
        case .videoInputSelected:
            return "videoInputSelected"
        case .videoInputFailed:
            return "videoInputFailed"
        case .meetingStartFailed:
            return "meetingStartFailed"
        case .meetingStartRequested:
            return "meetingStartRequested"
        case .meetingStartSucceeded:
            return "meetingStartSucceeded"
        case .meetingEnded:
            return "meetingEnded"
        case .meetingFailed:
            return "meetingFailed"
        case .meetingReconnected:
            return "meetingReconnected"
        case .unknown:
            return "unknown"
        }
    }
}


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

import Foundation

/// `SDKEvent` defines event that composes of name of event and attribute to describe the event
@objcMembers public class SDKEvent: NSObject {
    public let name: String
    public let eventAttributes: [AnyHashable: Any]

    public init(meetingHistoryEventName: MeetingHistoryEventName, eventAttributes: [AnyHashable: Any]) {
        self.name = String(describing: meetingHistoryEventName)
        self.eventAttributes = eventAttributes
    }

    public init(eventName: EventName, eventAttributes: [AnyHashable: Any]) {
        self.name = String(describing: eventName)
        self.eventAttributes = eventAttributes
    }
}


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

import Foundation

/// `AudioVideoConfiguration` represents the configuration to be used for audio and video during a meeting session.
@objcMembers public class AudioVideoConfiguration: NSObject {
    public let audioMode: AudioMode
    public let callKitEnabled: Bool

    convenience override public init() {
        self.init(audioMode: .stereo48K, callKitEnabled: false)
    }

    convenience public init(audioMode: AudioMode) {
        self.init(audioMode: audioMode, callKitEnabled: false)
    }

    convenience public init(callKitEnabled: Bool) {
        self.init(audioMode: .stereo48K, callKitEnabled: callKitEnabled)
    }

    public init(audioMode: AudioMode, callKitEnabled: Bool) {
        self.audioMode = audioMode
        self.callKitEnabled = callKitEnabled
    }
}


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

import Foundation

/// `AudioVideoControllerFacade` manages the signaling and peer connections.
@objc public protocol AudioVideoControllerFacade {
    var configuration: MeetingSessionConfiguration { get }
    var logger: Logger { get }

    /// Start AudioVideo Controller
    ///
    /// - Parameter audioVideoConfiguration: The configuration used for Audio & Video
    /// - Throws: `PermissionError.audioPermissionError` if `RecordPermission` is not given
    /// - Throws: `MediaError.audioFailedToStart` if audio client failed to start
    /// - Throws: `MediaError.illegalState` if audio client is already started before calling start()
    func start(audioVideoConfiguration: AudioVideoConfiguration) throws

    /// Start AudioVideo Controller
    ///
    /// - Parameter callKitEnabled: A Bool value to indicate whether the VoIP call to start has CallKit integration.
    /// This parameter is used to determine how audio session interruptions should be handled,
    /// in scenarios such as receving another phone call during the VoIP call.
    /// - Throws: `PermissionError.audioPermissionError` if `RecordPermission` is not given
    /// - Throws: `MediaError.audioFailedToStart` if audio client failed to start
    /// - Throws: `MediaError.illegalState` if audio client is already started before calling start()
    func start(callKitEnabled: Bool) throws

    /// Start AudioVideo Controller
    ///
    /// - Throws: `PermissionError.audioPermissionError` if `RecordPermission` is not given
    /// - Throws: `MediaError.audioFailedToStart` if audio client failed to start
    /// - Throws: `MediaError.illegalState` if audio client is already started before calling start()
    func start() throws

    /// Stop AudioVideo Controller. This will exit the meeting
    func stop()

    /// Start local video and begin transmitting frames from an internally held `DefaultCameraCaptureSource`.
    /// `stopLocalVideo` will stop the internal capture source if being used.
    ///
    /// Calling this after passing in a custom `VideoSource` will replace it with the internal capture source.
    ///
    /// This function will only have effect if `start` has already been called
    ///
    /// - Throws: `PermissionError.videoPermissionError` if video permission of `AVCaptureDevice` is not granted
    func startLocalVideo() throws

    /// Start local video with configurations and begin transmitting frames from an internally held `DefaultCameraCaptureSource`.
    /// `stopLocalVideo` will stop the internal capture source if being used.
    ///
    /// Calling this after passing in a custom `VideoSource` will replace it with the internal capture source.
    ///
    /// This function will only have effect if `start` has already been called
    /// If maxBitRateKbps is not set, it will be self adjusted depending on number of users and videos in the meeting
    ///
    /// - Parameter config: configurations of emitted video stream, e.g. simulcast, maxBitRateKbps
    /// - Throws: `PermissionError.videoPermissionError` if video permission of `AVCaptureDevice` is not granted
    func startLocalVideo(config: LocalVideoConfiguration) throws

    /// Start local video with a provided custom `VideoSource` which can be used to provide custom
    /// `VideoFrame`s to be transmitted to remote clients. This will call `VideoSource.addVideoSink`
    /// on the provided source.
    ///
    /// Calling this function repeatedly will replace the previous `VideoSource` as the one being
    /// transmitted. It will also stop and replace the internal capture source if `startLocalVideo`
    /// was previously called with no arguments.
    ///
    /// This function will only have effect if `start` has already been called
    ///
    /// - Parameter source: The source of video frames to be sent to other clients
    func startLocalVideo(source: VideoSource)

    /// Start local video with configurations and a provided custom `VideoSource` which can be used to provide custom
    /// `VideoFrame`s to be transmitted to remote clients. This will call `VideoSource.addVideoSink`
    /// on the provided source.
    ///
    /// Calling this function repeatedly will replace the previous `VideoSource` as the one being
    /// transmitted. It will also stop and replace the internal capture source if `startLocalVideo`
    /// was previously called with no arguments.
    ///
    /// This function will only have effect if `start` has already been called
    /// If maxBitRateKbps is not set, it will be self adjusted depending on number of users and videos in the meeting
    ///
    /// - Parameter source: The source of video frames to be sent to other clients
    /// - Parameter config: Configurations of emitted video stream, e.g. simulcast, maxBitRateKbps
    func startLocalVideo(source: VideoSource, config: LocalVideoConfiguration)

    /// Stops sending video for local attendee. This will additionally stop the internal capture source if being used.
    /// If using a custom video source, this will call `VideoSource.removeVideoSink` on the previously provided source.
    func stopLocalVideo()

    /// Enable remote video to start receiving streams
    func startRemoteVideo()

    /// Disable remote video to stop receiving streams
    func stopRemoteVideo()

    /// Subscribe to audio, video, and connection events with an `AudioVideoObserver`.
    ///
    /// - Parameter observer: The observer to subscribe to events with
    func addAudioVideoObserver(observer: AudioVideoObserver)

    /// Unsubscribes from audio, video, and connection events by removing specified `AudioVideoObserver`.
    ///
    /// - Parameter observer: The observer to unsubscribe from events with
    func removeAudioVideoObserver(observer: AudioVideoObserver)

    /// Subscribe to metrics events with an `MetricsObserver`.
    ///
    /// - Parameter observer: The observer to subscribe to events with
    func addMetricsObserver(observer: MetricsObserver)

    /// Unsubscribes from metrics events by removing specified `MetricsObserver`.
    ///
    /// - Parameter observer: The observer to unsubscribe from events with
    func removeMetricsObserver(observer: MetricsObserver)

    /// Add, update, or remove subscriptions to remote video sources provided via `remoteVideoSourcesDidBecomeAvailable`.
    ///
    /// This function requires using the `RemoteVideoSource` provided by `remoteVideoSourcesDidBecomeAvailable`, otherwise it will not update properly.
    /// This is what allows to use the `RemoteVideoSource` objects as keys in a map.
    ///
    /// Including a `RemoteVideoSource` in `addedOrUpdated` which was not previously provided will result in the negotiation of media flow for that source. After negotiation has
    /// completed,`videoTileDidAdd` on the tile controller will be called with the `TileState` of the source, and applications
    /// can render the video via 'bindVideoTile'. Reincluding a `RemoteVideoSource` can be done to update the provided `VideoSubscriptionConfiguration`,
    /// but it is not necessary to continue receiving frames.
    ///
    /// Including a `RemoteVideoSource` in `removed` will stop the flow video from that source, and lead to a `videoTileDidRemove` call on the
    /// tile controller to indicate to the application that the tile should be unbound. To restart the flow of media, the source should be re-added by
    /// including in `addedOrUpdated`. Note that videos no longer available in a meeting (i.e. listed in
    /// `remoteVideoSourcesDidBecomeUnavailable` do not need to be removed, as they will be automatically unsubscribed from.
    ///
    /// Note that before this function is called for the first time, the client will automatically subscribe to all video sources.
    /// However this behavior will cease upon first call (e.g. if there are 10 videos in the meeting, the controller will subscribe to all 10, however if
    /// `updateVideoSourceSubscriptions` is called with a single video in `addedOrUpdated`, the client will unsubscribe from the other 9.
    /// This automatic subscription behavior may be removed in future major version updates, builders should avoid relying on the logic
    /// and instead explicitly call `updateVideoSourceSubscriptions` with the sources they want to receive.
    ///
    /// - Parameter addedOrUpdated: Dictionary of remote video sources to configurations to add or update
    /// - Parameter removed: Array of remote video sources to remove
    func updateVideoSourceSubscriptions(addedOrUpdated: Dictionary<RemoteVideoSource, VideoSubscriptionConfiguration>, removed: Array<RemoteVideoSource>)

    /// Allows an attendee in a Replica meeting to immediately transition to a Primary meeting attendee
    /// without need for reconnection.
    ///
    ///  `PrimaryMeetingPromotionObserver.didPromoteToPrimaryMeeting` will be called exactly once on `observer` for each call. If
    ///  the promotion is successful,  `PrimaryMeetingPromotionObserver.didDemoteFromPrimaryMeeting` will be called exactly once
    ///  if/when the attendee is demoted. See the observer documentation for possible status codes.
    ///
    /// Application code may also receive a callback on `AudioVideoObserver.videoSessionDidStartWithStatus` without
    /// `MeetingSessionStatusCode.VideoAtCapacityViewOnly` to indicate they can begin to share video.
    ///
    /// `chime::DeleteAttendee` on the Primary meeting attendee will result in `PrimaryMeetingPromotionObserver.didDemoteFromPrimaryMeeting`
    /// to indicate the attendee is no longer able to share.
    ///
    /// Any disconnection will trigger an automatic demotion to avoid unexpected or unwanted promotion state on reconnection.
    /// This will also call `PrimaryMeetingPromotionObserver.didDemoteFromPrimaryMeeting`;  if the attendee still needs to be
    /// an interactive participant in the Primary meeting, `promoteToPrimaryMeeting` should be called again with the same credentials.
    ///
    /// Note that given the asynchronous nature of this function, this should not be called a second time before
    /// `PrimaryMeetingPromotionObserver.didPromoteToPrimaryMeeting` is called for the first time. Doing so may result in unexpected
    /// behavior.
    ///
    /// - Parameter credentials: The credentials for the primary meeting.  This needs to be obtained out of band.
    /// - Parameter observer: Will be called with a session status for the request and possible demotion. See possible options above.
    func promoteToPrimaryMeeting(credentials: MeetingSessionCredentials, observer: PrimaryMeetingPromotionObserver)

    /// Remove the promoted attendee from the Primary meeting. This client will stop sharing audio, video, and data messages.
    /// This will revert the end-user to precisely the state they were before a call to `promoteToPrimaryMeeting`
    ///
    /// This will have no effect if there was no previous successful call to `promoteToPrimaryMeeting`. This
    /// may result in `PrimaryMeetingPromotionObserver.didPromoteToPrimaryMeeting` but there is no need to wait for that callback
    /// to revert UX, etc.
    func demoteFromPrimaryMeeting()
}


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

import Foundation

@objc public protocol AudioVideoFacade: AudioVideoControllerFacade, RealtimeControllerFacade,
    DeviceController,
    VideoTileControllerFacade,
    ActiveSpeakerDetectorFacade,
    ContentShareController,
    EventAnalyticsFacade {}


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

import Foundation

/// `AudioVideoObserver` handles audio/video session events.
@objc public protocol AudioVideoObserver {
    /// Called when the audio session is connecting or reconnecting.
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter reconnecting: Whether the session is reconnecting or not.
    func audioSessionDidStartConnecting(reconnecting: Bool)

    /// Called when the audio session has started.
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter reconnecting: Whether the session is reconnecting or not.
    func audioSessionDidStart(reconnecting: Bool)

    /// Called when audio session got dropped due to poor network conditions.
    /// There will be an automatic attempt of reconnecting it.
    /// If the reconnection is successful, `onAudioSessionStarted` will be called with value of reconnecting as true
    ///
    /// Note: this callback will be called on main thread.
    func audioSessionDidDrop()

    /// Called when the audio session has stopped with the reason provided in the status.
    /// This callback implies that audio client has stopped permanently for this session
    /// and there will be no attempt of reconnecting it.
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter sessionStatus: The reason why the session has stopped.
    func audioSessionDidStopWithStatus(sessionStatus: MeetingSessionStatus)

    /// Called when the audio reconnection is canceled.
    ///
    /// Note: this callback will be called on main thread.
    func audioSessionDidCancelReconnect()

    /// Called when the connection health is recovered.
    ///
    /// Note: this callback will be called on main thread.
    func connectionDidRecover()

    /// Called when connection is becoming poor.
    ///
    /// Note: this callback will be called on main thread.
    func connectionDidBecomePoor()

    /// Called when the video session is connecting or reconnecting.
    ///
    /// Note: this callback will be called on main thread.
    func videoSessionDidStartConnecting()

    /// Called when the video session has started.
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter sessionStatus: The status of meeting session
    func videoSessionDidStartWithStatus(sessionStatus: MeetingSessionStatus)

    /// Called when the video session has stopped from a started state with the reason provided in the status.
    ///
    /// Note: this callback will be called on main thread.
    ///
    /// - Parameter sessionStatus: The reason why the session has stopped.
    func videoSessionDidStopWithStatus(sessionStatus: MeetingSessionStatus)

    /// Called on the main thread when video sources become available.
    ///
    /// Video sources can be explicitly subscribed to through `updateVideoSourceSubscriptions`.
    /// These should be stored and used when `updateVideoSourceSubscriptions` is called (i.e. you cannot use `RemoteVideoSource` objects created manually).
    /// See `updateVideoSourceSubscriptions` for more information.
    /// See note in `updateVideoSourceSubscriptions` documentation for information on
    /// subscription behavior if `updateVideoSourceSubscriptions` is never called.
    ///
    /// - Parameter sources: Array of remote video sources that are available
    func remoteVideoSourcesDidBecomeAvailable(sources: [RemoteVideoSource])

    /// Called on the main thread when video sources become unavailable.
    ///
    /// Note that these sources do not need to be removed via `updateVideoSourceSubscriptions`,
    /// as they will be automatically unsubscribed from.
    ///
    /// - Parameter sources: Array of video sources that are unavailable
    func remoteVideoSourcesDidBecomeUnavailable(sources: [RemoteVideoSource])

    /// Called on the main thread when video capacity status is updated.
    ///
    /// - Parameter available: True if camera send is available (due to video capacity status), False if not.
    func cameraSendAvailabilityDidChange(available: Bool)
}