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

import AmazonChimeSDK
import Foundation

typealias CompletionFunc = (Data?, Error?) -> Void

class HttpUtils {
    public static func postRequest(url: String,
                                   jsonData: Data? = nil,
                                   logger: Logger? = nil,
                                   completion: @escaping CompletionFunc) {
        makeHttpRequest(url: url, method: "post", jsonData: jsonData, logger: logger, completion: completion)
    }

    public static func getRequest(url: String, logger: Logger? = nil, completion: @escaping CompletionFunc) {
        makeHttpRequest(url: url, method: "get", jsonData: nil, logger: logger, completion: completion)
    }

    private static func makeHttpRequest(url: String,
                                        method: String,
                                        jsonData: Data?,
                                        logger: Logger?,
                                        completion: @escaping CompletionFunc) {
        guard let serverUrl = URL(string: url) else {
            logger?.error(msg: "Unable to parse Url please make sure check Url")
            return
        }

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

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

        URLSession.shared.dataTask(with: request) { data, resp, error in
            if let error = error {
                logger?.error(msg: error.localizedDescription)
                completion(nil, error)
                return
            }
            if let httpResponse = resp as? HTTPURLResponse {
                guard 200 ... 299 ~= httpResponse.statusCode else {
                    logger?.error(msg: "Received status code \(httpResponse.statusCode)")
                    completion(nil, NSError(domain: "", code: httpResponse.statusCode, userInfo: nil))
                    return
                }
            }
            guard let data = data else { return }
            completion(data, nil)
        }.resume()
    }

    public static func encodeStrForURL(str: String) -> String {
        return str.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? str
    }
}


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

import AmazonChimeSDK
import Foundation

class JoinRequestService: NSObject {
    static let logger = ConsoleLogger(name: "JoiningRequestService")

    static func postJoinRequest(meetingId: String,
                                name: String,
                                overriddenEndpoint: String,
                                primaryExternalMeetingId: String,
                                completion: @escaping (JoinMeetingResponse?) -> Void) {
        var url = overriddenEndpoint.isEmpty ? AppConfiguration.url : overriddenEndpoint
        url = url.hasSuffix("/") ? url : "\(url)/"
        let primaryExternalMeetingIdQueryParam = primaryExternalMeetingId.isEmpty ? "" : "&primaryExternalMeetingId=\(primaryExternalMeetingId)"
        let encodedURL = HttpUtils.encodeStrForURL(
            str: "\(url)join?title=\(meetingId)&name=\(name)&region=\(AppConfiguration.region)\(primaryExternalMeetingIdQueryParam)"
        )
        HttpUtils.postRequest(url: encodedURL, jsonData: nil, logger: logger) { data, _ in
            guard let data = data else {
                completion(nil)
                return
            }
            guard let joinMeetingResponse = self.processJson(data: data) else {
                completion(nil)
                return
            }
            completion(joinMeetingResponse)
        }
    }

    private static func processJson(data: Data) -> JoinMeetingResponse? {
        let jsonDecoder = JSONDecoder()
        do {
            let joinMeetingResponse = try jsonDecoder.decode(JoinMeetingResponse.self, from: data)
            return joinMeetingResponse
        } catch let DecodingError.dataCorrupted(context) {
            logger.error(msg: "Data corrupted: \(context)")
            return nil
        } catch let DecodingError.keyNotFound(key, context) {
            logger.error(msg: "Key '\(key)' not found: \(context.debugDescription), codingPath: \(context.codingPath)")
            return nil
        } catch let DecodingError.valueNotFound(value, context) {
            logger.error(msg: "Value '\(value)' not found: \(context.debugDescription), codingPath: \(context.codingPath)")
            return nil
        } catch let DecodingError.typeMismatch(type, context) {
            logger.error(msg: "Type '\(type)' mismatch: \(context.debugDescription), codingPath: \(context.codingPath)")
            return nil
        } catch {
            logger.error(msg: "Other decoding error: \(error)")
            return nil
        }
    }

    static func getCreateMeetingResponse(from joinMeetingResponse: JoinMeetingResponse) -> CreateMeetingResponse {
        let meeting = joinMeetingResponse.joinInfo.meeting.meeting
        let meetingResp = CreateMeetingResponse(meeting:
            Meeting(
                externalMeetingId: meeting.externalMeetingId,
                mediaPlacement: MediaPlacement(
                    audioFallbackUrl: meeting.mediaPlacement.audioFallbackUrl ?? "",
                    audioHostUrl: meeting.mediaPlacement.audioHostUrl,
                    signalingUrl: meeting.mediaPlacement.signalingUrl,
                    turnControlUrl: meeting.mediaPlacement.turnControlUrl ?? "",
                    eventIngestionUrl: meeting.mediaPlacement.eventIngestionUrl
                ),
                mediaRegion: meeting.mediaRegion,
                meetingId: meeting.meetingId,
                primaryMeetingId: meeting.primaryMeetingId
            )
        )
        return meetingResp
    }

    static func getCreateAttendeeResponse(from joinMeetingResponse: JoinMeetingResponse) -> CreateAttendeeResponse {
        let attendee = joinMeetingResponse.joinInfo.attendee.attendee
        let attendeeResp = CreateAttendeeResponse(attendee:
            Attendee(attendeeId: attendee.attendeeId,
                     externalUserId: attendee.externalUserId,
                     joinToken: attendee.joinToken)
        )
        return attendeeResp
    }
}


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

import Foundation
import AmazonChimeSDK

struct LogEntry: Codable {
    var sequenceNumber: Int
    var message: String
    var timestampMs: Int64
    var logLevel: String
}


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

import Foundation

struct LogRequestBody: Codable {
    var meetingId: String
    var attendeeId: String
    var appName: String
    var logs: [LogEntry]
}


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

import AmazonChimeSDK
import CoreVideo
import Foundation
import Metal
import MetalPerformanceShaders

// This processor is not available on Apple A7 and older chips
// since it uses MetalPerformanceShaders.
class MetalVideoProcessor: VideoSource, VideoSink {
    var videoContentHint = VideoContentHint.motion

    private let device: MTLDevice
    // Cache for textures created from CVPixelBuffers
    private let textureCache: CVMetalTextureCache
    // The command queue used to pass commands to the device.
    private let commandQueue: MTLCommandQueue
    private let sinks = NSMutableSet()

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

    init?() {
        // Initialize metal state and caches
        guard let defaultDevice = MTLCreateSystemDefaultDevice(),
            defaultDevice.supportsFeatureSet(.iOS_GPUFamily2_v1) else {
            return nil
        }
        device = defaultDevice
        var metalTextureCache: CVMetalTextureCache?
        CVMetalTextureCacheCreate(nil, nil, device, nil, &metalTextureCache)
        guard let cache = metalTextureCache else {
            return nil
        }
        textureCache = cache
        guard let queue = device.makeCommandQueue() else {
            return nil
        }
        commandQueue = queue
    }

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

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

    func onVideoFrameReceived(frame: VideoFrame) {
        let pixelBuffer = (frame.buffer as? VideoFramePixelBuffer)!
        let inputBuffer = pixelBuffer.pixelBuffer

        if bufferPool == nil || frame.width != bufferPoolWidth || frame.height != bufferPoolHeight {
            updateBufferPool(newWidth: frame.width, newHeight: frame.height)
        }
        var outputBuffer: CVPixelBuffer?
        CVPixelBufferPoolCreatePixelBuffer(nil, bufferPool!, &outputBuffer)
        let validOutputBuffer = outputBuffer!

        // For simplicity, we only support NV12 frames
        let pixelFormat = CVPixelBufferGetPixelFormatType(inputBuffer)
        if pixelFormat != kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
            pixelFormat != kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange {
            return
        }

        // Create textures from input and output pixel buffers
        let inputLumaTexture = createTexutreFromBuffer(pixelBuffer: inputBuffer,
                                                       plane: 0,
                                                       format: MTLPixelFormat.r8Unorm)!
        let inputChromaTexture = createTexutreFromBuffer(pixelBuffer: inputBuffer,
                                                         plane: 1,
                                                         format: MTLPixelFormat.rg8Unorm)!

        let outputLumaTexture = createTexutreFromBuffer(pixelBuffer: validOutputBuffer,
                                                        plane: 0,
                                                        format: MTLPixelFormat.r8Unorm)!
        let outputChromaTexture = createTexutreFromBuffer(pixelBuffer: validOutputBuffer,
                                                          plane: 1,
                                                          format: MTLPixelFormat.rg8Unorm)!

        // For simplicity, we just use a MetalPerformanceShader here on each of the planes
        // but using a custom shader should be similarly straightforward
        let commandBuffer = commandQueue.makeCommandBuffer()!
        let gaussianBlur = MPSImageGaussianBlur(device: device, sigma: 16)
        gaussianBlur.edgeMode = MPSImageEdgeMode.clamp
        gaussianBlur.encode(commandBuffer: commandBuffer,
                            sourceTexture: inputLumaTexture,
                            destinationTexture: outputLumaTexture)
        gaussianBlur.encode(commandBuffer: commandBuffer,
                            sourceTexture: inputChromaTexture,
                            destinationTexture: outputChromaTexture)
        commandBuffer.commit()
        commandBuffer.waitUntilCompleted()

        let processedFrame = VideoFrame(timestampNs: frame.timestampNs,
                                        rotation: frame.rotation,
                                        buffer: VideoFramePixelBuffer(pixelBuffer: validOutputBuffer))
        for sink in sinks {
            (sink as? VideoSink)?.onVideoFrameReceived(frame: processedFrame)
        }
    }

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

        bufferPoolWidth = newWidth
        bufferPoolHeight = newHeight
    }

    private func createTexutreFromBuffer(pixelBuffer: CVPixelBuffer,
                                         plane: Int,
                                         format: MTLPixelFormat) -> MTLTexture? {
        let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, plane)
        let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, plane)

        var metalTextureRef: CVMetalTexture?
        var texture: MTLTexture?
        let status = CVMetalTextureCacheCreateTextureFromImage(
            kCFAllocatorDefault,
            textureCache,
            pixelBuffer,
            nil,
            format,
            width,
            height,
            plane,
            &metalTextureRef
        )
        if status == kCVReturnSuccess {
            if let metalTextureRef = metalTextureRef {
                texture = CVMetalTextureGetTexture(metalTextureRef)
            }
        }
        return texture
    }
}


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

