"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.DifferentialDownloader = void 0;

function _bluebirdLst() {
  const data = require("bluebird-lst");

  _bluebirdLst = function () {
    return data;
  };

  return data;
}

function _builderUtilRuntime() {
  const data = require("builder-util-runtime");

  _builderUtilRuntime = function () {
    return data;
  };

  return data;
}

function _fsExtraP() {
  const data = require("fs-extra-p");

  _fsExtraP = function () {
    return data;
  };

  return data;
}

function _DataSplitter() {
  const data = require("./DataSplitter");

  _DataSplitter = function () {
    return data;
  };

  return data;
}

function _url() {
  const data = require("url");

  _url = function () {
    return data;
  };

  return data;
}

function _downloadPlanBuilder() {
  const data = require("./downloadPlanBuilder");

  _downloadPlanBuilder = function () {
    return data;
  };

  return data;
}

function _multipleRangeDownloader() {
  const data = require("./multipleRangeDownloader");

  _multipleRangeDownloader = function () {
    return data;
  };

  return data;
}

class DifferentialDownloader {
  // noinspection TypeScriptAbstractClassConstructorCanBeMadeProtected
  constructor(blockAwareFileInfo, httpExecutor, options) {
    this.blockAwareFileInfo = blockAwareFileInfo;
    this.httpExecutor = httpExecutor;
    this.options = options;
    this.fileMetadataBuffer = null;
    this.logger = options.logger;
  }

  createRequestOptions() {
    const result = {
      headers: Object.assign({}, this.options.requestHeaders, {
        accept: "*/*"
      })
    };
    (0, _builderUtilRuntime().configureRequestUrl)(this.options.newUrl, result); // user-agent, cache-control and other common options

    (0, _builderUtilRuntime().configureRequestOptions)(result);
    return result;
  }

  doDownload(oldBlockMap, newBlockMap) {
    // we don't check other metadata like compressionMethod - generic check that it is make sense to differentially update is suitable for it
    if (oldBlockMap.version !== newBlockMap.version) {
      throw new Error(`version is different (${oldBlockMap.version} - ${newBlockMap.version}), full download is required`);
    }

    const logger = this.logger;
    const operations = (0, _downloadPlanBuilder().computeOperations)(oldBlockMap, newBlockMap, logger);

    if (logger.debug != null) {
      logger.debug(JSON.stringify(operations, null, 2));
    }

    let downloadSize = 0;
    let copySize = 0;

    for (const operation of operations) {
      const length = operation.end - operation.start;

      if (operation.kind === _downloadPlanBuilder().OperationKind.DOWNLOAD) {
        downloadSize += length;
      } else {
        copySize += length;
      }
    }

    const newSize = this.blockAwareFileInfo.size;

    if (downloadSize + copySize + (this.fileMetadataBuffer == null ? 0 : this.fileMetadataBuffer.length) !== newSize) {
      throw new Error(`Internal error, size mismatch: downloadSize: ${downloadSize}, copySize: ${copySize}, newSize: ${newSize}`);
    }

    logger.info(`Full: ${formatBytes(newSize)}, To download: ${formatBytes(downloadSize)} (${Math.round(downloadSize / (newSize / 100))}%)`);
    return this.downloadFile(operations);
  }

  downloadFile(tasks) {
    const fdList = [];

    const closeFiles = () => {
      return Promise.all(fdList.map(openedFile => {
        return (0, _fsExtraP().close)(openedFile.descriptor).catch(e => {
          this.logger.error(`cannot close file "${openedFile.path}": ${e}`);
        });
      }));
    };

    return this.doDownloadFile(tasks, fdList).then(closeFiles).catch(e => {
      // then must be after catch here (since then always throws error)
      return closeFiles().catch(closeFilesError => {
        // closeFiles never throw error, but just to be sure
        try {
          this.logger.error(`cannot close files: ${closeFilesError}`);
        } catch (errorOnLog) {
          try {
            console.error(errorOnLog);
          } catch (ignored) {// ok, give up and ignore error
          }
        }

        throw e;
      }).then(() => {
        throw e;
      });
    });
  }

