//
//  ViewController.swift
//  ABC Bluetooth Demo
//
//  Created by AB Circle on 14/5/2020.
//  Copyright © 2020-2024 AB Circle. All rights reserved.
//

import UIKit
/// Import of the AB Circle Smart Card I/O Bluetooth iOS framework
import ABCSmartCardIO

/// Main view controller; controls the main screen
/// Also acts as delegate for the TextInputViewController and OptionsViewControllerProtocol
class ViewController: UIViewController, TextInputViewControllerProtocol, OptionsViewControllerProtocol, ReaderDiscoveredCallback {
   
    /// Enumerator holding the possible UI States
    private enum UIState {
        /// Initial (default) UI state on application startup
        case stateAppStart
        /// UI state when terminals are found
        case stateTerminalsFound
        /// UI state when connected to a terminal
        case stateConnected
    }
    
    /// Main instance of the AB Circle Terminal Factory.
    private let abcTerminalFactory = ABCircleTerminalFactory.getDefault()
    /// Create an instance for Card Terminal monitor
    private let abcCardTerminalMonitor = ABCCardTerminalMonitor()
    /// Global array of Card Terminals
    private var terminals  = [CardTerminal]()
    /// Current selected Card Terminal
    private weak var currentTerminal: CardTerminal?
    /// Current active Card
    private var card: Card?
    /// Flag indicating if the CardTerminal monitor has started or not
    private var monitorStarted: Bool = false
    /// Flag indicating if connected to a Card or not
    private var connected: Bool = false
    /// Options storage
    private let options = Options()
    /// Timer (used for automatically updating the Card Terminal list)
    private var timer: Timer? = nil
    /// Custom Card Terminal UIPickerView with Toolbar
    private let terminalPicker = ToolbarUIPickerView()
    
    /// Control outlet for custom key UIButton
    @IBOutlet weak var customKeyButton: UIButton!
    /// Control outlet for terminal UILabel
    @IBOutlet weak var labelTerminal: UILabel!
    /// Control outlet for terminal NoCaretUITextField
    @IBOutlet weak var selectedTerminal: NoCaretUITextField!
    /// Control outlet for Discover UIButton
    @IBOutlet weak var buttonDiscover: UIButton!
    /// Control outlet for Refresh UIButton
    @IBOutlet weak var buttonRefresh: UIButton!
    /// Control outlet for Monitor UISwitch
    @IBOutlet weak var monitorSwitch: UISwitch!
    /// Control outlet for protocol UILabel
    @IBOutlet weak var labelProtocol: UILabel!
    /// Control outlet for protocol T=0 UISwitch
    @IBOutlet weak var protocolT0: UISwitch!
    /// Control outlet for protocol T=0 label
    @IBOutlet weak var labelT0: UILabel!
    /// Control outlet for protocol T=1 UISwitch
    @IBOutlet weak var protocolT1: UISwitch!
    /// Control outlet for protocol T=1 label
    @IBOutlet weak var labelT1: UILabel!
    /// Control outlet for Mode UILabel
    @IBOutlet weak var labelMode: UILabel!
    /// Control outlet for mode selection UISegmentedControl
    @IBOutlet weak var modeSelector: UISegmentedControl!
    /// Control outlet for connect UIButton
    @IBOutlet weak var buttonConnect: UIButton!
    /// Control outlet for log UIView
    @IBOutlet weak var viewLog: UIView!
    /// Control outlet for log UIScrollView
    @IBOutlet weak var scrollViewLog: UIScrollView!
    /// Control outlet for log NoCaretUITextView
    @IBOutlet weak var textViewLog: NoCaretUITextView!
    /// Control outlet for apdu label UILabel
    @IBOutlet weak var apduLabel: UILabel!
    /// Control outlet for apdu NoCaretUITextField
    @IBOutlet weak var apduTextField: NoCaretUITextField!
    /// Control outlet for audo send UIButton
    @IBOutlet weak var apduSendButton: UIButton!
    /// Control outlet for wait for change UIButton
    @IBOutlet weak var waitForChangeButton: UIButton!
    /// Control outlet for Mode escape command UILabel
    @IBOutlet weak var escapeCommandLabel: UILabel!
    /// Control outlet for escape command NoCaretUITextField
    @IBOutlet weak var escapeCommandTextField: NoCaretUITextField!
    /// Control outlet for escape command send UIButton
    @IBOutlet weak var escapeCommandSendButton: UIButton!
    