import Foundation
import AmazonChimeSDK
import os

class PostLogger: AmazonChimeSDK.Logger {
    private let name: String
    private var level: LogLevel
    private let meetingConfig: MeetingSessionConfiguration
    private let url: String
    private var logEntries: [LogEntry] = []
    private var sequenceNumber = 0

    init(name: String, configuration: MeetingSessionConfiguration, url: String, logLevel: LogLevel = LogLevel.INFO) {
        self.level = logLevel
        self.meetingConfig = configuration
        self.name = name
        self.url = url
    }

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

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

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

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

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

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

    func getLogLevel() -> LogLevel {
        return level
    }

    func publishLog() {
        let requestBody = LogRequestBody(meetingId: meetingConfig.meetingId,
                                         attendeeId: meetingConfig.credentials.attendeeId,
                                         appName: name,
                                         logs: logEntries)
        guard let nonNilData = try? JSONEncoder().encode(requestBody) else {
            return
        }

        HttpUtils.postRequest(url: self.url, jsonData: nonNilData) { _, error in
            if let error = error {
                os_log("%@", type: .error, "PostLogger post request failed \(error)")
            } else {
                os_log("%@", type: .info, "PostLogger post request succeeded")
                self.logEntries.removeAll()
            }
        }
    }

    private func log(level: LogLevel, msg: String) {
        if level.rawValue < self.level.rawValue {
            return
        }
        logEntries.append(LogEntry(sequenceNumber: sequenceNumber,
                        message: msg,
                        timestampMs: Int64(Date().timeIntervalSince1970 * 1000),
                        logLevel: String(describing: level)))
        sequenceNumber += 1
    }
}


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

import Foundation

class TimeStampConversion: NSObject {
    static func formatTimestamp(timestamp: Int64) -> String {
        let date = Date(timeIntervalSince1970: TimeInterval(timestamp / 1000))
        let dateFormatter = DateFormatter()
        dateFormatter.locale = NSLocale.current
        dateFormatter.dateFormat = "HH:mm"
        return dateFormatter.string(from: date)
    }
}


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

import UIKit

extension UIViewController {
    func setupHideKeyboardOnTap() {
        view.addGestureRecognizer(dismissKeyboardRecognizer())
    }

    private func dismissKeyboardRecognizer() -> UIGestureRecognizer {
        let tap = UITapGestureRecognizer(target: view, action: #selector(view.endEditing(_:)))
        tap.cancelsTouchesInView = false
        return tap
    }
}


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

import AmazonChimeSDK
import ReplayKit

let appGroupId = "YOUR_APP_GROUP_ID"

let userDefaultsKeyMeetingId = "demoMeetingId"
let userDefaultsKeyExternalMeetingId = "demoExternalMeetingId"
let userDefaultsKeyCredentials = "demoMeetingCredentials"
let userDefaultsKeyUrls = "demoMeetingUrls"
let userDefaultsKeyBroadcastMetrics = "demoMeetingBroadcastMetrics"

class SampleHandler: RPBroadcastSampleHandler {
    let logger = ConsoleLogger(name: "SampleHandler")
    let appGroupUserDefaults = UserDefaults(suiteName: appGroupId)
    var currentMeetingSession: MeetingSession?
    var userDefaultsObserver: NSKeyValueObservation?
    var cachedBroadcastMetrics: [String: Double] = [:]

    lazy var replayKitSource: ReplayKitSource = { return ReplayKitSource(logger: logger) }()

    lazy var contentShareSource: ContentShareSource = {
        let source = ContentShareSource()
        source.videoSource = replayKitSource
        return source
    }()

    override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
        // User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
        guard let config = getSavedMeetingSessionConfig() else {
            logger.error(msg: "Unable to recreate MeetingSessionConfiguration from Broadcast Extension")
            finishBroadcastWithError(NSError(domain: "AmazonChimeSDKDemoBroadcast", code: 0))
            return
        }
        currentMeetingSession = DefaultMeetingSession(configuration: config, logger: logger)
        currentMeetingSession?.audioVideo.addMetricsObserver(observer: self)
        currentMeetingSession?.audioVideo.startContentShare(source: contentShareSource)

        // If the meetingId is changed from the demo app, we need to observe the meetingId and stop broadcast
        userDefaultsObserver = appGroupUserDefaults?.observe(\.demoMeetingId,
                                                 options: [.new, .old]) { [weak self] (_, _) in
            guard let strongSelf = self else { return }
            strongSelf.finishBroadcastWithError(NSError(domain: "AmazonChimeSDKDemoBroadcast", code: 1))
        }
    }

    override func broadcastPaused() {
        // User has requested to pause the broadcast. Samples will stop being delivered.
    }

    override func broadcastResumed() {
        // User has requested to resume the broadcast. Samples delivery will resume.
    }

    override func broadcastFinished() {
        // User has requested to finish the broadcast.
        replayKitSource.stop()
        currentMeetingSession?.audioVideo.stopContentShare()
        currentMeetingSession?.audioVideo.removeMetricsObserver(observer: self)
        appGroupUserDefaults?.removeObject(forKey: userDefaultsKeyBroadcastMetrics)
        userDefaultsObserver?.invalidate()
    }

    override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
        replayKitSource.processSampleBuffer(sampleBuffer: sampleBuffer, type: sampleBufferType)
    }

    // Recreate the MeetingSessionConfiguration from the active meeting in the app
    private func getSavedMeetingSessionConfig() -> MeetingSessionConfiguration? {
        guard let appGroupUserDefaults = appGroupUserDefaults else {
            logger.error(msg: "App Group User Defaults not found")
            return nil
        }
        let decoder = JSONDecoder()
        if let meetingId = appGroupUserDefaults.demoMeetingId,
           let externalMeetingId = appGroupUserDefaults.demoExternalMeetingId,
           let credentialsData = appGroupUserDefaults.demoMeetingCredentials,
           let urlsData = appGroupUserDefaults.demoMeetingUrls,
           let credentials = try? decoder.decode(MeetingSessionCredentials.self, from: credentialsData),
           let urls = try? decoder.decode(MeetingSessionURLs.self, from: urlsData) {

            return MeetingSessionConfiguration(meetingId: meetingId,
                                               externalMeetingId: externalMeetingId,
                                               credentials: credentials,
                                               urls: urls,
                                               urlRewriter: URLRewriterUtils.defaultUrlRewriter)
        }
        return nil
    }
}

// Since Broadcast Extension runs independently from Demo app, Metrics from Broadcasting does not
// automatically flow into the app. So we are saving these metrics from Broadcast Extension into
// shared App Groups User Defaults, observe and retrieve them in MetricsModel to display in demo app.
extension SampleHandler: MetricsObserver {
    func metricsDidReceive(metrics: [AnyHashable: Any]) {
        cachedBroadcastMetrics[ObservableMetric.contentShareVideoSendBitrate.description]
            = metrics[ObservableMetric.contentShareVideoSendBitrate] as? Double
        cachedBroadcastMetrics[ObservableMetric.contentShareVideoSendPacketLossPercent.description]
            = metrics[ObservableMetric.contentShareVideoSendPacketLossPercent] as? Double
        cachedBroadcastMetrics[ObservableMetric.contentShareVideoSendFps.description]
            = metrics[ObservableMetric.contentShareVideoSendFps] as? Double
        cachedBroadcastMetrics[ObservableMetric.contentShareVideoSendRttMs.description]
            = metrics[ObservableMetric.contentShareVideoSendRttMs] as? Double
        appGroupUserDefaults?.setValue(cachedBroadcastMetrics, forKey: userDefaultsKeyBroadcastMetrics)
    }
}

extension UserDefaults {
    @objc dynamic var demoMeetingId: String? {
        return string(forKey: userDefaultsKeyMeetingId)
    }
    @objc dynamic var demoExternalMeetingId: String? {
        return string(forKey: userDefaultsKeyExternalMeetingId)
    }
    @objc dynamic var demoMeetingCredentials: Data? {
        return object(forKey: userDefaultsKeyCredentials) as? Data
    }
    @objc dynamic var demoMeetingUrls: Data? {
        return object(forKey: userDefaultsKeyUrls) as? Data
    }
}


// swift-tools-version: 5.6
import PackageDescription

// Current stable version of the Amazon Chime SDK for iOS
let SDKVersion = "0.23.1"
let SDKMediaVersion = "0.18.1"
let SDKMachineLearningVersion = "0.2.0"

// Hosting url where the release artifacts are hosted.
let hostingUrl = "https://amazon-chime-sdk-ios.s3.amazonaws.com"

// Checksums for SDKs to verify that the hosted archive file matches the archive declared in the manifest file.
let SDKChecksum = "ee267d3232f72bc4e4c2ccd37dabed89750a1ef2159d3ef8d0396c8ef1cadfb4"
let SDKMediaChecksum = "20268ae2c8c17e8721576ebf0846a837db2e15eca37ebe0e9c0d6c1c365b3a12"
let SDKMachineLearningChecksum = "d79e15d11b24ee53219a0c02963e60c4b56cd2f4d121bf8d40c3fe3068a14a9d"

let package = Package(
    name: "AmazonChimeSDK",
    products: [
        .library(name: "AmazonChimeSDK", targets: ["AmazonChimeSDK", "AmazonChimeSDKMedia"]),
        .library(name: "AmazonChimeSDKMedia", targets: ["AmazonChimeSDKMedia"]),
        .library(name: "AmazonChimeSDKMachineLearning", targets: ["AmazonChimeSDKMachineLearning"])
    ],
    targets: [
        .binaryTarget(
            name: "AmazonChimeSDK",
            url: "\(hostingUrl)/sdk-without-bitcode/\(SDKVersion)/spm/AmazonChimeSDK-\(SDKVersion).zip",
            checksum: SDKChecksum
        ),
        .binaryTarget(
            name: "AmazonChimeSDKMedia",
            url: "\(hostingUrl)/media-without-bitcode/\(SDKMediaVersion)/spm/AmazonChimeSDKMedia-\(SDKMediaVersion).zip",
            checksum: SDKMediaChecksum
        ),
        .binaryTarget(
            name: "AmazonChimeSDKMachineLearning",
            url: "\(hostingUrl)/machine-learning-without-bitcode/\(SDKMachineLearningVersion)/spm/AmazonChimeSDKMachineLearning-\(SDKMachineLearningVersion).zip",
            checksum: SDKMachineLearningChecksum
        )
    ]
)