  doDownloadFile(tasks, fdList) {
    var _this = this;

    return (0, _bluebirdLst().coroutine)(function* () {
      const oldFileFd = yield (0, _fsExtraP().open)(_this.options.oldFile, "r");
      fdList.push({
        descriptor: oldFileFd,
        path: _this.options.oldFile
      });
      const newFileFd = yield (0, _fsExtraP().open)(_this.options.newFile, "w");
      fdList.push({
        descriptor: newFileFd,
        path: _this.options.newFile
      });
      const fileOut = (0, _fsExtraP().createWriteStream)(_this.options.newFile, {
        fd: newFileFd
      });
      yield new Promise((resolve, reject) => {
        const streams = [];
        const digestTransform = new (_builderUtilRuntime().DigestTransform)(_this.blockAwareFileInfo.sha512); // to simply debug, do manual validation to allow file to be fully written

        digestTransform.isValidateOnEnd = false;
        streams.push(digestTransform); // noinspection JSArrowFunctionCanBeReplacedWithShorthand

        fileOut.on("finish", () => {
          fileOut.close(() => {
            // remove from fd list because closed successfully
            fdList.splice(1, 1);

            try {
              digestTransform.validate();
            } catch (e) {
              reject(e);
              return;
            }

            resolve();
          });
        });
        streams.push(fileOut);
        let lastStream = null;

        for (const stream of streams) {
          stream.on("error", reject);

          if (lastStream == null) {
            lastStream = stream;
          } else {
            lastStream = lastStream.pipe(stream);
          }
        }

        const firstStream = streams[0];
        let w;

        if (_this.options.isUseMultipleRangeRequest) {
          w = (0, _multipleRangeDownloader().executeTasksUsingMultipleRangeRequests)(_this, tasks, firstStream, oldFileFd, reject);
          w(0);
          return;
        }

        let downloadOperationCount = 0;
        let actualUrl = null;

        _this.logger.info(`Differential download: ${_this.options.newUrl}`);

        const requestOptions = _this.createRequestOptions();

        requestOptions.redirect = "manual";

        w = index => {
          if (index >= tasks.length) {
            if (_this.fileMetadataBuffer != null) {
              firstStream.write(_this.fileMetadataBuffer);
            }

            firstStream.end();
            return;
          }

          const operation = tasks[index++];

          if (operation.kind === _downloadPlanBuilder().OperationKind.COPY) {
            (0, _DataSplitter().copyData)(operation, firstStream, oldFileFd, reject, () => w(index));
            return;
          }

          const range = `bytes=${operation.start}-${operation.end - 1}`;
          requestOptions.headers.range = range;
          const debug = _this.logger.debug;

          if (debug != null) {
            debug(`download range: ${range}`);
          }

          const request = _this.httpExecutor.createRequest(requestOptions, response => {
            // Electron net handles redirects automatically, our NodeJS test server doesn't use redirects - so, we don't check 3xx codes.
            if (response.statusCode >= 400) {
              reject((0, _builderUtilRuntime().createHttpError)(response));
            }

            response.pipe(firstStream, {
              end: false
            });
            response.once("end", () => {
              if (++downloadOperationCount === 100) {
                downloadOperationCount = 0;
                setTimeout(() => w(index), 1000);
              } else {
                w(index);
              }
            });
          });

          request.on("redirect", (statusCode, method, redirectUrl) => {
            _this.logger.info(`Redirect to ${removeQuery(redirectUrl)}`);

            actualUrl = redirectUrl;
            (0, _builderUtilRuntime().configureRequestUrl)(new (_url().URL)(actualUrl), requestOptions);
            request.followRedirect();
          });

          _this.httpExecutor.addErrorAndTimeoutHandlers(request, reject);

          request.end();
        };

        w(0);
      });
    })();
  }

  readRemoteBytes(start, endInclusive) {
    var _this2 = this;

    return (0, _bluebirdLst().coroutine)(function* () {
      const buffer = Buffer.allocUnsafe(endInclusive + 1 - start);

      const requestOptions = _this2.createRequestOptions();

      requestOptions.headers.range = `bytes=${start}-${endInclusive}`;
      let position = 0;
      yield _this2.request(requestOptions, chunk => {
        chunk.copy(buffer, position);
        position += chunk.length;
      });

      if (position !== buffer.length) {
        throw new Error(`Received data length ${position} is not equal to expected ${buffer.length}`);
      }

      return buffer;
    })();
  }

  request(requestOptions, dataHandler) {
    return new Promise((resolve, reject) => {
      const request = this.httpExecutor.createRequest(requestOptions, response => {
        if (!(0, _multipleRangeDownloader().checkIsRangesSupported)(response, reject)) {
          return;
        }

        response.on("data", dataHandler);
        response.on("end", () => resolve());
      });
      this.httpExecutor.addErrorAndTimeoutHandlers(request, reject);
      request.end();
    });
  }

}

exports.DifferentialDownloader = DifferentialDownloader;

function formatBytes(value, symbol = " KB") {
  return new Intl.NumberFormat("en").format((value / 1024).toFixed(2)) + symbol;
} // safety


function removeQuery(url) {
  const index = url.indexOf("?");
  return index < 0 ? url : url.substring(0, index);
} 
// __ts-babel@6.0.4
//# sourceMappingURL=DifferentialDownloader.js.map