    /// Called after the controller's view is loaded into memory.
    /// Initialises the local variables, the controls and starts the terminal list updater
    override func viewDidLoad() {
        super.viewDidLoad()
        
        /// Initialise local variables
        currentTerminal = nil
        card            = nil
        connected       = false
        /// Initialise options
        options.useDefaultKey           = true
        options.customKeyString         = ""
        options.nameFilter              = ""
        options.rssiFilter              = 0
        options.monitorRefreshRate      = 1
        options.monitorBatteryThreshold = 20
        /// Initialise terminal picker view
        selectedTerminal.inputView          = terminalPicker
        selectedTerminal.inputAccessoryView = terminalPicker.toolbar
        terminalPicker.delegate   = self
        terminalPicker.dataSource = self
        terminalPicker.toolbarDelegate = self
        terminalPicker.reloadAllComponents()
        /// Remove copy/paste buttons from toolbar on terminal picker
        let item : UITextInputAssistantItem = selectedTerminal.inputAssistantItem
        item.leadingBarButtonGroups = []
        item.trailingBarButtonGroups = []

        /// Initialise callbacks for showing APDU / Escape command input popup windows
        apduTextField.addTarget(self, action: #selector(showApduInputView), for: .touchDown)
        escapeCommandTextField.addTarget(self, action: #selector(showEscapeCommandInputView), for: .touchDown)

        /// Enable multi line on button wth long label
        waitForChangeButton.titleLabel?.numberOfLines = 0
        
        /// Initialise log text view control
        initialiseTextView(textView: textViewLog)
       
        /// Start queue that updates the terminal list
        startReaderListUpdater()

        /// Set this class as a delegate for CardMonitor
        abcCardTerminalMonitor.delegate = self

        /// Initialise UI fields state
        enableUIFields(state: .stateAppStart)
        
        /// Log startup
        addLogLine(line: "Application Start")
        addLogLine(line: "Using:")
        addLogLine(format: "%@ %@", arguments: FrameworkInformation.displayName, FrameworkInformation.shortVersionString)
        addLogLine(format: "%@",    arguments: FrameworkInformation.copyright)

        /// Start monitoring of CardTerminals
        startMonitor()
    }
    
    /// Closes the APDU / Escape Command popup and retrieves its content
    ///
    /// - Parameters:
    ///   - textViewContent: Textview content from the text view on the popup
    ///   - isApdu: Flag indicating APDU popup or Escape Command popup
    func closePopup(textViewContent: String, isApdu: Bool) {
        if (isApdu) {
            apduTextField.text = textViewContent
        }
        else {
            escapeCommandTextField.text = textViewContent
        }
    }
    
    /// Action handler for opening the Options view
    ///
    /// - Parameter sender: sender (UIButton)
    @IBAction func onShowOptionsView(_ sender: UIButton) {
        self.performSegue(withIdentifier: "ShowOptionsView", sender: self)
    }
    
    /// Overrride function called when the Custom Key view is preparing (just before opening).
    /// Used here to set the initial values of the controls on the Custom Key view.
    ///
    /// - Parameters:
    ///   - segue: Segue being handled
    ///   - sender: sender
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "ShowOptionsView" {
            let view = segue.destination as! OptionsViewController

            view.setInitialValues(parentViewController: self, options: options)
        }
    }
    
    /// Call back function handling the closing of the Options view.
    /// Used for either clearing or setting of the Custom Encryption Key used by the AB Circle Terminal Factory.
    /// Note: the custom key entered is always saved in the  "customKey" class variable, so it can be shown and
    /// activated at any time (usage is determined by the "useDefaultKey" flag). Also used for setting the scanBluetooth
    /// name and RSSI filter values, CardMonitor values and disabling or enabling of the ACK protocol for terminals that
    /// support it (terminals with fw 109.00 or higher).
    ///
    /// - Parameters:
    ///   - options: Options as set in the Options view.
    func onCloseOptionsWindow(options: Options) {
        self.options.set(newOptions: options)
        addLogLine(line: "Options:")
        if (options.useDefaultKey == true) {
            abcTerminalFactory.clearCustomEncryptionKey()
            addLogLine(line: "Using default key")
        }
        else {
            addLogLine(line: "Using custom key")
            addLogLine(format: "Custom key: %s", arguments: options.customKeyString)
            do {
                try abcTerminalFactory.setCustomEncryptionKey(customKeyString: options.customKeyString)
            }
            catch {
                addLogLine(line: "Invalid custom key entered; key should be 16 bytes")
            }
        }
        if options.nameFilter.isEmpty {
            addLogLine(line: "Name Filter: none")
        }
        else {
            addLogLine(format: "Name Filter: %@", arguments: options.nameFilter)
        }
        if options.rssiFilter == 0 {
            addLogLine(line: "RSSI Filter: none")
        }
        else {
            addLogLine(format: "RSSI Filter (-dbM): %d", arguments: options.rssiFilter)
        }
        do {
            let _ = try abcTerminalFactory.setBluetoothRssiFilter(rssiValue: -options.rssiFilter)
        }
        catch {
            addLogLine(line: "Error setting RSSI filter")
        }
        if (options.monitorRefreshRate == 0) {
            if abcCardTerminalMonitor.isMonitorRunning {
                monitorSwitch.setOn(false, animated: false)
                stopMonitor()
            }
            addLogLine(format: "CardTerminal monitor: Off", arguments: options.monitorRefreshRate)
        }
        else {
            addLogLine(format: "CardTerminal monitor refresh rate: %d second(s)", arguments: options.monitorRefreshRate)
        }
        abcCardTerminalMonitor.setBatteryThreshold(batteryThreshold: options.monitorBatteryThreshold)
        if (options.monitorBatteryThreshold == 0) {
            addLogLine(format: "CardTerminal monitor battery monitoring: Off")
        }
        else {
            addLogLine(format: "CardTerminal monitor battery threshold: %d%%", arguments: options.monitorBatteryThreshold)
        }
        abcCardTerminalMonitor.setMonitorRefreshRate(refreshRateInSeconds: options.monitorRefreshRate)
        if (options.ackEnabled) {
            abcTerminalFactory.ackProtocolEnabledForReaders = true
            addLogLine(line: "ACK Protocol: Enabled (for newly connected readers with fw 109.00 or higher)")
        }
        else {
            abcTerminalFactory.ackProtocolEnabledForReaders = false
            addLogLine(line: "ACK Protocol: Disabled (for newly connected readers)")
        }
    }
    
    /// Callback handler for showing the APDU input view
    @objc private func showApduInputView() {
        TextInputViewController.showPopup(parentVC: self, textViewContent: apduTextField.text ?? "", isApdu: true)
    }

    /// Callback handler for showing the Escape Command input view
    @objc private func showEscapeCommandInputView() {
        TextInputViewController.showPopup(parentVC: self, textViewContent: escapeCommandTextField.text ?? "", isApdu: false)
    }
    
    /// Callback handler for handling connection mode change.
    /// Enables or disables the protocol fields.
    ///
    /// - Parameter sender: sender
    @IBAction func onModeChanged(_ sender: UISegmentedControl) {
        if selectedModeIsDirect() {
            enableProtocolFields(enabled: false)
        }
        else {
            enableProtocolFields(enabled: true)
        }
    }
    
    /// Callback handler for handling the Discover button
    ///
    /// - Parameter sender: sender
    @IBAction func onDiscoverReaders(_ sender: UIButton) {
        addLogLine(line: "Discovering Bluetooth Devices for 10 seconds...")
        do {
            try abcTerminalFactory.startReaderDiscovery(durationInSeconds: 10, caller: self)
        }
        catch {
            let status = error as! Status.StatusCode
            
            addLogLine(format: "Discovery failed with status %@, %@", arguments: status.name, abcTerminalFactory.statusDescription(status: status))
        }
    }
    
    /// Callback handler, called when a reader is discovered (part of the ReaderDiscoveredCallback protocol)
    ///
    /// - Parameter reader: Discovered reader
    func onReaderDiscovered(reader: ABCSmartCardIO.DiscoveredReader) {
        addLogLine(format: "Discovered: %@ : %@", arguments: reader.getName, reader.getUUID)
    }
    
    /// Callback handler for handling the Disconnect button
    /// Disconnect the current selected terminal.
    ///
    /// - Parameter sender: sender
    @IBAction func onDisconnectTerminal(_ sender: UIButton) {
        if let terminal = currentTerminal {
            addLogLine(format: "Disconnecting %@", arguments: terminal.getName())
            abcTerminalFactory.disconnect(cardTerminal: terminal)
        }
    }
    
    /// Callback handler for handling the Battery button
    /// Retrieves and shows the battery level of the current selected terminal.
    ///
    /// - Parameter sender: sender
    @IBAction func onGetBatteryLevel(_ sender: UIButton) {
        if let terminal = currentTerminal {
            do {
                let level = try terminal.getBatteryLevel()
                if level == 0xFF {
                    addLogLine(line: "Battery charging...")
                }
                else {
                    addLogLine(format: "Battery level: %02d%%", arguments: level)
                }
            }
            catch {
                let status = error as! Status.StatusCode
                
                addLogLine(format: "Get battery level failed with status %@, %@", arguments: status.name, abcTerminalFactory.statusDescription(status: status))
            }
        }
        else {
            addLogLine(line: "Please select a terminal first...")
        }
    }
    
    /// Callback handler for handling the Version button
    /// Retrieves and shows the firmware version of the current selected terminal.
    ///
    /// - Parameter sender: sender
    @IBAction func onGetFirmwareVersion(_ sender: UIButton) {
        if let terminal = currentTerminal {
            do {
                let version = try terminal.getFirmwareVersion()
                
                addLogLine(format: "Firmware version: %@", arguments: version)
            }
            catch {
                let status = error as! Status.StatusCode
                
                addLogLine(format: "Get firmware version failed with status %@, %@", arguments: status.name, abcTerminalFactory.statusDescription(status: status))
            }
        }
        else {
            addLogLine(line: "Please select a terminal first...")
        }
    }
    
    /// Callback handler for handling the Refresh button.
    /// Calls the ABC Terminal Factory scanBluetooth() function for scanning and finding AB Circle bluetooth readers.
    ///
    /// - Parameter sender: sender
    @IBAction func onRefresh(_ sender: UIButton) {
        addLogLine(line: "Scanning Bluetooth for 10 seconds...")
        do {
            try abcTerminalFactory.scanBluetooth(name: options.nameFilter)
        }
        catch {
            let status = error as! Status.StatusCode
            
            addLogLine(format: "Scan failed with status %@, %@", arguments: status.name, abcTerminalFactory.statusDescription(status: status))
        }
    }
    
    /// Enables or disables the CardTerminal monitor
    ///
    /// - Parameter sender: UISwitch
    @IBAction func onMonitorChanged(_ sender: UISwitch) {
        if monitorSwitch.isOn {
            startMonitor()
        }
        else {
            stopMonitor()
        }
    }
    
    /// Starts the CardTerminal monitor
    private func startMonitor() {
        if !abcCardTerminalMonitor.isMonitorRunning && options.monitorRefreshRate == 0 {
            options.monitorRefreshRate = 1 //make sure we really start the monitor
        }
        if options.monitorBatteryThreshold == 0 {
            addLogLine(format: "Started CardTerminal monitor (refresh rate: %d sec, battery not monitored)",
                   arguments: options.monitorRefreshRate)
        }
        else {
            addLogLine(format: "Started CardTerminal monitor (refresh rate: %d sec, battery threshold: %02d%%)",
                       arguments: options.monitorRefreshRate, options.monitorBatteryThreshold)

        }
        abcCardTerminalMonitor.startMonitoring(refreshRateInSeconds: options.monitorRefreshRate, batteryThreshold: options.monitorBatteryThreshold)
    }
    
    /// Stops the CardTerminal monitor
    private func stopMonitor() {
        addLogLine(line: "Stopped CardTerminal monitor")
        abcCardTerminalMonitor.stopMonitoring()
    }
    
    /// Starts the card terminal list updater, using a timer. Checks for changes in the card terminal list every second
    /// and updates the list if needed.
    private func startReaderListUpdater() {
        timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(listReaders), userInfo: nil, repeats: true)
    }
    