import CoreBluetooth

/// FreeRTOS Constants
public enum AmazonFreeRTOS {
    /// FreeRTOS SDK Version.
    static let SDKVersion = "1.1.0"
}

/// BLE services used by the SDK.
public enum AmazonFreeRTOSGattService {
    /// Device Info Service. This is a required service for FreeRTOS.
    static let DeviceInfo = CBUUID(string: "8a7f1168-48af-4efb-83b5-e679f932ff00")
    /// Mqtt Proxy Service.
    static let MqttProxy = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30000")
    /// Network Config Service.
    static let NetworkConfig = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30100")
}

/// BLE characteristics used by the SDK.
public enum AmazonFreeRTOSGattCharacteristic {
    /// The version of the FreeRTOS.
    static let AfrVersion = CBUUID(string: "8a7f1168-48af-4efb-83b5-e679f932ff01")
    /// The broker endpoint of the mqtt.
    static let BrokerEndpoint = CBUUID(string: "8a7f1168-48af-4efb-83b5-e679f932ff02")
    /// The mtu of the device.
    static let Mtu = CBUUID(string: "8a7f1168-48af-4efb-83b5-e679f932ff03")
    /// The platform of the FreeRTOS.
    static let AfrPlatform = CBUUID(string: "8a7f1168-48af-4efb-83b5-e679f932ff04")
    /// The device id of the FreeRTOS.
    static let AfrDevId = CBUUID(string: "8a7f1168-48af-4efb-83b5-e679f932ff05")

    /// Used for mqtt control state.
    static let MqttProxyControl = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30001")
    /// Used for transfer mqtt messages.
    static let TXMqttMessage = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30002")
    /// Used for transfer mqtt messages.
    static let RXMqttMessage = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30003")
    /// Used for mqtt large object transfer.
    static let TXLargeMqttMessage = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30004")
    /// Used for mqtt large object transfer.
    static let RXLargeMqttMessage = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30005")

    /// Used for network control state.
    static let NetworkConfigControl = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30101")
    /// Used for transfer network messages.
    static let TXNetworkMessage = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30102")
    /// Used for transfer network messages.
    static let RXNetworkMessage = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30103")
    /// Used for network large object transfer.
    static let TXLargeNetworkMessage = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30104")
    /// Used for network large object transfer.
    static let RXLargeNetworkMessage = CBUUID(string: "a9d7166a-d72e-40a9-a002-48044cc30105")
}


import AWSIoT
import CoreBluetooth

/// FreeRTOS device.
public class AmazonFreeRTOSDevice: NSObject {

    /// CBPeripheral.
    public var peripheral: CBPeripheral
    /// advertisementData.
    public var advertisementData: [String: Any]?
    /// rssi.
    public var RSSI: NSNumber?

    /// FreeRTOS device should auto reconnect on non-explicit disconnect.
    public var reconnect = false
    /// The certificateId used to connect. see: https://github.com/awslabs/aws-sdk-ios-samples/tree/master/IoT-Sample/Swift
    public var certificateId: String?
    /// The credentialsProvider used to connect like AWSMobileClient for Cognito.
    public var credentialsProvider: AWSCredentialsProvider?

    /// Afr Version of the device.
    public var afrVersion: String?
    /// The brokerEndpoint on the device.
    public var brokerEndpoint: String?
    /// The MTU of the device.
    public var mtu: Int?
    /// Afr Platform of the device.
    public var afrPlatform: String?
    /// Afr device id.
    public var afrDevId: String?

    /// The saved networks.
    public var savedNetworks: [ListNetworkResp] = []
    /// The scaned networks.
    public var scanedNetworks: [ListNetworkResp] = []

    // Used for LOT read.
    internal var txLotDatas: [String: Data] = [:]
    // Used for LOT write.
    internal var rxLotDataQueues: [String: [Data]] = [:]

    /// Initializes a new FreeRTOS device.
    ///
    /// - Parameter peripheral: The CBPeripheral.
    /// - Returns: A new FreeRTOS device.
    public init(peripheral: CBPeripheral) {
        self.peripheral = peripheral
        super.init()
    }

    // clear context of the device.
    internal func reset() {

        removeIoTDataManager()

        txLotDatas.removeAll()
        rxLotDataQueues.removeAll()

        afrVersion = nil
        brokerEndpoint = nil
        mtu = nil
        afrPlatform = nil
        afrDevId = nil

        savedNetworks.removeAll()
        scanedNetworks.removeAll()
    }

    // register the AWSIoTDataManager
    internal func registerIoTDataManager(connect: Connect) -> Bool {

        let brokerEndpointParts = connect.brokerEndpoint.split(separator: ".")
        guard let endpoint = AWSEndpoint(urlString: "https://\(connect.brokerEndpoint)"), brokerEndpointParts.count > 2, let serviceConfiguration = AWSServiceConfiguration(region: String(brokerEndpointParts[2]).aws_regionTypeValue(), endpoint: endpoint, credentialsProvider: credentialsProvider) else {
            return false
        }
        AWSIoTDataManager.register(with: serviceConfiguration, forKey: peripheral.identifier.uuidString)
        updateIoTDataManager()
        return true
    }

    // remove the AWSIoTDataManager
    internal func removeIoTDataManager() {
        AWSIoTDataManager(forKey: peripheral.identifier.uuidString).disconnect()
        AWSIoTDataManager.remove(forKey: peripheral.identifier.uuidString)

        txLotDatas.removeValue(forKey: AmazonFreeRTOSGattService.MqttProxy.uuidString)
        rxLotDataQueues.removeValue(forKey: AmazonFreeRTOSGattService.MqttProxy.uuidString)
    }

    // update the metadata of the AWSIoTDataManager
    internal func updateIoTDataManager() {
        var userMetaData = ["AFRSDK": "iOS", "AFRSDKVersion": AmazonFreeRTOS.SDKVersion]
        if let afrVersion = afrVersion, !afrVersion.isEmpty {
            userMetaData["AFRLibVersion"] = afrVersion
        }
        if let afrPlatform = afrPlatform, !afrPlatform.isEmpty {
            userMetaData["Platform"] = afrPlatform
        }
        if let afrDevId = afrDevId, !afrDevId.isEmpty {
            userMetaData["AFRDevID"] = afrDevId
        }
        AWSIoTDataManager(forKey: peripheral.identifier.uuidString).updateUserMetaData(userMetaData)
    }
}

extension AmazonFreeRTOSDevice {

    /// Connect to the FreeRTOS device.
    /// - Parameters:
    ///    - reconnect: FreeRTOS device should auto reconnect on non-explicit disconnect.
    ///    - certificateId: The certificateId used to connect. see: https://github.com/awslabs/aws-sdk-ios-samples/tree/master/IoT-Sample/Swift
    ///    - credentialsProvider: The credentialsProvider used to connect like AWSMobileClient for Cognito.
    /// - Precondition: central is ready and device must be disconnected, otherwise it will be ignored.
    public func connect(reconnect: Bool, certificateId: String? = nil, credentialsProvider: AWSCredentialsProvider? = nil) {
        self.reconnect = reconnect
        self.certificateId = certificateId
        self.credentialsProvider = credentialsProvider
        if peripheral.state == .disconnected {
            AmazonFreeRTOSManager.shared.central?.connect(peripheral, options: nil)
        }
    }

    /// Disconnect from the FreeRTOS device.
    ///
    /// - Precondition: central is ready and device must be connected, otherwise it will be ignored.
    public func disconnect() {
        reconnect = false
        if peripheral.state == .connected {
            AmazonFreeRTOSManager.shared.central?.cancelPeripheralConnection(peripheral)
        }
    }
}

// Device Info Service

extension AmazonFreeRTOSDevice {

    /// Get afrVersion of the FreeRTOS device.
    public func getAfrVersion() {

        AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)] â†“ get afrVersion")

        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.DeviceInfo)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.AfrVersion) else {
            AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] DeviceInfo service or AfrVersion characteristic doesn't exist")
            return
        }
        peripheral.readValue(for: characteristic)
    }

    /// Get mqtt broker endpoint of the FreeRTOS device.
    public func getBrokerEndpoint() {

        AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)] â†“ get brokerEndpoint")

        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.DeviceInfo)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.BrokerEndpoint) else {
            AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] DeviceInfo service or BrokerEndpoint characteristic doesn't exist")
            return
        }
        peripheral.readValue(for: characteristic)
    }

    /// Get BLE mtu of the FreeRTOS device.
    public func getMtu() {

        AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)] â†“ get mtu")

        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.DeviceInfo)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.Mtu) else {
            AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] DeviceInfo service or Mtu characteristic doesn't exist")
            return
        }
        peripheral.readValue(for: characteristic)
    }

    /// Get afrPlatform of the FreeRTOS device.
    public func getAfrPlatform() {

        AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)] â†“ get afrPlatform")

        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.DeviceInfo)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.AfrPlatform) else {
            AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] DeviceInfo service or AfrPlatform characteristic doesn't exist")
            return
        }
        peripheral.readValue(for: characteristic)
    }

    /// Get afrDevId of the FreeRTOS device.
    public func getAfrDevId() {

        AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)] â†“ get afrDevId")

        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.DeviceInfo)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.AfrDevId) else {
            AmazonFreeRTOSManager.shared.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] DeviceInfo service or AfrDevId characteristic doesn't exist")
            return
        }
        peripheral.readValue(for: characteristic)
    }
}

// Network Config Service

extension AmazonFreeRTOSDevice {

    /// List saved and scanned wifi networks of device. Wifi networks are returned one by one, saved wifi ordered by priority and scanned wifi ordered by signal strength (rssi).
    ///
    /// - Parameter listNetworkReq: The list network request.
    public func listNetwork(_ listNetworkReq: ListNetworkReq) {

        // reset networks list for the peripheral

        savedNetworks.removeAll()
        scanedNetworks.removeAll()

        AmazonFreeRTOSManager.shared.listNetwork(peripheral, listNetworkReq: listNetworkReq)
    }

