#!/usr/bin/env node
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

'use strict';

// Native packages
var fs = require('fs');
var path = require('path');

// External dependencies
var program = require('commander');
var prettyjson = require('prettyjson');

// Azure IoT SDK dependencies
var DeviceClient = require('azure-iot-device').Client;
var DeviceConnectionString = require('azure-iot-device').ConnectionString;
var Registry = require('azure-iothub').Registry;

// Local dependencies
var packageJson = require('./package.json');
var inputError = require('./common.js').inputError;
var printSuccess = require('./common.js').printSuccess;
var serviceError = require('./common.js').serviceError;
var printErrorAndExit = require('./common.js').printErrorAndExit;
var getSas = require('./common.js').getSas;
var getHostFromSas = require('./common.js').getHostFromSas;
var createDeviceConnectionString = require('./common.js').createDeviceConnectionString;
var createMessageFromArgument = require('./common.js').createMessageFromArgument;
var showDeprecationText = require('./common.js').showDeprecationText;

showDeprecationText('az iot device simulate');


program
  .version(packageJson.version)
  .description('Simulate a device.')
  .option('--device-connection-string <device-connection-string>', 'connection string to use for the device')
  .option('-l, --login <iothub-connection-string>', 'use the connection string provided as argument to use to authenticate with your IoT Hub instance')
  .option('--protocol <amqp|amqp-ws|http|mqtt|mqtt-ws>', 'protocol used to send and receive messages (defaults to amqp)', verifyProtocol)
  .option('--send [message]', 'send a test message as a device. If the message is not specified, a default message will be used')
  .option('--send-interval <interval-in-milliseconds>', 'interval to use between each message being sent (defaults to 1000ms)', parseInt)
  .option('--send-count <message-count>', 'number of messages to send', parseInt)
  .option('--receive', 'Receive cloud-to-device (C2D) messages as a device')
  .option('-v, --verbose', 'shows all the information contained in the message received, including annotations and properties')
  .option('--receive-count <message-count>', 'number of C2D messages to receive', parseInt)
  .option('--settle <complete|abandon|reject>', 'indicate how the received C2D messages should be settled (defaults to \'complete\')', verifySettle)
  .option('--upload-file <file-path>', 'upload a file from the simulated device')
  .parse(process.argv);

function verifySettle(arg) {
  if (!!arg && arg !== 'complete' && arg !== 'reject' && arg !== 'abandon') {
    inputError('--settle can take only take one of the following values: \'complete\', \'reject\' or \'abandon\'');
  } else {
    return arg;
  }
}

function verifyProtocol(arg) {
  var lowerCaseArg = arg.toLowerCase();
  if (lowerCaseArg !== 'amqp' && lowerCaseArg !== 'amqp-ws' && lowerCaseArg !== 'http' && lowerCaseArg !== 'mqtt' && lowerCaseArg !== 'mqtt-ws') {
    inputError('--protocol can take only take one of the following values: \'amqp\', \'amqp-ws\', \'mqtt\', \'mqtt-ws\' or \'http\'');
  } else {
    return lowerCaseArg;
  }
}

var sas = getSas(program.login);

if(!program.deviceConnectionString && !sas) {
  inputError('You must specify the device connection string (--device-connection-string) or the IoT Hub connection string (--login), or use the \'login\' command first.');
}

if(!program.deviceConnectionString && !program.args[0]) {
  inputError('You must specify either a device connection string (--device-connection-string) or the IoT Hub connection string and a device id as first argument');
}

if (!program.send && !program.receive && !program.uploadFile) {
  inputError('Nothing to do: please use --send, --receive or --uploadFile');
}

var settleMethod = program.settle || 'complete';

var protocolArg = program.protocol || 'amqp';
var Protocol;
switch(protocolArg) {
  case 'amqp-ws':
    Protocol = require('azure-iot-device-amqp').AmqpWs;
    break;
  case 'http':
    Protocol = require('azure-iot-device-http').Http;
    break;
  case 'mqtt':
    Protocol = require('azure-iot-device-mqtt').Mqtt;
    if (settleMethod !== 'complete') {
      inputError('Cannot ' + settleMethod + ' messages with MQTT: messages are automatically completed.');
    }
    break;
  case 'mqtt-ws':
    Protocol = require('azure-iot-device-mqtt').MqttWs;
    if (settleMethod !== 'complete') {
      inputError('Cannot ' + settleMethod + ' messages with MQTT: messages are automatically completed.');
    }
    break;
  default:
    Protocol = require('azure-iot-device-amqp').Amqp;
    break;
}