    /// Callback handler for updating the list of Card Terminals.
    /// Gets the latest list from the AB Circle Terminal Factory by calling "terminals.list()" and checks if there are any changes.
    /// If there are changes, rebuilds the terminal picker and resets the terminal TextInput box if needed.
    @objc private func listReaders() {
        DispatchQueue.global(qos: .userInteractive).async { [weak self] in
            guard let self = self else {
                return
            }
            let newTerminals: [CardTerminal] = self.abcTerminalFactory.terminals().list()

            if self.terminalsChanged(terminalList1: newTerminals, terminalList2: self.terminals) {
                self.terminals.removeAll()
                self.terminals = newTerminals
                DispatchQueue.main.async {
                    if self.terminals.count == 0 {
                        self.selectedTerminal.text = ""
                        if (self.connected) {
                            self.disconnect()
                        }
                        self.currentTerminal = nil
                        self.enableUIFields(state: .stateAppStart)
                    }
                    else {
                        self.enableUIFields(state: .stateTerminalsFound)
                    }
                    self.terminalPicker.selectRow(0, inComponent: 0, animated: false)
                    self.terminalPicker.reloadAllComponents()
                    if self.terminals.count != 0 {
                        self.selectCurrentTerminal(newTerminal: self.terminals[0])
                    }
                    else {
                        self.addLogLine(line: "No terminal selected")
                    }
                }
            }
        }
    }
    
