"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var _this = this;
Object.defineProperty(exports, "__esModule", { value: true });
var colors_1 = require("colors");
var mqttClient_1 = require("./mqttClient");
var createService_1 = require("./app/services/createService");
var JobExecutionStatus;
(function (JobExecutionStatus) {
    JobExecutionStatus["QUEUED"] = "QUEUED";
    JobExecutionStatus["IN_PROGRESS"] = "IN_PROGRESS";
    JobExecutionStatus["SUCCEEDED"] = "SUCCEEDED";
    JobExecutionStatus["FAILED"] = "FAILED";
    JobExecutionStatus["TIMED_OUT"] = "TIMED_OUT";
    JobExecutionStatus["REJECTED"] = "REJECTED";
    JobExecutionStatus["REMOVED"] = "REMOVED";
    JobExecutionStatus["CANCELED"] = "CANCELED";
})(JobExecutionStatus || (JobExecutionStatus = {}));
var JobDocumentOperation;
(function (JobDocumentOperation) {
    JobDocumentOperation["app_fw_update"] = "app_fw_update";
})(JobDocumentOperation || (JobDocumentOperation = {}));
var DfuStatus;
(function (DfuStatus) {
    DfuStatus["downloadFirmware"] = "download_firmware";
    DfuStatus["applyUpdate"] = "apply_update";
    DfuStatus["none"] = "none";
})(DfuStatus || (DfuStatus = {}));
exports.nrfdevice = function (config, sensors, onConnect) {
    var deviceId = config.deviceId, caCert = config.caCert, clientCert = config.clientCert, privateKey = config.privateKey, endpoint = config.endpoint, appFwVersion = config.appFwVersion, mqttMessagesPrefix = config.mqttMessagesPrefix;
    var topics = function (deviceId) { return ({
        jobs: {
            notifyNext: "$aws/things/" + deviceId + "/jobs/notify-next",
            update: function (jobId) { return ({
                _: "$aws/things/" + deviceId + "/jobs/" + jobId + "/update",
                accepted: "$aws/things/" + deviceId + "/jobs/" + jobId + "/update/accepted",
            }); },
        },
        shadow: {
            update: {
                _: "$aws/things/" + deviceId + "/shadow/update",
            },
        },
    }); };
    console.log({
        deviceId: deviceId,
        endpoint: endpoint,
        region: endpoint.split('.')[2],
        topics: topics,
        appFwVersion: parseInt(appFwVersion, 10),
        mqttMessagesPrefix: mqttMessagesPrefix,
    });
    console.log(colors_1.cyan("connecting to " + colors_1.yellow(endpoint) + "..."));
    var client = mqttClient_1.mqttClient({
        id: deviceId,
        caCert: caCert,
        clientCert: clientCert,
        privateKey: privateKey,
        endpoint: endpoint,
    });
    var connectedOrReconnected = false;
    var notifyOfConnection = function (eventName) {
        if (!onConnect || connectedOrReconnected) {
            return;
        }
        console.log(colors_1.yellow("TRIGGERING ONCONNECT CALLBACK ON \"" + eventName + "\""));
        connectedOrReconnected = true;
        onConnect(deviceId, client);
    };
    client.on('error', function (error) {
        console.error("AWS IoT error " + error.message);
    });
    client.on('connect', function () { return __awaiter(_this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    console.log(colors_1.green('connected'));
                    notifyOfConnection('connect');
                    return [4 /*yield*/, waitForJobs(appFwVersion)];
                case 1:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    }); });
    client.on('message', function (topic, payload) {
        console.log(colors_1.magenta("< " + topic));
        var p = payload ? JSON.parse(payload.toString()) : {};
        console.log(colors_1.magenta("<"));
        console.log(colors_1.magenta(JSON.stringify(p, null, 2)));
        if (listeners[topic]) {
            listeners[topic]({
                topic: topic,
                payload: p,
            });
        }
        else {
            throw new Error("No listener registered for topic " + topic + "!");
        }
    });
    client.on('close', function () {
        console.error('disconnect');
    });
    client.on('reconnect', function () {
        console.log(colors_1.magenta('reconnect'));
        notifyOfConnection('reconnect');
    });
    var publish = function (topic, payload) {
        return new Promise(function (resolve, reject) {
            client.publish(topic, JSON.stringify(payload), undefined, function (error) {
                if (error) {
                    return reject(error);
                }
                console.log(colors_1.cyan("> " + topic));
                console.log(colors_1.blue(">"));
                console.log(colors_1.blue(JSON.stringify(payload, null, 2)));
                return resolve();
            });
        });
    };
    var sendMessage = function (timestamp, payload) { return __awaiter(_this, void 0, void 0, function () {
        var timeStamp;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    timeStamp = new Date(timestamp).toISOString();
                    console.debug("Timestamp " + timeStamp + " and messageId not included in message b/c the fw does not support it yet.");
                    return [4 /*yield*/, publish(mqttMessagesPrefix + "d/" + deviceId + "/d2c", payload)];
                case 1:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    }); };
    var subscribe = function (topic) {
        return new Promise(function (resolve, reject) {
            client.subscribe(topic, undefined, function (error, granted) {
                if (error) {
                    return reject(error);
                }
                console.log(colors_1.green("subscribed to " + colors_1.yellow(granted.map(function (_a) {
                    var topic = _a.topic;
                    return topic;
                }).join(', '))));
                return resolve();
            });
        });
    };
    Array.from(sensors.entries()).map(function (_a) {
        var name = _a[0], sensor = _a[1];
        return createService_1.createService(name, sensor, sendMessage).start();
    });
    var listeners = {};
    var registerListener = function (topic, callback) {
        listeners[topic] = callback;
    };
    var unregisterListener = function (topic) {
        delete listeners[topic];
    };
    var waitForJobs = function (appFwVersion) { return __awaiter(_this, void 0, void 0, function () {
        var job;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, reportAppFirmwareVersion(appFwVersion)];
                case 1:
                    _a.sent();
                    console.log(colors_1.green("reported firmware version " + colors_1.yellow(appFwVersion)));
                    return [4 /*yield*/, waitForNextUpdateJob()];
                case 2:
                    job = _a.sent();
                    console.log(job);
                    return [4 /*yield*/, acceptJob(job)];
                case 3:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    }); };
    var reportAppFirmwareVersion = function (fwVersion) { return __awaiter(_this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: 
                // Publish firmware version
                return [4 /*yield*/, publish(topics(deviceId).shadow.update._, {
                        state: {
                            reported: {
                                nrfcloud__dfu_v1__app_v: fwVersion,
                            },
                        },
                    })];
                case 1:
                    // Publish firmware version
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    }); };
    var waitForNextUpdateJob = function () {
        return new Promise(function (resolve) {
            var jobsNextTopic = topics(deviceId).jobs.notifyNext;
            registerListener(jobsNextTopic, function (_a) {
                var payload = _a.payload;
                if (!payload.execution) {
                    return;
                }
                unregisterListener(jobsNextTopic);
                resolve(payload.execution);
            });
            return subscribe(jobsNextTopic);
        });
    };
    var updateJob = function (job, expectedVersion, nextState, status) { return __awaiter(_this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, publish(topics(deviceId).jobs.update(job.jobId)._, {
                        status: status,
                        statusDetails: {
                            nextState: nextState,
                        },
                        expectedVersion: expectedVersion,
                        includeJobExecutionState: true,
                    })];
                case 1:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    }); };
    var updateJobProgress = function (job, expectedVersion, nextState) { return __awaiter(_this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, updateJob(job, expectedVersion, nextState, JobExecutionStatus.IN_PROGRESS)];
                case 1:
                    _a.sent();
                    console.log(colors_1.green("updated job " + colors_1.yellow(job.jobId) + " to in progress. nextState: " + nextState + "..."));
                    return [2 /*return*/];
            }
        });
    }); };
    var handleJobUpdateAccepted = function (job) { return function (_a) {
        var payload = _a.payload;
        return __awaiter(_this, void 0, void 0, function () {
            var _b;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0:
                        console.log('handleJobUpdateAccepted', {
                            job: job,
                            payload: payload,
                        });
                        if (payload.executionState.status !== JobExecutionStatus.IN_PROGRESS) {
                            return [2 /*return*/];
                        }
                        _b = payload.executionState.statusDetails.nextState;
                        switch (_b) {
                            case DfuStatus.downloadFirmware: return [3 /*break*/, 1];
                            case DfuStatus.applyUpdate: return [3 /*break*/, 3];
                        }
                        return [3 /*break*/, 6];
                    case 1:
                        console.log(colors_1.magenta("Skipping downloading the firmware from " + colors_1.yellow(job.jobDocument.location) + "..."));
                        return [4 /*yield*/, updateJobProgress(job, payload.executionState.versionNumber, DfuStatus.applyUpdate)];
                    case 2:
                        _c.sent();
                        return [3 /*break*/, 6];
                    case 3:
                        console.log(colors_1.magenta("Skipping applying the firmware update to " + colors_1.yellow(job.jobDocument.fwversion) + "..."));
                        return [4 /*yield*/, updateJob(job, payload.executionState.versionNumber, DfuStatus.none, JobExecutionStatus.SUCCEEDED)];
                    case 4:
                        _c.sent();
                        // handle next job
                        return [4 /*yield*/, waitForJobs(job.jobDocument.fwversion)];
                    case 5:
                        // handle next job
                        _c.sent();
                        _c.label = 6;
                    case 6: return [2 /*return*/];
                }
            });
        });
    }; };
    var acceptJob = function (job) { return __awaiter(_this, void 0, void 0, function () {
        var jobUpdateAcceptedTopic;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    jobUpdateAcceptedTopic = topics(deviceId).jobs.update(job.jobId)
                        .accepted;
                    return [4 /*yield*/, subscribe(jobUpdateAcceptedTopic)];
                case 1:
                    _a.sent();
                    return [4 /*yield*/, registerListener(jobUpdateAcceptedTopic, handleJobUpdateAccepted(job))];
                case 2:
                    _a.sent();
                    return [4 /*yield*/, updateJobProgress(job, job.versionNumber, DfuStatus.downloadFirmware)];
                case 3:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    }); };
    return {
        subscribe: subscribe,
        publish: publish,
        registerListener: registerListener,
        unregisterListener: unregisterListener,
        run: function (args) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) {
            return [2 /*return*/, waitForJobs(args.appFwVersion)];
        }); }); },
    };
};