var sendInterval = program.sendInterval || 1000;
var sendCount = program.sendCount || Number.MAX_SAFE_INTEGER;
var receiveCount = program.receiveCount || Number.MAX_SAFE_INTEGER;
var uploadFilePath = program.uploadFile;

var deviceConnectionString;
var deviceId = program.args[0];
if (!deviceId) {
  deviceConnectionString = program.deviceConnectionString;
  if(!deviceConnectionString) {
    inputError('You must specify either a device connection string (--device-connection-string) or the IoT Hub connection string and a device id as first argument');
  } else {
    deviceId = DeviceConnectionString.parse(deviceConnectionString).DeviceId;
    simulateDevice();
  }
} else {
  var registry = Registry.fromSharedAccessSignature(sas.toString());
  registry.get(deviceId, function(err, deviceInfo) {
    if (err) serviceError(err);
    else {
      var host = getHostFromSas(sas.toString());
      deviceConnectionString = createDeviceConnectionString(deviceInfo, host);
    }

    simulateDevice();
  });
}

function simulateDevice() {
  if (!deviceConnectionString) throw new Error('Couldn\'t figure out device connection string');
  if (!Protocol) throw new Error('Couldn\'t figure out protocol to connect to IoT Hub');

  var sendRunning = !!program.send;
  var receiveRunning = !!program.receive;
  var uploadRunning = !!program.uploadFile;

  var client = DeviceClient.fromConnectionString(deviceConnectionString, Protocol);
  client.open(function(err) {
    if (err) serviceError('Could not connect as device: ' + err.message);
    if (program.send) {
      var sendCounter = 0;

      var sendItv = setInterval(function() {
        var message = program.send === true ? createMessageFromArgument('Simulated message: #' + sendCounter) : createMessageFromArgument(program.send);
        client.sendEvent(message, function(err) {
          if (err) serviceError(err);
          else {
            printSuccess('Message #' + sendCounter + ' sent successfully');
            sendCounter++;
            if (sendCounter === sendCount) {
              sendRunning = false;
              clearInterval(sendItv);
            }
          }
        });
      }, sendInterval);
    }

    if (program.receive) {
      var receiveCounter = 0;
      var onMessage = function(msg) {
        printSuccess('==================');
        printSuccess('Message received:');
        console.log(prettyjson.render(msg.data.toString()));

        if (program.verbose) {
          console.log('user-id: ' + msg.userId);
          console.log('message-id: ' + msg.messageId);
          console.log('correlation-id: ' + msg.correlationId);
        }

        if (msg.properties.count() > 0) {
          printSuccess('--- properties ---');
          msg.properties.propertyList.forEach(function(prop) {
            console.log(prop.key + ': ' + prop.value);
          });
        }
        printSuccess('==================');
        receiveCounter++;
        if (receiveCounter === receiveCount) {
          receiveRunning = false;
          client.removeListener('message', onMessage);
        }
        client[settleMethod](msg, function(err) {
          if (err) serviceError('Could not ' + settleMethod + ' message: ' + err.message);
          else {
            printSuccess(settleMethod + ' message: Success');
          }
        });
      };

      client.on('message', onMessage);
    }

    if (uploadFilePath) {
      fs.stat(uploadFilePath, function (err, fileStats) {
        if (err) inputError('Cannot find: ' + program.uploadFile);
        var fileStream = fs.createReadStream(uploadFilePath);

        client.uploadToBlob(path.basename(uploadFilePath), fileStream, fileStats.size, function (err) {
          if (err) {
            printErrorAndExit('Cannot upload file: ' + err.constructor.name + ': ' + err.message);
          } else {
            printSuccess('Upload successful');
          }
          fileStream.destroy();
          uploadRunning = false;
        });
      });
    }

    var taskInterval = setInterval(function() {
      if (!sendRunning && !receiveRunning && !uploadRunning) {
        printSuccess('Device simulation finished.');
        client.close(function(err) {
          if (err) serviceError(err);
          else {
            clearInterval(taskInterval);
            process.exit(0);
          }
        });
      }
    }, 200);
  });
}