    /// Callback function handling the Protocol T=0 switch.
    /// Enables or disables the Connect button.
    ///
    /// - Parameter sender: sender
    @IBAction func onProtocolT0ValueChanged(_ sender: UISwitch) {
        if !protocolT0.isOn && !protocolT1.isOn {
            buttonConnect.isEnabled = false
        }
        else {
            buttonConnect.isEnabled = true
        }
    }
    
    /// Callback function handling the Protocol T=1 switch.
    /// Enables or disables the Connect button.
    ///
    /// - Parameter sender: sender
    @IBAction func onProtocolT1ValueChanged(_ sender: UISwitch) {
        if !protocolT0.isOn && !protocolT1.isOn {
            buttonConnect.isEnabled = false
        }
        else {
            buttonConnect.isEnabled = true
        }
    }
    
    /// Callback function handling the Connect button.
    /// If connected, tries to connect; else disconnects.
    ///
    /// - Parameter sender: sender
    @IBAction func onConnect(_ sender: Any) {
        if connected {
            disconnect()
        }
        else {
            connect()
        }
    }
    
    /// Callback handler handling the "Send APDU" button.
    /// Sends an APDU command to a connected card (if any)
    ///
    /// - Parameter sender: sender
    @IBAction func onSendAPDU(_ sender: UIButton) {
        guard let _ = currentTerminal else {
            addLogLine(line: "No terminal selected!")
            return
        }
        guard let _ = card else {
            addLogLine(line: "No card connected!")
            return
        }
        guard let apdu = apduTextField.text else {
            addLogLine(line: "Please enter an APDU first!")
            return
        }
        if apdu.isEmpty {
            addLogLine(line: "Please enter an APDU first!")
        }
        else {
            sendAPDUThroughBasicChannel(apdu: apdu)
        }
    }