    /// Save wifi network to device.
    ///
    /// - Parameter saveNetworkReq: The save network request.
    public func saveNetwork(_ saveNetworkReq: SaveNetworkReq) {
        AmazonFreeRTOSManager.shared.saveNetwork(peripheral, saveNetworkReq: saveNetworkReq)
    }

    /// Edit wifi network of device. Currently only support priority change.
    ///
    /// - Parameter editNetworkReq: The edit network request.
    public func editNetwork(_ editNetworkReq: EditNetworkReq) {
        AmazonFreeRTOSManager.shared.editNetwork(peripheral, editNetworkReq: editNetworkReq)
    }

    /// Delete saved wifi network from device.
    ///
    /// - Parameter deleteNetworkReq: The delete network request.
    public func deleteNetwork(_ deleteNetworkReq: DeleteNetworkReq) {
        AmazonFreeRTOSManager.shared.deleteNetwork(peripheral, deleteNetworkReq: deleteNetworkReq)
    }
}


/// Mqtt message types. Reference: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718021
public enum MqttMessageType: Int, Codable {
    /// Connect.
    case connect = 1
    /// Connack.
    case connack = 2
    /// Publish.
    case publish = 3
    /// Puback.
    case puback = 4
    /// Not supported by AWS IoT.
    case pubrec = 5
    /// Not supported by AWS IoT.
    case pubrel = 6
    /// Not supported by AWS IoT.
    case pubcomp = 7
    /// Subscribe.
    case subscribe = 8
    /// Suback.
    case suback = 9
    /// Unsubscribe.
    case unsubscribe = 10
    /// Unsuback.
    case unsuback = 11
    /// Pingreq.
    case pingreq = 12
    /// Pingresp.
    case pingresp = 13
    /// Disconnnect.
    case disconnnect = 14
}

/// Network message type.
public enum NetworkMessageType: Int, Codable {
    /// ListNetworkReq.
    case listNetworkReq = 1
    /// ListNetworkResp.
    case listNetworkResp = 2
    /// SaveNetworkReq.
    case saveNetworkReq = 3
    /// SaveNetworkResp.
    case saveNetworkResp = 4
    /// EditNetworkReq.
    case editNetworkReq = 5
    /// EditNetworkResp.
    case editNetworkResp = 6
    /// DeleteNetworkReq.
    case deleteNetworkReq = 7
    /// DeleteNetworkResp.
    case deleteNetworkResp = 8
}

/// Network security types.
public enum NetworkSecurityType: Int, Codable {
    /// Open.
    case open = 0
    /// Wep.
    case wep = 1
    /// Wpa.
    case wpa = 2
    /// Wpa 2.
    case wpa2 = 3
    /// Wpa 2 Enterprise.
    case wpa2Ent = 4
    /// Not Supported.
    case notSupported = 5
}

/// Network statuses.
public enum NetworkOpStatus: Int, Codable {
    /// Success.
    case success = 0
    /// Failure.
    case failure = 1
    /// Timeout.
    case timeout = 2
    /// Not Supported.
    case notSupported = 3
}

/// Keys for cbor.
public enum CborKey: String {
    /// brokerEndpoint.
    case brokerEndpoint = "a"
    /// bssid.
    case bssid = "b"
    /// cleanSession.
    case cleanSession = "c"
    /// clientID.
    case clientID = "d"
    /// connected.
    case connected = "e"
    /// hidden.
    case hidden = "f"
    /// index.
    case index = "g"
    /// maxNetworks.
    case maxNetworks = "h"
    /// msgID.
    case msgID = "i"
    /// newIndex.
    case newIndex = "j"
    /// payload.
    case payload = "k"
    /// proxyState.
    case proxyState = "l"
    /// psk.
    case psk = "m"
    /// qoS.
    case qoS = "n"
    /// qoSs.
    case qoSs = "o"
    /// rssi.
    case rssi = "p"
    /// security.
    case security = "q"
    /// ssid.
    case ssid = "r"
    /// status.
    case status = "s"
    /// timeout.
    case timeout = "t"
    /// topic.
    case topic = "u"
    /// topics.
    case topics = "v"
    /// type.
    case type = "w"
    /// connect.
    case connect = "y"
}


import AWSIoT
import CBORCoding
import CoreBluetooth
import os.log

/// FreeRTOS manager.
public class AmazonFreeRTOSManager: NSObject {

    /// Shared instence of FreeRTOS manager.
    public static let shared = AmazonFreeRTOSManager()

    /// Enable debug messages.
    public var isDebug: Bool = false
    /// Debug messages.
    public var debugMessages = String()

    /// Service UUIDs in the advertising packets.
    public var advertisingServiceUUIDs: [CBUUID] = [AmazonFreeRTOSGattService.DeviceInfo]
    /// The FreeRTOS devices using peripheral identifier as key.
    public var devices: [UUID: AmazonFreeRTOSDevice] = [:]

    /// BLE Central Manager of the FreeRTOS manager.
    public var central: CBCentralManager?

    /// Initializes a new FreeRTOS manager.
    ///
    /// - Returns: A new FreeRTOS manager.
    override public init() {
        super.init()
        central = CBCentralManager(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])
    }
}

extension AmazonFreeRTOSManager {

    /// Start scan for FreeRTOS devices.
    ///
    /// - Precondition: `central` is ready and not scanning.
    public func startScanForDevices() {
        if let central = central, !central.isScanning {
            central.scanForPeripherals(withServices: advertisingServiceUUIDs, options: nil)
        }
    }

    /// Stop scan for FreeRTOS devices.
    ///
    /// - Precondition: `central` is ready and is scanning.
    public func stopScanForDevices() {
        if let central = central, central.isScanning {
            central.stopScan()
        }
    }

    /// Disconnect. Clear all contexts. Scan for FreeRTOS devices.
    public func rescanForDevices() {
        stopScanForDevices()

        for device in devices.values {
            device.disconnect()
        }
        devices.removeAll()

        startScanForDevices()
    }
}

extension AmazonFreeRTOSManager: CBCentralManagerDelegate {

    /// CBCentralManagerDelegate
    public func centralManagerDidUpdateState(_ central: CBCentralManager) {
        NotificationCenter.default.post(name: .afrCentralManagerDidUpdateState, object: nil)
        debugPrint("[Central] afrCentralManagerDidUpdateState: \(central.state.rawValue)")
    }

    /// CBCentralManagerDelegate
    public func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
        if !devices.keys.contains(peripheral.identifier) {
            devices[peripheral.identifier] = AmazonFreeRTOSDevice(peripheral: peripheral)
        }
        devices[peripheral.identifier]?.advertisementData = advertisementData
        devices[peripheral.identifier]?.RSSI = RSSI
        NotificationCenter.default.post(name: .afrCentralManagerDidDiscoverDevice, object: nil, userInfo: ["identifier": peripheral.identifier])
        debugPrint("[Central] afrCentralManagerDidDiscoverPeripheral: \(peripheral.identifier.uuidString)")
    }

    /// CBCentralManagerDelegate
    public func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
        devices[peripheral.identifier]?.reset()
        peripheral.delegate = self
        peripheral.discoverServices([AmazonFreeRTOSGattService.DeviceInfo, AmazonFreeRTOSGattService.MqttProxy, AmazonFreeRTOSGattService.NetworkConfig])
        NotificationCenter.default.post(name: .afrCentralManagerDidConnectDevice, object: nil, userInfo: ["identifier": peripheral.identifier])
        debugPrint("[\(peripheral.identifier.uuidString)] afrCentralManagerDidConnectPeripheral")
    }

    /// CBCentralManagerDelegate
    public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        devices[peripheral.identifier]?.reset()
        if devices[peripheral.identifier]?.reconnect ?? false {
            central.connect(peripheral, options: nil)
        }

        if let error = error {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrCentralManagerDidDisconnectPeripheral: \(error.localizedDescription)")
            NotificationCenter.default.post(name: .afrCentralManagerDidDisconnectDevice, object: nil, userInfo: ["identifier": peripheral.identifier, "error": error])
            return
        }
        NotificationCenter.default.post(name: .afrCentralManagerDidDisconnectDevice, object: nil, userInfo: ["identifier": peripheral.identifier])
        debugPrint("[\(peripheral.identifier.uuidString)] afrCentralManagerDidDisconnectPeripheral")
    }

    /// CBCentralManagerDelegate
    public func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        if let error = error {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrCentralManagerDidFailToConnectPeripheral: \(error.localizedDescription)")
            NotificationCenter.default.post(name: .afrCentralManagerDidFailToConnectDevice, object: nil, userInfo: ["identifier": peripheral.identifier, "error": error])
            return
        }
        NotificationCenter.default.post(name: .afrCentralManagerDidFailToConnectDevice, object: nil, userInfo: ["identifier": peripheral.identifier])
        debugPrint("[\(peripheral.identifier.uuidString)] afrCentralManagerDidFailToConnectPeripheral")
    }
}

extension AmazonFreeRTOSManager: CBPeripheralDelegate {

