//
// Copyright (c) Microsoft and contributors.  All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//

/**
 * Implements uploading of a page blob featuring
 * - conversion of non-fixed VHDs to fixed VHDs on the fly
 * - multiple parameter-controlled simultaneous uploads
 * - fast speed
 * - simultaneous MD5 hash computation (if CPU is not fully utilized by upload)
 * - error retry
 * - incremental upload
 * - support for using base (aka parent) VHD in the cloud to speed up difference VHD uploads
 */

var assert = require('assert');
var fs = require('fs');
var util = require('util');
var crypto = require('crypto');
var azureCommon = require('azure-common');
var azureStorage = require('azure-storage');

var jobTracker = require('./jobTracker');
var intSet = require('./intSet');
var BitArray = require('../../../../util/bitArray');
var blobInfo = require('./blobInfo');
var callbackAggregator = require('../../../../util/callbackAggregator');
var utils = require('../../../../util/utils');

var blobUtils = require('../../../../util/blobUtils');
var splitDestinationUri = blobUtils.splitDestinationUri;
var splitBlobResourceName = blobUtils.splitBlobResourceName;
var BlobConstants = azureCommon.Constants.BlobConstants;

var http = require('http');
http.globalAgent.maxSockets = Math.max(2048, http.globalAgent.maxSockets);

// compare arrays, strings or buffers
function compare(a, b) {
  if (!a && !b) {
    return true;
  }
  if (!a || !b || a.length !== b.length) {
    return false;
  }
  for (var i = 0; i < a.length; ++i) {
    if (a[i] !== b[i]) {
      return false;
    }
  }
  return true;
}

// implement right-justification for formatted output
function extendLeftTo(string, size, fixed) {
  var str = fixed !== undefined ? string.toFixed(fixed) : string.toString();
  var newStr = '              ' + str;
  return newStr.slice(-Math.max(size, str.length));
}

function findNonZeroBlocks(fd, blockSize, callback) {
  var fileSize = fs.fstatSync(fd).size;
  var numBlocks = Math.ceil(fileSize / blockSize);
  
  var nonZeroBlocks = new BitArray(numBlocks);
  var buf = new Buffer(blockSize);
  readFileBlock(0);

  function readFileBlock(blockIndex) {
    var fileOffset = blockIndex * blockSize;
    var len = Math.min(blockSize, fileSize - fileOffset);
    if (len <= 0)
      return callback(null, nonZeroBlocks);

    fs.read(fd, buf, 0, len, fileOffset, readCallback);

    function readCallback(err, bytesRead) {
      if (err)
        return callback(err);

      var isNonZero = false;
      for (var i = 0; i < bytesRead; i++) {
        if (buf[i]) {
          isNonZero = true;
          break;
        }
      }

      if (isNonZero || bytesRead == len) {
        nonZeroBlocks.set(blockIndex, isNonZero);
        readFileBlock(blockIndex + 1);
      } else {
        len -= bytesRead;
        fileOffset += bytesRead;
        fs.read(fd, buf, 0, len, fileOffset, readCallback);        
      }
    }
  }
}