    /// Callback handler handling the "Send Escape Command" button.
    /// Sends an Escape Command to a connected card (if any)
    ///
    /// - Parameter sender: sender
    @IBAction func onSendEscapeCommand(_ sender: UIButton) {
        guard let _ = currentTerminal else {
            addLogLine(line: "No terminal selected!")
            return
        }
        guard let _ = card else {
            addLogLine(line: "No card connected!")
            return
        }
        guard let escapeCommand = escapeCommandTextField.text else {
            addLogLine(line: "Please enter a escape command first!")
            return
        }
        if escapeCommand.isEmpty {
            addLogLine(line: "Please enter a escape command first!")
        }
        else {
            sendEscapeCommand(escapeCommand: escapeCommand)
        }
    }
    
    /// Callback function for handling the "Wait for Change" button.
    /// Calls the AB Circle Terminal Factory "waitForChange" function to wait for a card change action on the current selected card terminal.
    /// If a change occured, will show the change detected by calling "terminals().list()" function twice with state filters for card insertion and card removal.
    ///
    /// - Parameter sender: sender
    @IBAction func onWaitForChangeClicked(_ sender: UIButton) {
        self.addLogLine(line: "Waiting for card change for 5 seconds...")
        DispatchQueue.global(qos: .userInitiated).async {
            if self.abcTerminalFactory.terminals().waitForChange(timeout: 5000) == true {
                self.addLogLine(line: "Card change detected!")
                
                let terminalsIn = self.abcTerminalFactory.terminals().list(state: .CARD_INSERTION)
                for terminal in terminalsIn {
                    self.addLogLine(format: "%@ - card inserted", arguments: terminal.getName())
                }
                let terminalsOut = self.abcTerminalFactory.terminals().list(state: .CARD_REMOVAL)
                for terminal in terminalsOut {
                    self.addLogLine(format: "%@ - card removed", arguments: terminal.getName())
                }

            }
            else {
                self.addLogLine(line: "No card change detected!")
            }
        }
    }
    
    /// Clears the log
    ///
    /// - Parameter sender: sender
    @IBAction func onClear(_ sender: UIButton) {
        textViewLog.text = ""
    }
    
    /// Initialises a text view control with empty text and a default border.
    ///
    /// - Parameter textView: Textview control to initialise
    private func initialiseTextView(textView: UITextView)
    {
        textView.text               = ""
        textView.layer.borderWidth  = 1.0
        textView.layer.cornerRadius = 8
        textView.layer.borderColor  =  UIColor.lightGray.cgColor
    }
    
    /// (Re-)Selects the current card terminal
    ///
    /// - Parameter newTerminal: New terminal selected on the UI
    private func selectCurrentTerminal(newTerminal: CardTerminal)
    {
        if currentTerminal?.getName() != newTerminal.getName() {
            if currentTerminal != nil {
                currentTerminal = nil
            }
            currentTerminal = newTerminal
            if let terminal = currentTerminal {
                selectedTerminal.text = terminal.getName()
                addLogLine(format: "Terminal %@ selected", arguments: terminal.getName())
            }
        }
    }
    