    /// CBPeripheralDelegate
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let error = error {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrPeripheralDidDiscoverServices: \(error.localizedDescription)")
            return
        }
        for service in peripheral.services ?? [] {
            peripheral.discoverCharacteristics(nil, for: service)
        }
        NotificationCenter.default.post(name: .afrPeripheralDidDiscoverServices, object: nil, userInfo: ["peripheral": peripheral])
    }

    /// CBPeripheralDelegate
    public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let error = error {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrPeripheralDidDiscoverCharacteristics: \(error.localizedDescription)")
            return
        }
        for characteristic in service.characteristics ?? [] {
            peripheral.setNotifyValue(true, for: characteristic)
        }
        switch service.uuid {

        case AmazonFreeRTOSGattService.DeviceInfo:
            devices[peripheral.identifier]?.getAfrVersion()
            devices[peripheral.identifier]?.getBrokerEndpoint()
            devices[peripheral.identifier]?.getMtu()
            devices[peripheral.identifier]?.getAfrPlatform()
            devices[peripheral.identifier]?.getAfrDevId()

        case AmazonFreeRTOSGattService.MqttProxy:
            guard let characteristic = service.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.MqttProxyControl) else {
                debugPrint("[\(peripheral.identifier.uuidString)][ERROR] MqttServiceState characteristic doesn't exist")
                return
            }
            devices[peripheral.identifier]?.peripheral.writeValue(Data([1]), for: characteristic, type: .withResponse)

        case AmazonFreeRTOSGattService.NetworkConfig:
            guard let characteristic = service.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.NetworkConfigControl) else {
                debugPrint("[\(peripheral.identifier.uuidString)][ERROR] NetworkServiceState characteristic doesn't exist")
                return
            }
            devices[peripheral.identifier]?.peripheral.writeValue(Data([1]), for: characteristic, type: .withResponse)

        default:
            return
        }

        NotificationCenter.default.post(name: .afrPeripheralDidDiscoverCharacteristics, object: nil, userInfo: ["peripheral": peripheral, "service": service])
    }

    /// CBPeripheralDelegate
    public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {

        if let error = error {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrPeripheralDidUpdateValueForCharacteristic: \(error.localizedDescription)")
            return
        }

        switch characteristic.uuid {

        case AmazonFreeRTOSGattCharacteristic.AfrVersion:
            didUpdateValueForAfrVersion(peripheral: peripheral, characteristic: characteristic)

        case AmazonFreeRTOSGattCharacteristic.BrokerEndpoint:
            didUpdateValueForBrokerEndpoint(peripheral: peripheral, characteristic: characteristic)

        case AmazonFreeRTOSGattCharacteristic.Mtu:
            didUpdateValueForMtu(peripheral: peripheral, characteristic: characteristic)

        case AmazonFreeRTOSGattCharacteristic.AfrPlatform:
            didUpdateValueForAfrPlatform(peripheral: peripheral, characteristic: characteristic)

        case AmazonFreeRTOSGattCharacteristic.AfrDevId:
            didUpdateValueForAfrDevId(peripheral: peripheral, characteristic: characteristic)

        case AmazonFreeRTOSGattCharacteristic.TXMqttMessage, AmazonFreeRTOSGattCharacteristic.TXNetworkMessage:
            didUpdateValueForTXMessage(peripheral: peripheral, characteristic: characteristic, data: nil)

        case AmazonFreeRTOSGattCharacteristic.TXLargeMqttMessage, AmazonFreeRTOSGattCharacteristic.TXLargeNetworkMessage:
            didUpdateValueForTXLargeMessage(peripheral: peripheral, characteristic: characteristic)

        default:
            return
        }
    }

    /// CBPeripheralDelegate
    public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {

        if let error = error {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrPeripheralDidWriteValueForCharacteristic: \(error.localizedDescription)")
            return
        }

        switch characteristic.uuid {

        case AmazonFreeRTOSGattCharacteristic.RXLargeMqttMessage, AmazonFreeRTOSGattCharacteristic.RXLargeNetworkMessage:
            writeValueToRXLargeMessage(peripheral: peripheral, characteristic: characteristic)

        default:
            return
        }
    }
}

extension AmazonFreeRTOSManager {

    internal func encode<T: Encodable>(_ object: T) -> Data? {
        do {
            let encoder = CBOREncoder()
            let encoded = try encoder.encode(object)
            return Data(encoded)
        } catch {
            debugPrint("[ERROR] CBOR decode error: \(error)")
        }
        return nil
    }

    internal func decode<T: Decodable>(_ type: T.Type, from data: Data) -> T? {
        do {
            let decoder = CBORDecoder()
            let decoded = try decoder.decode(type, from: data)
            return decoded
        } catch {
            debugPrint("[ERROR] CBOR decode error: \(error)")
        }
        return nil
    }

    internal func debugPrint(_ debugMessage: String) {
        guard isDebug else {
            return
        }
        debugMessages += "\(Date())-\(debugMessage)\n"
        os_log("[AFR]%@", log: .default, type: .debug, debugMessage)
    }
}

extension AmazonFreeRTOSManager {

    /// Process data of AfrVersion characteristic from `peripheral`.
    ///
    /// - Parameters:
    ///    - peripheral: The FreeRTOS peripheral.
    ///    - characteristic: The AfrVersion characteristic.
    public func didUpdateValueForAfrVersion(peripheral: CBPeripheral, characteristic: CBCharacteristic) {

        guard let value = characteristic.value, let afrVersion = String(data: value, encoding: .utf8) else {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrDeviceInfoAfrVersion: Invalid AfrVersion")
            return
        }
        devices[peripheral.identifier]?.afrVersion = afrVersion
        devices[peripheral.identifier]?.updateIoTDataManager()
        NotificationCenter.default.post(name: .afrDeviceInfoAfrVersion, object: nil, userInfo: ["afrVersion": afrVersion])
        debugPrint("[\(peripheral.identifier.uuidString)] â†’ afrDeviceInfoAfrVersion: \(afrVersion)")
    }

    /// Process data of BrokerEndpoint characteristic from `peripheral`.
    ///
    /// - Parameters:
    ///     - peripheral: The FreeRTOS peripheral.
    ///     - characteristic: The BrokerEndpoint characteristic.
    ///
    public func didUpdateValueForBrokerEndpoint(peripheral: CBPeripheral, characteristic: CBCharacteristic) {

        guard let value = characteristic.value, let brokerEndpoint = String(data: value, encoding: .utf8) else {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrDeviceInfoBrokerEndpoint: Invalid BrokerEndpoint")
            return
        }
        devices[peripheral.identifier]?.brokerEndpoint = brokerEndpoint
        NotificationCenter.default.post(name: .afrDeviceInfoBrokerEndpoint, object: nil, userInfo: ["brokerEndpoint": brokerEndpoint])
        debugPrint("[\(peripheral.identifier.uuidString)] â†’ afrDeviceInfoBrokerEndpoint: \(brokerEndpoint)")
    }

    /// Process data of Mtu characteristic from `peripheral`. It will also triger on mtu value change.
    ///
    /// - Parameters:
    ///     - peripheral: The FreeRTOS peripheral.
    ///     - characteristic: The Mtu characteristic.
    public func didUpdateValueForMtu(peripheral: CBPeripheral, characteristic: CBCharacteristic) {

        guard let value = characteristic.value, let mtuStr = String(data: value, encoding: .utf8), let mtu = Int(mtuStr), mtu > 3 else {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrDeviceInfoMtu: Invalid Mtu")
            return
        }
        devices[peripheral.identifier]?.mtu = mtu
        NotificationCenter.default.post(name: .afrDeviceInfoMtu, object: nil, userInfo: ["mtu": mtu])
        debugPrint("[\(peripheral.identifier.uuidString)] â†’ afrDeviceInfoMtu: \(mtu)")
    }

    /// Process data of AfrPlatform characteristic from `peripheral`.
    ///
    /// - Parameters:
    ///     - peripheral: The FreeRTOS peripheral.
    ///     - characteristic: The AfrPlatform characteristic.
    public func didUpdateValueForAfrPlatform(peripheral: CBPeripheral, characteristic: CBCharacteristic) {

        guard let value = characteristic.value, let afrPlatform = String(data: value, encoding: .utf8) else {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrDeviceInfoAfrPlatform: Invalid AfrPlatform")
            return
        }
        devices[peripheral.identifier]?.afrPlatform = afrPlatform
        devices[peripheral.identifier]?.updateIoTDataManager()
        NotificationCenter.default.post(name: .afrDeviceInfoAfrPlatform, object: nil, userInfo: ["afrPlatform": afrPlatform])
        debugPrint("[\(peripheral.identifier.uuidString)] â†’ afrDeviceInfoAfrPlatform: \(afrPlatform)")
    }

    /// Process data of AfrDevId characteristic from `peripheral`.
    ///
    /// - Parameters:
    ///     - peripheral: The FreeRTOS peripheral.
    ///     - characteristic: The AfrDevId characteristic.
    public func didUpdateValueForAfrDevId(peripheral: CBPeripheral, characteristic: CBCharacteristic) {

        guard let value = characteristic.value, let afrDevId = String(data: value, encoding: .utf8) else {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] afrDeviceInfoAfrDevId: Invalid AfrDevId")
            return
        }
        devices[peripheral.identifier]?.afrDevId = afrDevId
        devices[peripheral.identifier]?.updateIoTDataManager()
        NotificationCenter.default.post(name: .afrDeviceInfoAfrDevId, object: nil, userInfo: ["afrDevId": afrDevId])
        debugPrint("[\(peripheral.identifier.uuidString)] â†’ afrDeviceInfoAfrDevId: \(afrDevId)")
    }
}

extension AmazonFreeRTOSManager {

    // Process data of TXMqttMessage or TXNetworkMessage characteristic from `peripheral`.
    internal func didUpdateValueForTXMessage(peripheral: CBPeripheral, characteristic: CBCharacteristic, data: Data?) {

        guard let value = data ?? characteristic.value else {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] didUpdateValueForTXMessage: Invalid message")
            return
        }