exports.copyBlob = function copyBlob(sourceUri, sourceAccountKey, destinationUri, destinationAccountKey, callback) {

  var splitDestination = splitDestinationUri(destinationUri);
  if (!splitDestination.accountName || !splitDestination.blobName) {
    throw new Error('Destination is not well formed :' + destinationUri);
  }

  if (sourceAccountKey) {
    sourceUri = signBlobUri(sourceUri, sourceAccountKey);
  }

  // Using default host provided by Node SDK if not specified
  var blobService = utils.createBlobService(splitDestination.accountName, destinationAccountKey, splitDestination.host);
  var container = splitDestination.container || '$root';
  blobService.createContainerIfNotExists(container, function(error1, containerCreated, response1) {
    if (error1) {
      callback(error1, null, response1);
      return;
    }

    blobService.startCopyBlob(sourceUri, container, splitDestination.blobName, checkStatus);
    function checkStatus(error, blob, response) {
      if (!error) {
        var status = blob.copy.status;
        var copyStatusMessage = '  Copying image\r';
        if (response.headers && response.headers.hasOwnProperty('x-ms-copy-progress')) {
          var copyProgress = response.headers['x-ms-copy-progress'].split('/');
          if (copyProgress.length == 2) {
            if (copyProgress[0] > 0 && copyProgress[1] > 0) {
              copyStatusMessage = '  Copying image: ' +
                Math.floor((copyProgress[0]*100.0 / copyProgress[1])) + '% (' +
                response.headers['x-ms-copy-progress'] + ') \r';
            }
          }
        }

        fs.writeSync(1, copyStatusMessage);
        if (status.toLowerCase() !== 'success') {
          setTimeout(function() {
            blobService.getBlobProperties(container, splitDestination.blobName, checkStatus);
          }, 3000);
          return;
        }
      }
      callback(error, blob, response);
    }
  });
};

var signBlobUri = exports.signBlobUri = function signBlobUri(uri, storageAccountKey) {
  var splitUri = splitDestinationUri(uri);
  if (!splitUri.accountName || !splitUri.blobName) {
    throw new Error('The uri is not well formed :' + uri);
  }

  var leftPadTwo = function (n) {
    return (n < 10 ? '0' : '') + n;
  };

  function _toUTC(date) {
    return (date.getUTCFullYear()+'-' +
      leftPadTwo((date.getUTCMonth()+1))+'-' +
      leftPadTwo(date.getUTCDate())+'T' +
      leftPadTwo(date.getUTCHours())+':' +
      leftPadTwo(date.getUTCMinutes())+':' +
      leftPadTwo(date.getUTCSeconds())+'Z');
  }

  if (storageAccountKey) {
    var startDate = new Date();
    startDate.setMinutes(startDate.getMinutes() - 2);
    var expiryDate = new Date(startDate);
    expiryDate.setMinutes(startDate.getMinutes() + 60);
    var blobService = azureStorage.createBlobService(splitUri.accountName, storageAccountKey);
    var sharedAccessPolicy = {
        Id: null,
        AccessPolicy: {
          Permissions: BlobConstants.SharedAccessPermissions.READ,
          Start: _toUTC(startDate),
          Expiry: _toUTC(expiryDate)
        }
      };
    var queryString = blobService.generateSharedAccessSignature(splitUri.container, splitUri.blobName, sharedAccessPolicy);
    uri = uri + '?' + queryString;
  }

  return uri;
};

// UploadPageBlob()
//
// destinationUri may or may not contain the full path
// http:// prefix is optional
// Example:
//
// myaccount/myfilename.ext
// http://myaccout/myDir/myfile
// http://myAccount.blob.core.azure-preview.com/mydir/myfile
//
// port number and GET parameters are not supported
exports.uploadPageBlob = function uploadPageBlob(destinationUri, accountKey, fileName, options, callback) {

  var splitDestination = splitDestinationUri(destinationUri);
  if (!splitDestination.accountName) {
    throw new Error('Destination is not well formed :' + destinationUri);
  }

  var blobService = utils.createBlobService(splitDestination.accountName, accountKey, splitDestination.host);
  uploadPageBlobFromBlobService(blobService, splitDestination.resourceName, fileName, options, callback);
};

exports.deleteBlob = function deleteBlob(destinationUri, accountKey, callback) {
  var splitDestination = splitDestinationUri(destinationUri);
  if (!splitDestination.accountName) {
    throw new Error('Destination is not well formed :' + destinationUri);
  }

  var blobService = utils.createBlobService(splitDestination.accountName, accountKey, splitDestination.host);
  var splitName = splitBlobResourceName(splitDestination.resourceName);
  blobService.deleteBlob(splitName.container, splitName.blobName, callback);
};