    /// Compares 2 CardTerminals array lists and determines if there are changes
    ///
    /// - Parameters:
    ///   - terminalList1: CardTerminal list 1
    ///   - terminalList2: CardTerminal list 2
    /// - Returns: True if changes were found, else false
    private func terminalsChanged(terminalList1: [CardTerminal], terminalList2: [CardTerminal]) -> Bool {
        return !terminalList1.elementsEqual(terminalList2, by: { $0.getName() == $1.getName() })
    }
    
    /// Sends an APDU through a card's basic channel and receives the response.
    ///
    /// - Parameter apdu: APDU in hexadecimal string form
    private func sendAPDUThroughBasicChannel(apdu: String) {
        guard let cardChannel = getBasicChannel() else {
            return
        }
        guard let commandAPDU = getCommandAPDU(apduString: apdu) else {
            return
        }
        do {
            addLogLine(format: "APDU < %@", arguments: commandAPDU.toString())
            let responseAPDU = try cardChannel.transmit(command: commandAPDU)
            addLogLine(format: "APDU > %@", arguments: responseAPDU.toString())
        }
        catch {
            addLogLine(format: "Send APDU failed with status %@, %@", arguments: cardChannel.lastStatusCode.name, cardChannel.lastStatusDescription)
        }
    }
    
    /// Gets the basic channel of a currently connected card
    ///
    /// - Returns: A CardChannel object on success, else nil
    private func getBasicChannel() -> CardChannel? {
        var channel: CardChannel? = nil
        
        guard let card = card else {
            addLogLine(line: "Send APDU failed: card not connected!")
            return nil
        }
        do {
            channel = try card.getBasicChannel()
        }
        catch {
            addLogLine(format: "Send APDU failed: could not get card basic channel, status %@, %@", arguments: card.lastStatusCode.name, card.lastStatusDescription)
        }
        return channel
    }
    
    /// Creates a CommandAPDU from the entered APDU hexadecimal string
    ///
    /// - Parameter apduString: APDU in hexadecimal string form
    /// - Returns: A CommandAPDU object on success, else nil
    private func getCommandAPDU(apduString: String) -> CommandAPDU? {
        var commandAPDU: CommandAPDU? = nil
        
        do {
            commandAPDU = try CommandAPDU(apdu: apduString)
        }
        catch {
            if let commandAPDU = commandAPDU {
                addLogLine(format: "Send APDU failed; could not initialise APDU command, status %@, %@", arguments: commandAPDU.lastStatusCode.name, commandAPDU.lastStatusDescription)
            }
            else {
                let status = error as! Status.StatusCode
                
                addLogLine(format: "Send APDU failed; could not initialise APDU command, status %@, %@", arguments: status.name, abcTerminalFactory.statusDescription(status: status))
            }
        }
        return commandAPDU
    }
    
    /// Sends an Escape command to a connected card
    ///
    /// - Parameter escapeCommand: Escape command in hexadecimal string form
    private func sendEscapeCommand(escapeCommand: String) {
        let command: Data = Helper.hexStringToData(hexString: escapeCommand)
        guard let card = card else {
            addLogLine(line: "Card not connected!")
            return
        }
        do {
            addLogLine(format: "ESC  < %@", arguments: escapeCommand)
            let response = try card.transmitControlCommand(controlCode: 3500, command: command)
            if let response = response {
                addLogLine(format: "ESC  > %@", arguments: Helper.dataToHexString(data: response))
            }
            else {
                addLogLine(line: "Send escape command success!")
            }
        }
        catch {
            addLogLine(format: "Send escape command failed with status %@, %@", arguments: card.lastStatusCode.name, card.lastStatusDescription)
        }
    }
    
    /// Connects to a card in the selected terminal
    private func connect() {
        var displayProtocol: String
        var cardProtocol: Card.ConnectionProtocol = .T0orT1
        
        guard let currentTerminal = currentTerminal else {
            addLogLine(line: "No terminal selected...")
            return
        }
        if (connected) {
            addLogLine(line: "Already connected...")
            return
        }
        if card != nil {
            card = nil
        }
        if selectedModeIsDirect() {
            cardProtocol = .Direct
        }
        else {
            if protocolT0.isOn && !protocolT1.isOn {
                cardProtocol = .T0
            }
            else if !protocolT0.isOn && protocolT1.isOn {
                cardProtocol = .T1
            }
            else if protocolT0.isOn && protocolT1.isOn {
                cardProtocol = .T0orT1
            }
        }
        displayProtocol = cardProtocol.name
        addLogLine(format: "Connect using protocol %@", arguments: displayProtocol)
        do {
            card = try currentTerminal.connect(protocolString: cardProtocol.rawValue)
            if let card = card {
                addLogLine(format: "Success! Protocol: %@; ATR: %@", arguments: card.getProtocol(), card.getATR().toString())
                connected = true
                buttonConnect.setTitle("Disconnect", for: .normal)
                enableUIFields(state: .stateConnected)
            }
        }
        catch {
            addLogLine(format: "Connect failed with status %@, %@", arguments: currentTerminal.lastStatusCode.name, currentTerminal.lastStatusDescription)
        }
    }
    
