import { dew as _utilsDewDew } from "../utils.dew.js";
import { dew as _GenericWorkerDewDew } from "../stream/GenericWorker.dew.js";
import { dew as _utf8DewDew } from "../utf8.dew.js";
import { dew as _crc32DewDew } from "../crc32.dew.js";
import { dew as _signatureDewDew } from "../signature.dew.js";
var exports = {},
    _dewExec = false;
export function dew() {
  if (_dewExec) return exports;
  _dewExec = true;

  var utils = _utilsDewDew();

  var GenericWorker = _GenericWorkerDewDew();

  var utf8 = _utf8DewDew();

  var crc32 = _crc32DewDew();

  var signature = _signatureDewDew();
  /**
   * Transform an integer into a string in hexadecimal.
   * @private
   * @param {number} dec the number to convert.
   * @param {number} bytes the number of bytes to generate.
   * @returns {string} the result.
   */


  var decToHex = function (dec, bytes) {
    var hex = "",
        i;

    for (i = 0; i < bytes; i++) {
      hex += String.fromCharCode(dec & 0xff);
      dec = dec >>> 8;
    }

    return hex;
  };
  /**
   * Generate the UNIX part of the external file attributes.
   * @param {Object} unixPermissions the unix permissions or null.
   * @param {Boolean} isDir true if the entry is a directory, false otherwise.
   * @return {Number} a 32 bit integer.
   *
   * adapted from http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute :
   *
   * TTTTsstrwxrwxrwx0000000000ADVSHR
   * ^^^^____________________________ file type, see zipinfo.c (UNX_*)
   *     ^^^_________________________ setuid, setgid, sticky
   *        ^^^^^^^^^________________ permissions
   *                 ^^^^^^^^^^______ not used ?
   *                           ^^^^^^ DOS attribute bits : Archive, Directory, Volume label, System file, Hidden, Read only
   */


  var generateUnixExternalFileAttr = function (unixPermissions, isDir) {
    var result = unixPermissions;

    if (!unixPermissions) {
      // I can't use octal values in strict mode, hence the hexa.
      //  040775 => 0x41fd
      // 0100664 => 0x81b4
      result = isDir ? 0x41fd : 0x81b4;
    }

    return (result & 0xFFFF) << 16;
  };
  /**
   * Generate the DOS part of the external file attributes.
   * @param {Object} dosPermissions the dos permissions or null.
   * @param {Boolean} isDir true if the entry is a directory, false otherwise.
   * @return {Number} a 32 bit integer.
   *
   * Bit 0     Read-Only
   * Bit 1     Hidden
   * Bit 2     System
   * Bit 3     Volume Label
   * Bit 4     Directory
   * Bit 5     Archive
   */


  var generateDosExternalFileAttr = function (dosPermissions, isDir) {
    // the dir flag is already set for compatibility
    return (dosPermissions || 0) & 0x3F;
  };
  /**
   * Generate the various parts used in the construction of the final zip file.
   * @param {Object} streamInfo the hash with information about the compressed file.
   * @param {Boolean} streamedContent is the content streamed ?
   * @param {Boolean} streamingEnded is the stream finished ?
   * @param {number} offset the current offset from the start of the zip file.
   * @param {String} platform let's pretend we are this platform (change platform dependents fields)
   * @param {Function} encodeFileName the function to encode the file name / comment.
   * @return {Object} the zip parts.
   */


  var generateZipParts = function (streamInfo, streamedContent, streamingEnded, offset, platform, encodeFileName) {
    var file = streamInfo['file'],
        compression = streamInfo['compression'],
        useCustomEncoding = encodeFileName !== utf8.utf8encode,
        encodedFileName = utils.transformTo("string", encodeFileName(file.name)),
        utfEncodedFileName = utils.transformTo("string", utf8.utf8encode(file.name)),
        comment = file.comment,
        encodedComment = utils.transformTo("string", encodeFileName(comment)),
        utfEncodedComment = utils.transformTo("string", utf8.utf8encode(comment)),
        useUTF8ForFileName = utfEncodedFileName.length !== file.name.length,
        useUTF8ForComment = utfEncodedComment.length !== comment.length,
        dosTime,
        dosDate,
        extraFields = "",
        unicodePathExtraField = "",
        unicodeCommentExtraField = "",
        dir = file.dir,
        date = file.date;
    var dataInfo = {
      crc32: 0,
      compressedSize: 0,
      uncompressedSize: 0
    }; // if the content is streamed, the sizes/crc32 are only available AFTER
    // the end of the stream.

    if (!streamedContent || streamingEnded) {
      dataInfo.crc32 = streamInfo['crc32'];
      dataInfo.compressedSize = streamInfo['compressedSize'];
      dataInfo.uncompressedSize = streamInfo['uncompressedSize'];
    }

    var bitflag = 0;

    if (streamedContent) {
      // Bit 3: the sizes/crc32 are set to zero in the local header.
      // The correct values are put in the data descriptor immediately
      // following the compressed data.
      bitflag |= 0x0008;
    }

    if (!useCustomEncoding && (useUTF8ForFileName || useUTF8ForComment)) {
      // Bit 11: Language encoding flag (EFS).
      bitflag |= 0x0800;
    }

    var extFileAttr = 0;
    var versionMadeBy = 0;

    if (dir) {
      // dos or unix, we set the dos dir flag
      extFileAttr |= 0x00010;
    }

    if (platform === "UNIX") {
      versionMadeBy = 0x031E; // UNIX, version 3.0

      extFileAttr |= generateUnixExternalFileAttr(file.unixPermissions, dir);
    } else {
      // DOS or other, fallback to DOS
      versionMadeBy = 0x0014; // DOS, version 2.0

      extFileAttr |= generateDosExternalFileAttr(file.dosPermissions, dir);
    } // date
    // @see http://www.delorie.com/djgpp/doc/rbinter/it/52/13.html
    // @see http://www.delorie.com/djgpp/doc/rbinter/it/65/16.html
    // @see http://www.delorie.com/djgpp/doc/rbinter/it/66/16.html


    dosTime = date.getUTCHours();
    dosTime = dosTime << 6;
    dosTime = dosTime | date.getUTCMinutes();
    dosTime = dosTime << 5;
    dosTime = dosTime | date.getUTCSeconds() / 2;
    dosDate = date.getUTCFullYear() - 1980;
    dosDate = dosDate << 4;
    dosDate = dosDate | date.getUTCMonth() + 1;
    dosDate = dosDate << 5;
    dosDate = dosDate | date.getUTCDate();

    if (useUTF8ForFileName) {
      // set the unicode path extra field. unzip needs at least one extra
      // field to correctly handle unicode path, so using the path is as good
      // as any other information. This could improve the situation with
      // other archive managers too.
      // This field is usually used without the utf8 flag, with a non
      // unicode path in the header (winrar, winzip). This helps (a bit)
      // with the messy Windows' default compressed folders feature but
      // breaks on p7zip which doesn't seek the unicode path extra field.
      // So for now, UTF-8 everywhere !
      unicodePathExtraField = // Version
      decToHex(1, 1) + // NameCRC32
      decToHex(crc32(encodedFileName), 4) + // UnicodeName
      utfEncodedFileName;
      extraFields += // Info-ZIP Unicode Path Extra Field
      "\x75\x70" + // size
      decToHex(unicodePathExtraField.length, 2) + // content
      unicodePathExtraField;
    }

    if (useUTF8ForComment) {
      unicodeCommentExtraField = // Version
      decToHex(1, 1) + // CommentCRC32
      decToHex(crc32(encodedComment), 4) + // UnicodeName
      utfEncodedComment;
      extraFields += // Info-ZIP Unicode Path Extra Field
      "\x75\x63" + // size
      decToHex(unicodeCommentExtraField.length, 2) + // content
      unicodeCommentExtraField;
    }

    var header = ""; // version needed to extract

    header += "\x0A\x00"; // general purpose bit flag

    header += decToHex(bitflag, 2); // compression method

    header += compression.magic; // last mod file time

    header += decToHex(dosTime, 2); // last mod file date

    header += decToHex(dosDate, 2); // crc-32

    header += decToHex(dataInfo.crc32, 4); // compressed size

    header += decToHex(dataInfo.compressedSize, 4); // uncompressed size

    header += decToHex(dataInfo.uncompressedSize, 4); // file name length

    header += decToHex(encodedFileName.length, 2); // extra field length

    header += decToHex(extraFields.length, 2);
    var fileRecord = signature.LOCAL_FILE_HEADER + header + encodedFileName + extraFields;
    var dirRecord = signature.CENTRAL_FILE_HEADER + // version made by (00: DOS)
    decToHex(versionMadeBy, 2) + // file header (common to file and central directory)
    header + // file comment length
    decToHex(encodedComment.length, 2) + // disk number start
    "\x00\x00" + // internal file attributes TODO
    "\x00\x00" + // external file attributes
    decToHex(extFileAttr, 4) + // relative offset of local header
    decToHex(offset, 4) + // file name
    encodedFileName + // extra field
    extraFields + // file comment
    encodedComment;
    return {
      fileRecord: fileRecord,
      dirRecord: dirRecord
    };
  };
  /**
   * Generate the EOCD record.
   * @param {Number} entriesCount the number of entries in the zip file.
   * @param {Number} centralDirLength the length (in bytes) of the central dir.
   * @param {Number} localDirLength the length (in bytes) of the local dir.
   * @param {String} comment the zip file comment as a binary string.
   * @param {Function} encodeFileName the function to encode the comment.
   * @return {String} the EOCD record.
   */


  var generateCentralDirectoryEnd = function (entriesCount, centralDirLength, localDirLength, comment, encodeFileName) {
    var dirEnd = "";
    var encodedComment = utils.transformTo("string", encodeFileName(comment)); // end of central dir signature

    dirEnd = signature.CENTRAL_DIRECTORY_END + // number of this disk
    "\x00\x00" + // number of the disk with the start of the central directory
    "\x00\x00" + // total number of entries in the central directory on this disk
    decToHex(entriesCount, 2) + // total number of entries in the central directory
    decToHex(entriesCount, 2) + // size of the central directory   4 bytes
    decToHex(centralDirLength, 4) + // offset of start of central directory with respect to the starting disk number
    decToHex(localDirLength, 4) + // .ZIP file comment length
    decToHex(encodedComment.length, 2) + // .ZIP file comment
    encodedComment;
    return dirEnd;
  };
  /**
   * Generate data descriptors for a file entry.
   * @param {Object} streamInfo the hash generated by a worker, containing information
   * on the file entry.
   * @return {String} the data descriptors.
   */


  var generateDataDescriptors = function (streamInfo) {
    var descriptor = "";
    descriptor = signature.DATA_DESCRIPTOR + // crc-32                          4 bytes
    decToHex(streamInfo['crc32'], 4) + // compressed size                 4 bytes
    decToHex(streamInfo['compressedSize'], 4) + // uncompressed size               4 bytes
    decToHex(streamInfo['uncompressedSize'], 4);
    return descriptor;
  };
  /**
   * A worker to concatenate other workers to create a zip file.
   * @param {Boolean} streamFiles `true` to stream the content of the files,
   * `false` to accumulate it.
   * @param {String} comment the comment to use.
   * @param {String} platform the platform to use, "UNIX" or "DOS".
   * @param {Function} encodeFileName the function to encode file names and comments.
   */


  function ZipFileWorker(streamFiles, comment, platform, encodeFileName) {
    GenericWorker.call(this, "ZipFileWorker"); // The number of bytes written so far. This doesn't count accumulated chunks.

    this.bytesWritten = 0; // The comment of the zip file

    this.zipComment = comment; // The platform "generating" the zip file.

    this.zipPlatform = platform; // the function to encode file names and comments.

    this.encodeFileName = encodeFileName; // Should we stream the content of the files ?

    this.streamFiles = streamFiles; // If `streamFiles` is false, we will need to accumulate the content of the
    // files to calculate sizes / crc32 (and write them *before* the content).
    // This boolean indicates if we are accumulating chunks (it will change a lot
    // during the lifetime of this worker).

    this.accumulate = false; // The buffer receiving chunks when accumulating content.

    this.contentBuffer = []; // The list of generated directory records.

    this.dirRecords = []; // The offset (in bytes) from the beginning of the zip file for the current source.

    this.currentSourceOffset = 0; // The total number of entries in this zip file.

    this.entriesCount = 0; // the name of the file currently being added, null when handling the end of the zip file.
    // Used for the emitted metadata.

    this.currentFile = null;
    this._sources = [];
  }

  utils.inherits(ZipFileWorker, GenericWorker);
  /**
   * @see GenericWorker.push
   */

  ZipFileWorker.prototype.push = function (chunk) {
    var currentFilePercent = chunk.meta.percent || 0;
    var entriesCount = this.entriesCount;
    var remainingFiles = this._sources.length;

    if (this.accumulate) {
      this.contentBuffer.push(chunk);
    } else {
      this.bytesWritten += chunk.data.length;
      GenericWorker.prototype.push.call(this, {
        data: chunk.data,
        meta: {
          currentFile: this.currentFile,
          percent: entriesCount ? (currentFilePercent + 100 * (entriesCount - remainingFiles - 1)) / entriesCount : 100
        }
      });
    }
  };
  /**
   * The worker started a new source (an other worker).
   * @param {Object} streamInfo the streamInfo object from the new source.
   */


  ZipFileWorker.prototype.openedSource = function (streamInfo) {
    this.currentSourceOffset = this.bytesWritten;
    this.currentFile = streamInfo['file'].name;
    var streamedContent = this.streamFiles && !streamInfo['file'].dir; // don't stream folders (because they don't have any content)

    if (streamedContent) {
      var record = generateZipParts(streamInfo, streamedContent, false, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);
      this.push({
        data: record.fileRecord,
        meta: {
          percent: 0
        }
      });
    } else {
      // we need to wait for the whole file before pushing anything
      this.accumulate = true;
    }
  };
  /**
   * The worker finished a source (an other worker).
   * @param {Object} streamInfo the streamInfo object from the finished source.
   */


  ZipFileWorker.prototype.closedSource = function (streamInfo) {
    this.accumulate = false;
    var streamedContent = this.streamFiles && !streamInfo['file'].dir;
    var record = generateZipParts(streamInfo, streamedContent, true, this.currentSourceOffset, this.zipPlatform, this.encodeFileName);
    this.dirRecords.push(record.dirRecord);

    if (streamedContent) {
      // after the streamed file, we put data descriptors
      this.push({
        data: generateDataDescriptors(streamInfo),
        meta: {
          percent: 100
        }
      });
    } else {
      // the content wasn't streamed, we need to push everything now
      // first the file record, then the content
      this.push({
        data: record.fileRecord,
        meta: {
          percent: 0
        }
      });

      while (this.contentBuffer.length) {
        this.push(this.contentBuffer.shift());
      }
    }

    this.currentFile = null;
  };
  /**
   * @see GenericWorker.flush
   */


  ZipFileWorker.prototype.flush = function () {
    var localDirLength = this.bytesWritten;

    for (var i = 0; i < this.dirRecords.length; i++) {
      this.push({
        data: this.dirRecords[i],
        meta: {
          percent: 100
        }
      });
    }

    var centralDirLength = this.bytesWritten - localDirLength;
    var dirEnd = generateCentralDirectoryEnd(this.dirRecords.length, centralDirLength, localDirLength, this.zipComment, this.encodeFileName);
    this.push({
      data: dirEnd,
      meta: {
        percent: 100
      }
    });
  };
  /**
   * Prepare the next source to be read.
   */


  ZipFileWorker.prototype.prepareNextSource = function () {
    this.previous = this._sources.shift();
    this.openedSource(this.previous.streamInfo);

    if (this.isPaused) {
      this.previous.pause();
    } else {
      this.previous.resume();
    }
  };
  /**
   * @see GenericWorker.registerPrevious
   */


  ZipFileWorker.prototype.registerPrevious = function (previous) {
    this._sources.push(previous);

    var self = this;
    previous.on('data', function (chunk) {
      self.processChunk(chunk);
    });
    previous.on('end', function () {
      self.closedSource(self.previous.streamInfo);

      if (self._sources.length) {
        self.prepareNextSource();
      } else {
        self.end();
      }
    });
    previous.on('error', function (e) {
      self.error(e);
    });
    return this;
  };
  /**
   * @see GenericWorker.resume
   */


  ZipFileWorker.prototype.resume = function () {
    if (!GenericWorker.prototype.resume.call(this)) {
      return false;
    }

    if (!this.previous && this._sources.length) {
      this.prepareNextSource();
      return true;
    }

    if (!this.previous && !this._sources.length && !this.generatedError) {
      this.end();
      return true;
    }
  };
  /**
   * @see GenericWorker.error
   */


  ZipFileWorker.prototype.error = function (e) {
    var sources = this._sources;

    if (!GenericWorker.prototype.error.call(this, e)) {
      return false;
    }

    for (var i = 0; i < sources.length; i++) {
      try {
        sources[i].error(e);
      } catch (e) {// the `error` exploded, nothing to do
      }
    }

    return true;
  };
  /**
   * @see GenericWorker.lock
   */


  ZipFileWorker.prototype.lock = function () {
    GenericWorker.prototype.lock.call(this);
    var sources = this._sources;

    for (var i = 0; i < sources.length; i++) {
      sources[i].lock();
    }
  };

  exports = ZipFileWorker;
  return exports;
}