        let testCharacteristicService: CBService? = characteristic.service
        guard let guardCharacteristicService = testCharacteristicService else {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] didUpdateValueForTXMessage: Invalid characteristic")
            return
        }
        let characteristicService: CBService = guardCharacteristicService

        switch characteristicService.uuid {

        case AmazonFreeRTOSGattService.MqttProxy:

            guard let mqttMessage = decode(MqttMessage.self, from: value) else {
                debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid message")
                return
            }

            switch mqttMessage.type {

            case .connect:

                guard let connect = decode(Connect.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid connect")
                    return
                }

                debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†‘ \(connect)")

                if !(devices[peripheral.identifier]?.registerIoTDataManager(connect: connect) ?? false) {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid connect: registerIoTDataManager failed")
                    return
                }

                if let certificateId = devices[peripheral.identifier]?.certificateId {
                    AWSIoTDataManager(forKey: peripheral.identifier.uuidString).connect(withClientId: connect.clientID, cleanSession: connect.cleanSession, certificateId: certificateId) { status in

                        switch status {

                        case .connected:
                            self.mqttConnack(peripheral: peripheral, characteristic: characteristic, status: status.rawValue)

                        default:
                            self.debugPrint("[\(peripheral.identifier.uuidString)][MQTT] connectUsingWebSocket status: \(status.rawValue)")
                            return
                        }
                    }
                } else if devices[peripheral.identifier]?.credentialsProvider != nil {
                    AWSIoTDataManager(forKey: peripheral.identifier.uuidString).connectUsingWebSocket(withClientId: connect.clientID, cleanSession: connect.cleanSession) { status in

                        switch status {

                        case .connected:
                            self.mqttConnack(peripheral: peripheral, characteristic: characteristic, status: status.rawValue)

                        default:
                            self.debugPrint("[\(peripheral.identifier.uuidString)][MQTT] connectUsingWebSocket status: \(status.rawValue)")
                            return
                        }
                    }
                } else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid connect: No credential")
                }

            case .publish:

                guard let publish = decode(Publish.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid publish")
                    return
                }

                debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†‘ \(publish)")

                guard let qoS = AWSIoTMQTTQoS(rawValue: publish.qoS) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid publish: qoS")
                    return
                }

                if qoS == AWSIoTMQTTQoS.messageDeliveryAttemptedAtMostOnce {
                    AWSIoTDataManager(forKey: peripheral.identifier.uuidString).publishData(publish.payload, onTopic: publish.topic, qoS: qoS)
                    return
                }
                AWSIoTDataManager(forKey: peripheral.identifier.uuidString).publishData(publish.payload, onTopic: publish.topic, qoS: qoS) {
                    self.mqttPuback(peripheral: peripheral, characteristic: characteristic, msgID: publish.msgID)
                }

            case .puback:
                // need aws iot sdk change, current it auto send puback when recived at sdk side.
                return

            case .subscribe:

                guard let subscribe = decode(Subscribe.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid subscribe")
                    return
                }

                debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†‘ \(subscribe)")

                guard subscribe.topics.count == subscribe.qoSs.count else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid subscribe: topics and qoSs not match")
                    return
                }

                for (index, topic) in subscribe.topics.enumerated() {

                    guard let qoS = AWSIoTMQTTQoS(rawValue: subscribe.qoSs[index]) else {
                        debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid subscribe: qoS")
                        return
                    }

                    AWSIoTDataManager(forKey: peripheral.identifier.uuidString).subscribe(toTopic: topic, qoS: qoS, messageCallback: { data in
                        self.mqttPublish(peripheral: peripheral, characteristic: characteristic, msgID: subscribe.msgID, topic: topic, qoS: subscribe.qoSs[index], data: data)
                    }, ackCallback: {
                        self.mqttSuback(peripheral: peripheral, characteristic: characteristic, msgID: subscribe.msgID, status: subscribe.qoSs[index])
                    })
                }

            case .unsubscribe:

                guard let unsubscribe = decode(Unsubscribe.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid unsubscribe")
                    return
                }

                debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†‘ \(unsubscribe)")

                for topic in unsubscribe.topics {

                    AWSIoTDataManager(forKey: peripheral.identifier.uuidString).unsubscribeTopic(topic)

                    mqttUnsubscribe(peripheral: peripheral, characteristic: characteristic, msgID: unsubscribe.msgID)
                }

            case .disconnnect:

                guard let disconnect = decode(Disconnect.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid disconnect")
                    return
                }

                debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†‘ \(disconnect)")

                devices[peripheral.identifier]?.removeIoTDataManager()

            case .pingreq:

                guard let pingreq = decode(Pingreq.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid pingreq")
                    return
                }

                debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†‘ \(pingreq)")

                guard AWSIoTDataManager(forKey: peripheral.identifier.uuidString).getConnectionStatus() == .connected else {
                    return
                }

                mqttPingresp(peripheral: peripheral, characteristic: characteristic)

            default:
                return
            }

        case AmazonFreeRTOSGattService.NetworkConfig:

            guard let networkMessage = decode(NetworkMessage.self, from: value) else {
                debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid message")
                return
            }

            switch networkMessage.type {

            case .listNetworkResp:
                guard let listNetworkResp = decode(ListNetworkResp.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid listNetworkResp")
                    return
                }
                debugPrint("[\(peripheral.identifier.uuidString)][NETWORK] â†’ \(listNetworkResp)")

                if listNetworkResp.index < 0 {

                    // Scaned networks also include saved networks so we filter that out when ssid and security are the same, update the saved network with the scaned bssid, rssi and hidden prams.

                    if let indexSaved = devices[peripheral.identifier]?.savedNetworks.firstIndex(where: { network -> Bool in
                        network.ssid == listNetworkResp.ssid && network.security == listNetworkResp.security
                    }) {
                        if let rssi = devices[peripheral.identifier]?.savedNetworks[indexSaved].rssi, rssi < listNetworkResp.rssi {
                            devices[peripheral.identifier]?.savedNetworks[indexSaved].status = listNetworkResp.status
                            devices[peripheral.identifier]?.savedNetworks[indexSaved].bssid = listNetworkResp.bssid
                            devices[peripheral.identifier]?.savedNetworks[indexSaved].rssi = listNetworkResp.rssi
                            devices[peripheral.identifier]?.savedNetworks[indexSaved].hidden = listNetworkResp.hidden
                        }
                        return
                    }

                    // Scaned networks sorted by rssi, if ssid and security are same, choose the network with stronger rssi.

                    if let indexScaned = devices[peripheral.identifier]?.scanedNetworks.firstIndex(where: { network -> Bool in
                        network.ssid == listNetworkResp.ssid && network.security == listNetworkResp.security
                    }) {
                        if let rssi = devices[peripheral.identifier]?.scanedNetworks[indexScaned].rssi, rssi < listNetworkResp.rssi {
                            devices[peripheral.identifier]?.scanedNetworks[indexScaned] = listNetworkResp
                        }
                    } else {
                        devices[peripheral.identifier]?.scanedNetworks.append(listNetworkResp)
                    }
                    devices[peripheral.identifier]?.scanedNetworks.sort(by: { networkA, networkB -> Bool in
                        networkA.rssi > networkB.rssi
                    })
                } else {

                    // Saved networks sorted by index

                    devices[peripheral.identifier]?.savedNetworks.append(listNetworkResp)
                    devices[peripheral.identifier]?.savedNetworks.sort(by: { networkA, networkB -> Bool in
                        networkA.index < networkB.index
                    })
                }
                NotificationCenter.default.post(name: .afrDidListNetwork, object: nil, userInfo: ["peripheral": peripheral.identifier, "listNetworkResp": listNetworkResp])

            case .saveNetworkResp:
                guard let saveNetworkResp = decode(SaveNetworkResp.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid saveNetworkResp")
                    return
                }
                debugPrint("[\(peripheral.identifier.uuidString)][NETWORK] â†’ \(saveNetworkResp)")
                NotificationCenter.default.post(name: .afrDidSaveNetwork, object: nil, userInfo: ["peripheral": peripheral.identifier, "saveNetworkResp": saveNetworkResp])

            case .editNetworkResp:
                guard let editNetworkResp = decode(EditNetworkResp.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid editNetworkResp")
                    return
                }
                debugPrint("[\(peripheral.identifier.uuidString)][NETWORK] â†’ \(editNetworkResp)")
                NotificationCenter.default.post(name: .afrDidEditNetwork, object: nil, userInfo: ["peripheral": peripheral.identifier, "editNetworkResp": editNetworkResp])

            case .deleteNetworkResp:
                guard let deleteNetworkResp = decode(DeleteNetworkResp.self, from: value) else {
                    debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid deleteNetworkResp")
                    return
                }
                debugPrint("[\(peripheral.identifier.uuidString)][NETWORK] â†’ \(deleteNetworkResp)")
                NotificationCenter.default.post(name: .afrDidDeleteNetwork, object: nil, userInfo: ["peripheral": peripheral.identifier, "deleteNetworkResp": deleteNetworkResp])

            default:
                return
            }

        default:
            return
        }
    }

    // Process data of TXLargeMqttMessage or TXLargeNetworkMessage characteristic from `peripheral`. Used by large object transfer.
    internal func didUpdateValueForTXLargeMessage(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
        guard let mtu = devices[peripheral.identifier]?.mtu else {
            debugPrint("[\(peripheral.identifier.uuidString)][LOT][ERROR] Mtu unknown")
            return
        }

        let testCharacteristicService: CBService? = characteristic.service
        guard let guardCharacteristicService = testCharacteristicService else {
            debugPrint("[\(peripheral.identifier.uuidString)][ERROR] didUpdateValueForTXLargeMessage: Invalid characteristic")
            return
        }
        let characteristicService: CBService = guardCharacteristicService

        if let txLotData = devices[peripheral.identifier]?.txLotDatas[characteristicService.uuid.uuidString], let value = characteristic.value {
            let data = Data([UInt8](txLotData) + [UInt8](value))
            devices[peripheral.identifier]?.txLotDatas[characteristicService.uuid.uuidString] = data
            debugPrint("[\(peripheral.identifier.uuidString)][LOT] â†‘ \(data)")
        } else if let value = characteristic.value {
            devices[peripheral.identifier]?.txLotDatas[characteristicService.uuid.uuidString] = value
            debugPrint("[\(peripheral.identifier.uuidString)][LOT] â†‘ \(value)")
        }
        if characteristic.value?.count ?? 0 < mtu - 3 {
            if let txLotData = devices[peripheral.identifier]?.txLotDatas[characteristicService.uuid.uuidString] {
                didUpdateValueForTXMessage(peripheral: peripheral, characteristic: characteristic, data: txLotData)
                debugPrint("[\(peripheral.identifier.uuidString)][LOT] LAST FULL â†‘ \(txLotData)")
            }
            devices[peripheral.identifier]?.txLotDatas.removeValue(forKey: characteristicService.uuid.uuidString)
        } else {
            peripheral.readValue(for: characteristic)
        }
    }

    // Write data to RXMqttMessage or RXNetworkMessage characteristic of `peripheral`. Used by large object transfer.
    internal func writeValueToRXMessage(peripheral: CBPeripheral, characteristic: CBCharacteristic, data: Data) {
        DispatchQueue.main.async {
            guard let mtu = self.devices[peripheral.identifier]?.mtu else {
                self.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] Mtu unknown")
                return
            }

            let testCharacteristicService: CBService? = characteristic.service
            guard let guardCharacteristicService = testCharacteristicService else {
                self.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] writeValueToRXMessage: Invalid characteristic")
                return
            }
            let characteristicService: CBService = guardCharacteristicService

            if data.count > mtu - 3 {
                guard let characteristic = characteristicService.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.RXLargeMqttMessage) ?? characteristicService.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.RXLargeNetworkMessage) else {
                    self.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] RXLargeMqttMessage or RXLargeNetworkMessage characteristic doesn't exist")
                    return
                }
                if self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString] == nil {
                    self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString] = [data]
                } else {
                    self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString]?.append(data)
                }
                if self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString]?.count == 1 {
                    self.writeValueToRXLargeMessage(peripheral: peripheral, characteristic: characteristic)
                }
                return
            }
            guard let characteristic = characteristicService.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.RXMqttMessage) ?? characteristicService.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.RXNetworkMessage) else {
                self.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] RXMqttMessage or RXNetworkMessage characteristic doesn't exist")
                return
            }

            peripheral.writeValue(data, for: characteristic, type: .withResponse)
        }
    }

    // Write data to RXLargeMqttMessage or RXLargeNetworkMessage characteristic of `peripheral`. Used by large object transfer.
    internal func writeValueToRXLargeMessage(peripheral: CBPeripheral, characteristic: CBCharacteristic) {
        DispatchQueue.main.async {
            guard let mtu = self.devices[peripheral.identifier]?.mtu else {
                self.debugPrint("[\(peripheral.identifier.uuidString)][LOT][ERROR] Mtu unknown")
                return
            }

            let testCharacteristicService: CBService? = characteristic.service
            guard let guardCharacteristicService = testCharacteristicService else {
                self.debugPrint("[\(peripheral.identifier.uuidString)][ERROR] writeValueToRXLargeMessage: Invalid characteristic")
                return
            }
            let characteristicService: CBService = guardCharacteristicService

            guard let rxLotData = self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString]?.first else {
                return
            }
            let data = Data([UInt8](rxLotData).prefix(mtu - 3))
            if data.count < mtu - 3 {
                self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString]?.removeFirst()
                self.debugPrint("[\(peripheral.identifier.uuidString)][LOT] LAST PART â†“ \(data) - \(self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString]?.count ?? 0) in queue")
            } else {
                self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString]?[0] = Data([UInt8](rxLotData).dropFirst(mtu - 3))
                self.debugPrint("[\(peripheral.identifier.uuidString)][LOT] â†“ \(rxLotData) - \(self.devices[peripheral.identifier]?.rxLotDataQueues[characteristicService.uuid.uuidString]?.count ?? 0) in queue")
            }
            peripheral.writeValue(data, for: characteristic, type: .withResponse)
        }
    }
}