    /// Disconnects from a card (if connected)
    private func disconnect() {
        if (!connected) {
            addLogLine(line: "Not connected...")
            return
        }
        guard let card = self.card else {
            addLogLine(line: "Not connected...")
            return
        }
        do {
            try card.disconnect(reset: false)
            connected = false
            buttonConnect.setTitle("Connect", for: .normal)
            enableUIFields(state: .stateTerminalsFound)
            self.card = nil
        }
        catch {
            addLogLine(format: "Disconnect failed with status %@, %@", arguments: card.lastStatusCode.name, card.lastStatusDescription)
        }
        
    }
    
    /// Indicates if Direct Mode is selected
    ///
    /// - Returns: True if Direct Mode is selected, else false
    private func selectedModeIsDirect() -> Bool {
        return (modeSelector.selectedSegmentIndex == 1)
    }
    
    /// Enables groups of UI fields depending on the entered state
    ///
    /// - Parameter state: UI state (see enum UIState in this class for possible values)
    private func enableUIFields(state: UIState) {
        switch state {
            case .stateAppStart:
                enableMainFields(enabled: true)
                enableCommandFields(enabled: false)
                enableConnectionFields(enabled: false)
            case .stateTerminalsFound:
                enableMainFields(enabled: true)
                enableConnectionFields(enabled: true)
                enableCommandFields(enabled: false)
            case .stateConnected:
                enableMainFields(enabled: false)
                enableConnectionFields(enabled: false, excludeConnect: true)
                enableCommandFields(enabled: true)
        }
    }
    
    /// Enables or disables the main UI fields
    ///
    /// - Parameter enabled: True to set the fields as enabled, false to set the fields as disabled
    private func enableMainFields(enabled: Bool) {
        labelTerminal.isEnabled     = enabled
        selectedTerminal.isEnabled  = enabled
        buttonRefresh.isEnabled     = enabled
    }
    
    /// Enables or disables the command related UI fields
    ///
    /// - Parameter enabled: True to set the fields as enabled, false to set the fields as disabled
    private func enableCommandFields(enabled: Bool) {
        if enabled {
            if selectedModeIsDirect() {
                apduLabel.isEnabled             = false
                apduTextField.isEnabled         = false
                apduSendButton.isEnabled        = false
            }
            else {
                apduLabel.isEnabled             = true
                apduTextField.isEnabled         = true
                apduSendButton.isEnabled        = true
            }
            escapeCommandLabel.isEnabled        = true
            escapeCommandTextField.isEnabled    = true
            escapeCommandSendButton.isEnabled   = true
        }
        else {
            apduLabel.isEnabled                 = false
            apduTextField.isEnabled             = false
            apduSendButton.isEnabled            = false
            escapeCommandLabel.isEnabled        = false
            escapeCommandTextField.isEnabled    = false
            escapeCommandSendButton.isEnabled   = false
        }
    }
    
    /// Enables or disables the connection related UI fields
    ///
    /// - Parameter enabled: True to set the fields as enabled, false to set the fields as disabled
    private func enableConnectionFields(enabled: Bool, excludeConnect: Bool = false) {
        if !excludeConnect {
            buttonConnect.isEnabled = enabled
        }
        labelMode.isEnabled             = enabled
        modeSelector.isEnabled          = enabled
        waitForChangeButton.isEnabled   = enabled
        enableProtocolFields(enabled: enabled)
    }
    
    /// Enables or disables the protocol selection related UI fields
    ///
    /// - Parameter enabled: True to set the fields as enabled, false to set the fields as disabled
    private func enableProtocolFields(enabled: Bool) {
        if enabled {
            if selectedModeIsDirect() {
                labelProtocol.isEnabled = false
                labelT0.isEnabled       = false
                labelT1.isEnabled       = false
                protocolT0.isEnabled    = false
                protocolT1.isEnabled    = false
            }
            else {
                labelProtocol.isEnabled = true
                labelT0.isEnabled       = true
                labelT1.isEnabled       = true
                protocolT0.isEnabled    = true
                protocolT1.isEnabled    = true
            }
        }
        else {
            labelProtocol.isEnabled = false
            labelT0.isEnabled       = false
            labelT1.isEnabled       = false
            protocolT0.isEnabled    = false
            protocolT1.isEnabled    = false
        }
    }
    