var uploadPageBlobFromBlobService =
  exports.uploadPageBlobFromBlobService =
    function uploadPageBlobFromBlobService(blobService, blobResourceName, fileName, options, callback) {

  var logger = options.logger || console;
  var logFunc = logger.info || console.log;
  var warnFunc = logger.warn || console.warn;
  var errorFunc = logger.error || console.error;
  var doNothing = function() {};
  var verboseFunc = logger.verbose || (options.verbose ? logFunc : doNothing);
  var sillyFunc =  logger.silly || doNothing;
  var exitWithError = options.exitWithError || function() {
    throw new Error(util.inspect(arguments));
  };

  var skipLine = options.skipLine || function() {
    fs.writeSync(1, '\n'); // just like console.log('');
  };

  verboseFunc('Uploading page blob ' + fileName);
  verboseFunc(' to ' +
      blobService.protocol + blobService.storageAccount + '/' + blobService.host +
      '/' + blobResourceName);
  var startTime = new Date();

  if (!callback) {
    callback = function(error) {
      if (error) {
        errorFunc(util.inspect(error));
      }
    };
  }

  var numThreads = options.threads || 128;
  numThreads = Math.floor(numThreads);
  if (numThreads < 1 || numThreads.toString() === 'NaN') {
    throw new Error('Number of parallel connections should be a positive number : '+ options.threads);
  }

  var splitName = splitBlobResourceName(blobResourceName);
  var container = splitName.container;
  var blobName = splitName.blobName;

  var size = fs.statSync(fileName).size;
  verboseFunc('Max parallel connections = ' + numThreads);
  verboseFunc('File size = ' + size);
  if (size === 0) {
    throw new Error('File cannot be read or has zero size: ' + fileName);
  }
  if (size % 512) {
    throw new Error('file size is not a multiple of 512 (not yet implemented) :' + size);
  }

  var blockSize = Math.min(2 * 1024 * 1024, BlobConstants.DEFAULT_WRITE_BLOCK_SIZE_IN_BYTES);
  blockSize -= (blockSize % 512);
  var dynVHD = false;
  var uploadSize = size;
  var uploadSizeWithFooter = uploadSize;
  var vhdInfo = null;
  if (options.vhd) {
    vhdInfo = require('./vhdTools').getVHDInfo(fileName);
    if (vhdInfo.ddHeader) { // dynamic or difference
      dynVHD = true;
      size = vhdInfo.footer.currentSize;
      if (vhdInfo.blocksSize !== size) {
        warnFunc('The total size of blocks ' + vhdInfo.blocksSize / 1024 / 1024 + ' Mb is different');
        warnFunc('  from declared VHD size ' + size / 1024 / 1024 + ' Mb');
        size = vhdInfo.blocksSize;
      }
      size += 512; // add footer
      verboseFunc('Dynamic or difference VHD: converting to fixed type');
      verboseFunc('Fixed VHD size = ' + size / 1024 + ' KB');
      var curSize = blockSize;
      blockSize = vhdInfo.ddHeader.blockSize; // should be vhdInfo.ddHeader.blockSize, should divide it
      while (blockSize > curSize) {
        blockSize /= 2;
      }

      if (vhdInfo.fullIndices) {
        uploadSize = vhdInfo.fullIndices.length * vhdInfo.ddHeader.blockSize;
        uploadSizeWithFooter = uploadSize + 512; // add footer
      } else {
        warnFunc('Warning: difference VHD with fixed [grand]parent ' + fileName);
        uploadSize -= 512; // without footer
      }
    }

    var sizeMB = size / 1024 / 1024;
    logFunc('VHD size : ' + ((sizeMB >= 10240) ?
      (sizeMB / 1024).toFixed() + ' GB'
      : sizeMB.toFixed() + ' MB'));
  }

  logFunc('Uploading ' + uploadSizeWithFooter / 1024 + ' KB');

  while (uploadSize / blockSize < numThreads / 2 && blockSize > 128 * 1024) {
    blockSize /= 2;
  }

  verboseFunc('Blob container = ' + container);
  verboseFunc('Blob name = ' + blobName);
  verboseFunc('VHD block size = ' + blockSize / 1024 + ' KB');

  var sizeExt = size + 511 - (size + 511) % 512;

  var maxIndex = Math.ceil((uploadSize / blockSize) - 1);
  var uploadBlocksPerBATBlocks = dynVHD ? vhdInfo.ddHeader.blockSize / blockSize : null;

  var maxRequest = maxIndex + dynVHD;

  // check if we need incremental upload

  var uploadedRegions = null;
  // need to finish two things to succeed: md5 (sometimes) and upload (always)
  // use callbackAggregator to track it, without tracking errors for upload
  var md5Agg = callbackAggregator.callbackAndAggregator(function(finished) {
    var alreadyExisted = finished.upload[1]; // arg 1
    verboseFunc(alreadyExisted ? 'Done - nothing to upload.' : 'Done!');
    callback(null, alreadyExisted);
  });

  var md5AggUpload = md5Agg.getCallback('upload');

  var skipMd5 = options.skipMd5;

  if (options.force) {
    // create a container if does not exist only
    blobInfo.getBlobInfo(blobService, container, null, null, true, onGotBlobInfo);
  } else {
    // see if we can do incremental upload
    blobInfo.getBlobInfo(blobService, container, blobName, {start : size - 512, end : size - 1}, true, onGotBlobInfo);
  }

  function onGotBlobInfo(error, result) {
    if (error) {
      errorFunc('Cannot access blob /' + blobResourceName +
          ' or its properties : ' + error.code);
      exitWithError(error);
    } else {
      if (result) {
        uploadedRegions = result.pageRegions;
        if (uploadedRegions && uploadedRegions.length === 0) {
          uploadedRegions = null;
        }
        if (uploadedRegions && result.blobProperties &&
            result.blobProperties.contentLength == size) {
          sillyFunc(''); // skip line for visibility in 'silly' mode
          verboseFunc('Uploading incrementally');
          if (result.blobProperties && result.blobProperties.contentMD5) {
            verboseFunc('Using pre-set MD5 = ' + result.blobProperties.contentMD5);
            skipMd5 = true;
          }
          // check footer (if vhd)
          if (dynVHD) {
            if (!compare(vhdInfo.footer.buffer, result.data)) {
              exitWithError((result.data && result.data.length > 0 ? '' : 'Couldn\'t read cloud blob footer. ') +
                  'Files are different - cannot upload incrementally. Use -f option to overwrite.');
            }
          }
          onBlobCreated();
          return;
        }
      }
      uploadedRegions = null;
    }
    if (options.parentBlob) {
      verboseFunc('Parent blob: ' + options.parentBlob);
      if (!vhdInfo || !vhdInfo.isDiff) {
        exitWithError('Error: base VHD specified for non-difference type VHD: type = ' + vhdInfo.footer.diskType);
      }
      createDiffVHDPageBlob(blobService, container, blobName, options.parentBlob, options.threads, vhdInfo, onBlobCreated);
    } else {
      blobService.createPageBlob(container, blobName, sizeExt,
        function onCreate(error) {
          if (error) {
            var errorCode = typeof(error) === 'object' ? error.code : undefined;
            errorFunc('Error: blobService.createPageBlob() ended with error code = ' + errorCode);
            if (!errorCode) errorFunc(util.inspect(error));
            callback(error);
            return;
          }
          onBlobCreated();
        });
    }
  }

  function createDiffVHDPageBlob(blobService, container, blobName, baseUri, threads, vhdInfo, callback) {
    var splitParentUri = splitDestinationUri(baseUri);
    blobService.startCopyBlob(splitParentUri.container, splitParentUri.blobName, container, blobName, function(error) {
      if (error) {
        errorFunc('Cannot copy or invalid blob: ' + baseUri + '\nError code ' + error.code);
        errorFunc(util.inspect(error));
        return;
      }
      var footerRegion = {start : size - 512, end : size - 1};
      blobInfo.getBlobInfo(blobService, container, blobName, null, false, function(error, result) {
        if (error || !result) {
          errorFunc(util.inspect(error));
          callback(error); // nothing to do here
          return;
        }
        var deleteRegions = [footerRegion];
        uploadedRegions = result.pageRegions;
        var regionsSet = intSet.createIntSet(uploadedRegions); // this does not copy regions!
        var blockSize = vhdInfo.ddHeader.blockSize;

        function process(index) {
          var start = blockSize * index;
          var end = start + blockSize - 1;
          if (regionsSet.intersects(start, end)) {
            // For simplicity and possibly speed,
            // delete the whole block, rather then some uploaded parts
            deleteRegions.push({start : start, end : end});
          }
        }

        if (vhdInfo.bat) { // This is not a Windows file name! :) 'bat' stands for VHD 'Block Allocation Table'
          for (var j in vhdInfo.bat) {
            process(j);
          }
        } else {
          for(var jj = vhdInfo.ddHeader.maxTableEntries; --jj >= 0;) {
            process(jj);
          }
        }

        threads = threads || 128;
        threads = Math.min(threads, deleteRegions.length);
        var next = threads;
        var running = threads;
        for (var i = 0;  i < threads; ++i) {
          deleteRegion(i);
        }
        var exiting = false;

        function deleteRegion(i) {
          if (exiting) {
            return;
          }
          blobService.clearPageRange(container, blobName, deleteRegions[i].start, deleteRegions[i].end, function(error) {
            if (error) {
              errorFunc('Cannot delele blob regions ' +
                  util.inspect(deleteRegions[i]));
              errorFunc(util.inspect(error));
              exiting = true;
              callback(error);
              return;
            }
            regionsSet.subtractInterval(deleteRegions[i].start, deleteRegions[i].end);
            next++;
            if (next >= deleteRegions.length) {
              if (--running <= 0) {
                verboseFunc('Successfully copied and cleaned up base VHD');
                onDeleted();
              }
              return;
            }
            deleteRegion(next);
          });
        }

        function onDeleted() {
          if (result.blobProperties && result.blobProperties.contentMD5) {
            result.blobProperties.contentMD5 = null; // clear it from the blob
            var blobOptions = { contentMD5: null, contentType: 'application/octet-stream' };
            blobService.setBlobProperties(container, blobName, blobOptions, function (error) {
              if (error) {
                skipLine();
                exitWithError(error);
              }
              callback(null, result);
            });
          } else {
            callback(null, result);
          }
        }
      });
    });
  }

  var md5Done = 0;
  var exiting = false;

  var md5AggMd5 = null;

  function calculateMD5(callback) {
    md5AggMd5 = md5Agg.getCallback('md5');
    var md5hash = crypto.createHash('md5');
    var stream =
      options.vhd ? vhdInfo.getReadStream() : fs.ReadStream(fileName);
    stream.on('data', function (data) {
      if (exiting) {
        verboseFunc('Waiting to finish...');
        stream.destroy();
        return;
      }
      md5hash.update(data);
      md5Done += data.length;
    });
    stream.on('end', function () {
      if (exiting) {
        return;
      }
      var hash = md5hash.digest('base64');
      callback(hash);
    });
    stream.on('error', function (e) {
      callback(null, e);
    });
  }

  function onMD5Calculated(md5, error) {
    if (error) {
      errorFunc('Cannot calculate MD5 or read file ' + fileName);
      exitWithError(error);
    }
    var blobOptions = { contentMD5: md5, contentType: 'application/octet-stream' };
    blobService.setBlobProperties(container, blobName, blobOptions, function (error) {
      if (error) {
        skipLine();
        errorFunc(' Error in setBlobProperties() ');
        errorFunc(util.inspect(error));
      } else  if (options.verbose) {
        skipLine(); // do not write over progress messages
        verboseFunc('MD5 hash is computed and written to the blob: ' + md5);
      }
      md5AggMd5(error);
    });
  }

  function onBlobCreated(error) {
    if (error) {
      errorFunc('Cannot create or copy a blob');
      errorFunc(util.inspect(error));
      callback(error);
      return;
    }
    // initiate MD5 calculation
    if (!skipMd5) {
      sillyFunc('Starting to calculate MD5 hash for input file (use -m to skip it)');
      calculateMD5(onMD5Calculated);
    }

    var doneThreads = 0;
    var totalBlocks = maxRequest + 1;
    var tracker = jobTracker.getJobTracker(totalBlocks);
    var wasIdle = false;

    numThreads = Math.min(numThreads, maxRequest + 1);
    var runningThreads = 0;
    var alreadyDone = 0; // previously uploaded blocks - to adjust speed computation and check if anything was uploaded

    var fd;
    var nonZeroBlockSize = Math.max(2 * 1024 * 1024, blockSize);
    assert(nonZeroBlockSize % blockSize === 0);
    var nonZeroBlocks;

    if (dynVHD) {
      startThreads();
    } else {
      fd = fs.openSync(fileName, 'r');

      verboseFunc('Detecting the empty data blocks in the local file...');
      findNonZeroBlocks(fd, nonZeroBlockSize, function(error, res) {
        if (error) {
          errorFunc('Error while trying to detect non-zero blocks');
          errorFunc(util.inspect(error));
          callback(error);
          return;
        }
        verboseFunc('Detecting the empty data blocks completed.');
        nonZeroBlocks = res;
        startThreads();
      });
    }

    function startThreads() {
      for(var num = 0; num < numThreads && !tracker.isDone(); ++num) {
        runningThreads++;
        next(num);
      }
    }

    function printStatus() {
      var percentRequested = 100 * tracker.getRequestedPart();
      var percentCompleted = 100 * tracker.getCompletedPart();
      var doneBlocks = tracker.getCompleted();
      var speed = ' ';
      var uploadedBlocks = Math.max(doneBlocks - alreadyDone, 0);
      if ((doneBlocks >= numThreads || percentCompleted > 90) && uploadedBlocks > 0) {
        var elapsedTime = (new Date() - startTime) / 1000;
        var kbps = (blockSize / 1024 * uploadedBlocks) / elapsedTime;
        var timeMinSec = (elapsedTime % 60).toFixed();
        if (elapsedTime >= 60) {
          timeMinSec = Math.floor(elapsedTime / 60) + 'm' + extendLeftTo(timeMinSec, 2);
        }
        speed = ' Time:' + extendLeftTo(timeMinSec, 5) + 's Speed:' + extendLeftTo(kbps, 6, 0) + ' KB/s';
      }
      var runningRequests = runningThreads - doneThreads;
      var string =
        'Requested:' + extendLeftTo(percentRequested, 5, 1) + '%' +
        ' Completed:' + extendLeftTo(percentCompleted, 5, 1) + '%' +
        ' Running:' + extendLeftTo(runningRequests, 4) +
        speed + ' \r';
      // Write to stdout (fd is 1)
      // console.log() cannot be used because it will not pass '\r' as it it (will do '\n')
      fs.writeSync(1, string);
    }

    function next(number) {
      var index = tracker.nextIndex();
      if (index === null || index === undefined) {
        wasIdle = true;
        doneThreads++;
        printStatus();
        if (doneThreads == runningThreads) {
          skipLine();
          if (md5Agg.isInProgressFor('md5')) {
            logFunc('Finishing computing MD5 hash, ' + (md5Done * 100 / size).toFixed() +'% is complete.');
          }

          md5AggUpload(null, alreadyDone === tracker.getCompleted());
        }
        return;
      }

      function toNext(error, pageBlob, response) {
        // To avoid possible deep recursion, Use setImmediate instead of invoking next() directly
        // Use process.nextTick in older versions of Node, where setImmediate is not available
        var reschedule = global.setImmediate || process.nextTick;
        
        if (exiting) {
          return; // do not display all errors
        }
        if (error) {
          if (options.verbose) {
            skipLine(); // skip progress line
            warnFunc(number + '> Error at upload index ' + index + ' - will retry. ');
          }
          // on errors, remove one 'thread' on every other error
          // except when we are done with sending requests - in which case that some 'threads' are likely already finished
          if (tracker.error(index) % 2 && (!wasIdle || tracker.getErrorRate() > 0.95)) { // always go there for now
            doneThreads++;
            if (doneThreads == numThreads) {
              printStatus();
              skipLine();
              errorFunc('Error in blobService.createBlobPagesFrom...()');
              errorFunc(util.inspect(error));
              if (response) errorFunc('response=' + util.inspect(response));
              exiting = true;
              callback(error);
              return;
            }
            printStatus();
          } else {
            reschedule(function() {next(number);});
          }
          return;
        }
        tracker.done(index);

        reschedule(function() {next(number);});
      }

      if (exiting) {
        return;
      }
      uploadAtIndex(index ? index - 1 : maxRequest); // change indices to start uploading from the footer, in both dynamic and fixed VHD cases, then continue from the beginning
      printStatus();

      function isAlreadyUploaded(start, end) {
        if (uploadedRegions) {
          var set = intSet.createIntSet(start, end);
          set.subtractRanges(uploadedRegions);
          return set.isEmpty();
        }
        return false;
      }

      function uploadAtIndex(index) {
        if (index > maxIndex) { // create a footer
          vhdInfo.footer.convertToFixed();
          if (!size || size % 512) {
            errorFunc('Incorrect file size - should be a multiple of 512: ' + size + '\n size % 512 =' + size % 512);
          }
          if (vhdInfo.footer.buffer.length !== 512) {
            errorFunc('Error: footer length (should be 512) =' + vhdInfo.footer.buffer.length);
          }
          // check if already uploaded
          if (isAlreadyUploaded(size - 512, size - 1)) {
            // nothing to do here
            alreadyDone++;
            toNext();
            return;
          }
          // don't use string object, either by using Buffer.toString() or manually - this won't work
          blobService.createPagesFromStream(container, blobName,
            vhdInfo.footer.buffer, size - 512, size - 1, toNext);
        } else {
          var posIn = index * blockSize;
          var posOut = posIn;
          if (dynVHD) {
            var batIndex = Math.floor(index / uploadBlocksPerBATBlocks);
            posIn = vhdInfo.getDynVhdOffset(posOut);
            if (vhdInfo.fullIndices) {
              posOut = vhdInfo.fullIndices[batIndex] * vhdInfo.ddHeader.blockSize + (index % uploadBlocksPerBATBlocks) * blockSize;
            }
          } else {
            // check if all zeroes (fixed vhd)
            var nonZeroBlockIndex = Math.floor(index * blockSize / nonZeroBlockSize);
            if (!nonZeroBlocks.get(nonZeroBlockIndex)) {
              // nothing to do here
              alreadyDone++;
              toNext();
              return;
            }
          }

          var realEndPosIn = Math.min(posIn + blockSize, size);
          realEndPosIn--;
          var realEndPosOut = Math.min(posOut + blockSize, size);
          realEndPosOut--;

          // check if already uploaded
          if (isAlreadyUploaded(posOut, realEndPosOut)) {
            // nothing to do here
            alreadyDone++;
            toNext();
            return;
          }

          var readStream;
          if (dynVHD) {
            readStream = vhdInfo.getReadStream({start: posOut, end: realEndPosOut});
          } else {
            readStream = fs.createReadStream(fileName,
              {fd: fd, autoClose: false, start: posIn, end: realEndPosIn});
          }

          if (!readStream || !size) {
            callback('Cannot create read stream');
            throw new Error('Cannot create read stream');
          }

          if (posOut % 512 !== 0) {
            errorFunc(posOut + ': posOut % 512 =' + posOut % 512);
          }

          blobService.createPagesFromStream(container, blobName,
            readStream, posOut, realEndPosOut, toNext);
        }
      }
    }
  }
};