extension AmazonFreeRTOSManager {

    internal func mqttConnack(peripheral: CBPeripheral, characteristic: CBCharacteristic, status: Int) {

        let connack = Connack(status: status)

        debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†“ \(connack)")

        guard let data = encode(connack) else {
            debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid connack")
            return
        }

        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }

    internal func mqttPuback(peripheral: CBPeripheral, characteristic: CBCharacteristic, msgID: Int) {

        let puback = Puback(msgID: msgID)

        debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†“ \(puback)")

        guard let data = encode(puback) else {
            debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid puback")
            return
        }

        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }

    internal func mqttPublish(peripheral: CBPeripheral, characteristic: CBCharacteristic, msgID: Int, topic: String, qoS: Int, data: Data) {

        let publish = Publish(topic: topic, msgID: msgID, qoS: qoS, payload: data)

        debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†“ \(publish)")

        guard let data = encode(publish) else {
            debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid publish")
            return
        }

        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }

    internal func mqttSuback(peripheral: CBPeripheral, characteristic: CBCharacteristic, msgID: Int, status: Int) {

        let suback = Suback(msgID: msgID, status: status)

        debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†“ \(suback)")

        guard let data = encode(suback) else {
            debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid suback")
            return
        }

        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }

    internal func mqttUnsubscribe(peripheral: CBPeripheral, characteristic: CBCharacteristic, msgID: Int) {

        let unsuback = Unsuback(msgID: msgID)

        debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†“ \(unsuback)")

        guard let data = encode(unsuback) else {
            debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid unsuback")
            return
        }

        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }

    internal func mqttPingresp(peripheral: CBPeripheral, characteristic: CBCharacteristic) {

        let pingresp = Pingresp()

        debugPrint("[\(peripheral.identifier.uuidString)][MQTT] â†“ \(pingresp)")

        guard let data = encode(pingresp) else {
            debugPrint("[\(peripheral.identifier.uuidString)][MQTT][ERROR] Invalid pingresp")
            return
        }

        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }
}

extension AmazonFreeRTOSManager {

    internal func listNetwork(_ peripheral: CBPeripheral, listNetworkReq: ListNetworkReq) {

        debugPrint("[\(peripheral.identifier.uuidString)][NETWORK] â†“ \(listNetworkReq)")

        guard let data = encode(listNetworkReq) else {
            debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid listNetworkReq")
            return
        }
        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.NetworkConfig)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.RXNetworkMessage) else {
            debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] NetworkConfig service or RXNetworkMessage characteristic doesn't exist")
            return
        }
        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }

    internal func saveNetwork(_ peripheral: CBPeripheral, saveNetworkReq: SaveNetworkReq) {

        debugPrint("[\(peripheral.identifier.uuidString)][NETWORK] â†“ \(saveNetworkReq)")

        guard let data = encode(saveNetworkReq) else {
            debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid saveNetworkReq")
            return
        }
        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.NetworkConfig)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.RXNetworkMessage) else {
            debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] NetworkConfig service or RXNetworkMessage characteristic doesn't exist")
            return
        }
        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }

    internal func editNetwork(_ peripheral: CBPeripheral, editNetworkReq: EditNetworkReq) {

        debugPrint("[\(peripheral.identifier.uuidString)][NETWORK] â†“ \(editNetworkReq)")

        guard let data = encode(editNetworkReq) else {
            debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid editNetworkReq")
            return
        }
        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.NetworkConfig)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.RXNetworkMessage) else {
            debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] NetworkConfig service or RXNetworkMessage characteristic doesn't exist")
            return
        }
        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }

    internal func deleteNetwork(_ peripheral: CBPeripheral, deleteNetworkReq: DeleteNetworkReq) {

        debugPrint("[\(peripheral.identifier.uuidString)][NETWORK] â†“ \(deleteNetworkReq)")

        guard let data = encode(deleteNetworkReq) else {
            debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] Invalid deleteNetworkReq")
            return
        }
        guard let characteristic = peripheral.serviceOf(uuid: AmazonFreeRTOSGattService.NetworkConfig)?.characteristicOf(uuid: AmazonFreeRTOSGattCharacteristic.RXNetworkMessage) else {
            debugPrint("[\(peripheral.identifier.uuidString)][NETWORK][ERROR] NetworkConfig service or RXNetworkMessage characteristic doesn't exist")
            return
        }
        writeValueToRXMessage(peripheral: peripheral, characteristic: characteristic, data: data)
    }
}


import CoreBluetooth

extension CBPeripheral {

    /// Helper - get the service for a particular UUID
    public func serviceOf(uuid: CBUUID) -> CBService? {
        return services?.first(where: { service -> Bool in
            service.uuid == uuid
        })
    }
}


import CoreBluetooth

extension CBService {

    /// Helper - get the characteristic for a particular UUID
    public func characteristicOf(uuid: CBUUID) -> CBCharacteristic? {
        return characteristics?.first(where: { characteristic -> Bool in
            characteristic.uuid == uuid
        })
    }
}


/// Notifications for operations
extension NSNotification.Name {

    // BLE Central

    /// FreeRTOS BLE Central Manager didUpdateState.
    public static let afrCentralManagerDidUpdateState = NSNotification.Name("afrCentralManagerDidUpdateState")
    /// FreeRTOS BLE Central Manager didDiscoverDevice.
    public static let afrCentralManagerDidDiscoverDevice = NSNotification.Name("afrCentralManagerDidDiscoverDevice")
    /// FreeRTOS BLE Central Manager didConnectDevice.
    public static let afrCentralManagerDidConnectDevice = NSNotification.Name("afrCentralManagerDidConnectDevice")
    /// FreeRTOS BLE Central Manager didDisconnectDevice.
    public static let afrCentralManagerDidDisconnectDevice = NSNotification.Name("afrCentralManagerDidDisconnectDevice")
    /// FreeRTOS BLE Central Manager didFailToConnectDevice.
    public static let afrCentralManagerDidFailToConnectDevice = NSNotification.Name("afrCentralManagerDidFailToConnectDevice")

    // BLE Peripheral

    /// FreeRTOS BLE Peripheral didDiscoverServices.
    public static let afrPeripheralDidDiscoverServices = NSNotification.Name("afrPeripheralDidDiscoverServices")
    /// FreeRTOS BLE Peripheral didDiscoverCharacteristics.
    public static let afrPeripheralDidDiscoverCharacteristics = NSNotification.Name("afrPeripheralDidDiscoverCharacteristics")

    // DeviceInfo

    /// FreeRTOS return device info afr version.
    public static let afrDeviceInfoAfrVersion = NSNotification.Name("afrDeviceInfoAfrVersion")
    /// FreeRTOS return device info broker endpoint.
    public static let afrDeviceInfoBrokerEndpoint = NSNotification.Name("afrDeviceInfoBrokerEndpoint")
    /// FreeRTOS return device info mtu.
    public static let afrDeviceInfoMtu = NSNotification.Name("afrDeviceInfoMtu")
    /// FreeRTOS return device info afr platform.
    public static let afrDeviceInfoAfrPlatform = NSNotification.Name("afrDeviceInfoAfrPlatform")
    /// FreeRTOS return device info afr dev id.
    public static let afrDeviceInfoAfrDevId = NSNotification.Name("afrDeviceInfoAfrDevId")

    // NetworkConfig

    /// FreeRTOS list network returned a saved or scaned network.
    public static let afrDidListNetwork = NSNotification.Name("afrDidListNetwork")
    /// FreeRTOS save network did save a network.
    public static let afrDidSaveNetwork = NSNotification.Name("afrDidSaveNetwork")
    /// FreeRTOS edit network did edit a saved network.
    public static let afrDidEditNetwork = NSNotification.Name("afrDidEditNetwork")
    /// FreeRTOS delete network did delete a saved network.
    public static let afrDidDeleteNetwork = NSNotification.Name("afrDidDeleteNetwork")
}