    /// Output the terminal list to the log ui field
    private func outputTerminalsToLog() {
        for terminal in terminals {
            addLogLine(format: "Terminal %@ found", arguments: terminal.getName())
        }
    }
    
    /// Add a formatted line to the log ui field
    ///
    /// - Parameters:
    ///   - format: Line format to use
    ///   - arguments: Arguments for the format
    private func addLogLine(format : String, arguments: CVarArg...) {
        let message = String(format: format.replacingOccurrences(of: "%s", with: "%@"), arguments: arguments)

        self.addLogLine(line: message)
        self.scrollLogToEnd()
    }
    
    /// Adds a line to the log ui filed
    ///
    /// - Parameter line: Line to add
    private func addLogLine(line: String) {
        DispatchQueue.main.async {
            let formatter = DateFormatter()
            
            formatter.dateFormat = "HH:mm:ss"
            let message = "[" + formatter.string(from: Date()) + "] " + line + "\n"

            self.textViewLog.text = self.textViewLog.text + message
        }
    }
    
    /// Scrolls the log ui field contents to the end
    private func scrollLogToEnd() {
        DispatchQueue.main.async {
            let range = NSRange(location: self.textViewLog.text.count, length: 0)
            self.textViewLog.scrollRangeToVisible(range)
        }
    }
}

/// ViewController extension acting as  UIPickerViewDelegate and UIPickerViewDataSource
extension ViewController : UIPickerViewDelegate, UIPickerViewDataSource {
    
    /// Gets the number of components for the picker view. Here always 1, since we only use 1 component.
    ///
    /// - Parameter pickerView: UIPickerView to handle
    /// - Returns: Int holding the number of components
    func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
    }
    
    /// Returns the number of rows for a component.
    /// Here returns the number of rows in the main CardTerminal list (as that is the data source for the UIPickerView)
    ///
    /// - Parameters:
    ///   - pickerView: An object representing the picker view requesting the data.
    ///   - component: A zero-indexed number identifying a component of pickerView. Components are numbered left-to-right.
    /// - Returns: Int holding the number of rows for the specified component
    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return terminals.count
    }
    
    /// Called by the picker view when it needs the title to use for a given row in a given component.
    /// Here returns the name of the terminal from the terminal list associated with the row as datasource
    ///
    /// - Parameters:
    ///   - pickerView: An object representing the picker view requesting the data.
    ///   - row: A zero-indexed number identifying a row of component. Rows are numbered top-to-bottom.
    ///   - component: A zero-indexed number identifying a component of pickerView. Components are numbered left-to-right.
    /// - Returns: The string to use as the title of the indicated component row.
    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        if row < terminals.count {
            return terminals[row].getName()
        }
        else {
            return nil
        }
    }
}

/// ViewController extension acting as ToolbarUIPickerViewDelegate
extension ViewController: ToolbarUIPickerViewDelegateProtocol {
    
    /// User tapped 'Done' button. Here selects the current terminal and closes the UIPickerView.
    func didTapDone() {
        let row = terminalPicker.selectedRow(inComponent: 0)
        
        if row < terminals.count {
            terminalPicker.selectRow(row, inComponent: 0, animated: false)
      
            selectCurrentTerminal(newTerminal: terminals[row])
        }
        selectedTerminal.resignFirstResponder()
    }
    
    /// User tapped 'Cancel' button. Closes the UIPickerView.
    func didTapCancel() {
        selectedTerminal.resignFirstResponder()
    }
}

/// Viewcontroller extension acting as ABCCardTerminalMonitor delegate
extension ViewController: ABCCardTerminalMonitorProtocol {
    /// Call back function definition to implement by the delegate.
    /// Called once when a terminal has hit the battery threshold
    ///
    /// - Parameters:
    ///   - terminalName: Name of card terminal hitting the threshold
    ///   - batteryThreshold: Battery threshold reached
    func onBatteryThresholdReached(terminalName: String, batteryThreshold: UInt8) {
        addLogLine(format: "Terminal %@ battery threshold reached! (%d%%)", arguments: terminalName, batteryThreshold)
    }
    
    /// Call back function definition to implement by the delegate.
    /// Called once when a terminal has disconnected
    ///
    /// - Parameter terminalName: Name of card terminal that has disconnected
    func onDisconnected(terminalName: String) {
        addLogLine(format: "Terminal %@ disconnected!", arguments: terminalName)
    }
}
