"use strict";

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

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

  _builderUtilRuntime = function () {
    return data;
  };

  return data;
}

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

  _fsExtra = function () {
    return data;
  };

  return data;
}

var _fs = require("fs");

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, _fsExtra().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;
      });
    });
  }

  async doDownloadFile(tasks, fdList) {
    const oldFileFd = await (0, _fsExtra().open)(this.options.oldFile, "r");
    fdList.push({
      descriptor: oldFileFd,
      path: this.options.oldFile
    });
    const newFileFd = await (0, _fsExtra().open)(this.options.newFile, "w");
    fdList.push({
      descriptor: newFileFd,
      path: this.options.newFile
    });
    const fileOut = (0, _fs.createWriteStream)(this.options.newFile, {
      fd: newFileFd
    });
    await 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);
    });
  }

  async readRemoteBytes(start, endInclusive) {
    const buffer = Buffer.allocUnsafe(endInclusive + 1 - start);
    const requestOptions = this.createRequestOptions();
    requestOptions.headers.range = `bytes=${start}-${endInclusive}`;
    let position = 0;
    await this.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