/// Mqtt proxy message of Connack.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Connack: Encodable {

    /// Mqtt connection status. Reference: https://github.com/aws/aws-sdk-ios/blob/master/AWSIoT/AWSIoTMQTTTypes.h#L20
    private var messageType: Int
    public var status: Int

    public init(status: Int) {
        messageType = MqttMessageType.connack.rawValue
        self.status = status
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case status = "s"
    }
}


/// Mqtt proxy message of Connect.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Connect: Decodable {
    /// Mqtt client id.
    public var clientID: String
    /// Mqtt broker endpoint.
    public var brokerEndpoint: String
    /// Mqtt clean session.
    public var cleanSession: Bool

    private enum CodingKeys: String, CodingKey {
        case clientID = "d"
        case brokerEndpoint = "a"
        case cleanSession = "c"
    }
}


/// Mqtt proxy message of Disconnect.
public struct Disconnect: Decodable {}


/// Generic mqtt proxy message.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct MqttMessage: Decodable {
    /// Mqtt message type.
    public var type: MqttMessageType

    private enum CodingKeys: String, CodingKey {
        case type = "w"
    }
}


/// Mqtt proxy message of Pingreq.
public struct Pingreq: Decodable {}


/// Mqtt proxy message of Pingresp.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Pingresp: Encodable {

    /// Mqtt message type
    private var messageType: Int

    public init() {
        messageType = MqttMessageType.pingresp.rawValue
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
    }
}


/// Mqtt proxy message of Puback.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Puback: Encodable {

    /// Mqtt message type
    private var messageType: Int
    /// Mqtt message id.
    public var msgID: Int

    public init(msgID: Int) {
        messageType = MqttMessageType.puback.rawValue
        self.msgID = msgID
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case msgID = "i"
    }
}


/// Mqtt proxy message of Publish.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Publish: Codable {
    /// Mqtt message type
    private var messageType: Int
    /// Mqtt topic.
    public var topic: String
    /// Mqtt message id.
    public var msgID: Int
    /// Mqtt QoS.
    public var qoS: Int
    /// Mqtt payload.
    public var payload: Data

    /// Publish msg init (internal).
    ///
    /// - Parameters:
    ///     - topic: Mqtt topic.
    ///     - msgID: Mqtt message id.
    ///     - qoS: Mqtt QoS.
    ///     - payload: Mqtt payload.
    /// - Returns: A new SaveNetworkReq.
    public init(topic: String, msgID: Int, qoS: Int, payload: Data) {
        messageType = MqttMessageType.publish.rawValue
        self.topic = topic
        self.msgID = msgID
        self.qoS = qoS
        self.payload = payload
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case topic = "u"
        case msgID = "i"
        case qoS = "n"
        case payload = "k"
    }

    public init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        messageType = try values.decode(Int.self, forKey: .messageType)
        topic = try values.decode(String.self, forKey: .topic)

        if values.contains(.msgID) {
            msgID = try values.decode(Int.self, forKey: .msgID)
        } else {
            msgID = 0
        }

        qoS = try values.decode(Int.self, forKey: .qoS)
        payload = try values.decode(Data.self, forKey: .payload)
    }
}


/// Mqtt proxy message of Suback.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Suback: Encodable {

    /// Mqtt message type
    private var messageType: Int
    /// Mqtt message id.
    public var msgID: Int
    /// Suback status. Use subscribe Qos on iOS.
    public var status: Int

    public init(msgID: Int, status: Int) {
        messageType = MqttMessageType.suback.rawValue
        self.msgID = msgID
        self.status = status
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case msgID = "i"
        case status = "s"
    }
}


/// Mqtt proxy message of Subscribe.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Subscribe: Decodable {
    /// Mqtt topics.
    public var topics: [String]
    /// Mqtt message id.
    public var msgID: Int
    /// Mqtt QoSs.
    public var qoSs: [Int]

    private enum CodingKeys: String, CodingKey {
        case topics = "v"
        case msgID = "i"
        case qoSs = "o"
    }
}


/// Mqtt proxy message of Unsuback.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Unsuback: Encodable {

    /// Mqtt message type
    private var messageType: Int
    /// Mqtt message id.
    public var msgID: Int

    public init(msgID: Int) {
        messageType = MqttMessageType.unsuback.rawValue
        self.msgID = msgID
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case msgID = "i"
    }
}


/// Mqtt proxy message of Unsubscribe.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct Unsubscribe: Decodable {
    /// Mqtt topics.
    public var topics: [String]
    /// Mqtt message id.
    public var msgID: Int

    private enum CodingKeys: String, CodingKey {
        case topics = "v"
        case msgID = "i"
    }
}


/// Delete network request.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct DeleteNetworkReq: Encodable {

    /// message type
    private var messageType: Int
    /// Index of the saved network to delete.
    public var index: Int

    /// DeleteNetworkReq is used to delete a saved network.
    ///
    /// - Parameter Index: index of the saved network to delete.
    /// - Returns: A new DeleteNetworkReq.
    public init(index: Int) {
        messageType = NetworkMessageType.deleteNetworkReq.rawValue
        self.index = index
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case index = "g"
    }
}


/// Delete network response.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct DeleteNetworkResp: Decodable {
    /// Delete operation status.
    public var status: NetworkOpStatus

    private enum CodingKeys: String, CodingKey {
        case status = "s"
    }
}


/// Edit network request.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct EditNetworkReq: Encodable {

    /// message type
    private var messageType: Int
    /// Old index of the saved network.
    public var index: Int
    /// New index of the saved network.
    public var newIndex: Int

    /// EditNetworkReq is used to update the priority of a saved network.
    ///
    /// - Parameters:
    ///     - index: Old index of the saved network.
    ///     - newIndex: New index of the saved network.
    /// - Returns: A new EditNetworkReq.
    public init(index: Int, newIndex: Int) {
        messageType = NetworkMessageType.editNetworkReq.rawValue
        self.index = index
        self.newIndex = newIndex
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case index = "g"
        case newIndex = "j"
    }
}


/// Edit network response.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct EditNetworkResp: Decodable {
    /// Edit operation status.
    public var status: NetworkOpStatus

    private enum CodingKeys: String, CodingKey {
        case status = "s"
    }
}


/// List network request.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct ListNetworkReq: Encodable {

    /// message type
    private var messageType: Int
    /// Max number of networks to scan.
    public var maxNetworks: Int
    /// Time to scan in seconds.
    public var timeout: Int

    /// ListNetworkReq is used to list saved and scanned wifi networks.
    ///
    /// - Parameters:
    ///     - maxNetworks: Max number of networks to scan.
    ///     - timeout: Time to scan in seconds.
    /// - Returns: A new EditNetworkReq.
    public init(maxNetworks: Int, timeout: Int) {
        messageType = NetworkMessageType.listNetworkReq.rawValue
        self.maxNetworks = maxNetworks
        self.timeout = timeout
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case maxNetworks = "h"
        case timeout = "t"
    }
}


/// List network response.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct ListNetworkResp: Decodable {
    /// Index of the network. < 0 for scaned networks.
    public var index: Int
    /// List operation status.
    public var status: NetworkOpStatus
    /// Wifi ssid.
    public var ssid: String
    /// Wifi bssid (Mac address).
    public var bssid: Data
    /// Wifi rssi (Signal strenth).
    public var rssi: Int
    /// Wifi security type.
    public var security: NetworkSecurityType
    /// Wifi is hidden or not.
    public var hidden: Bool
    /// Wifi is connected or not.
    public var connected: Bool
    /// Last Network in the list or not
    public var last: Bool?

    private enum CodingKeys: String, CodingKey {
        case index = "g"
        case status = "s"
        case ssid = "r"
        case bssid = "b"
        case rssi = "p"
        case security = "q"
        case hidden = "f"
        case connected = "e"
        case last = "l"
    }
}


/// Generic network config message.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct NetworkMessage: Decodable {
    /// Network message type.
    public var type: NetworkMessageType

    private enum CodingKeys: String, CodingKey {
        case type = "w"
    }
}


/// Save network request.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct SaveNetworkReq: Encodable {
    /// Mqtt message type
    private var messageType: Int
    /// Index of the network.
    public var index: Int
    /// Wifi ssid.
    public var ssid: String
    /// Wifi bssid (Mac address).
    public var bssid: Data
    /// Wifi password. Saved network ignore this value.
    public var psk: String
    /// Wifi security type.
    public var security: NetworkSecurityType
    /// Connect immediately or just save for later.
    public var connect: Bool

    /// SaveNetworkReq is used to save wifi network.
    ///
    /// - Parameters:
    ///     - index: Index of the network.
    ///     - ssid: Wifi ssid.
    ///     - bssid: Wifi bssid (Mac address).
    ///     - psk: Wifi password. Saved network ignore this value.
    ///     - security: Wifi security type.
    ///     - connect: Connect immediately or just save for later.
    /// - Returns: A new SaveNetworkReq.
    public init(index: Int, ssid: String, bssid: Data, psk: String, security: NetworkSecurityType, connect: Bool) {
        messageType = NetworkMessageType.saveNetworkReq.rawValue
        self.index = index
        self.ssid = ssid
        self.bssid = bssid
        self.psk = psk
        self.security = security
        self.connect = connect
    }

    private enum CodingKeys: String, CodingKey {
        case messageType = "w"
        case index = "g"
        case ssid = "r"
        case bssid = "b"
        case psk = "m"
        case security = "q"
        case connect = "y"
    }
}


/// Save network response.
/// To reduce the encoded CBOR message size, we maps the variable name with a single character by CodingKey
/// Check the "CborKey" Enum to see the mapping relationship.
public struct SaveNetworkResp: Decodable {
    /// Save operation status.
    public var status: NetworkOpStatus

    private enum CodingKeys: String, CodingKey {
        case status = "s"
    }
}


@testable import AmazonFreeRTOS
import XCTest

class AmazonFreeRTOSTests: XCTestCase {}