const PullStream = require('../PullStream');
const unzip = require('./unzip');
const BufferStream = require('../BufferStream');
const parseExtraField = require('../parseExtraField');
const path = require('path');
const fs = require('fs-extra');
const parseDateTime = require('../parseDateTime');
const parseBuffer = require('../parseBuffer');
const Bluebird = require('bluebird');

const signature = Buffer.alloc(4);
signature.writeUInt32LE(0x06054b50, 0);

function getCrxHeader(source) {
  const sourceStream = source.stream(0).pipe(PullStream());

  return sourceStream.pull(4).then(function(data) {
    const signature = data.readUInt32LE(0);
    if (signature === 0x34327243) {
      let crxHeader;
      return sourceStream.pull(12).then(function(data) {
        crxHeader = parseBuffer.parse(data, [
          ['version', 4],
          ['pubKeyLength', 4],
          ['signatureLength', 4],
        ]);
      }).then(function() {
        return sourceStream.pull(crxHeader.pubKeyLength +crxHeader.signatureLength);
      }).then(function(data) {
        crxHeader.publicKey = data.slice(0, crxHeader.pubKeyLength);
        crxHeader.signature = data.slice(crxHeader.pubKeyLength);
        crxHeader.size = 16 + crxHeader.pubKeyLength +crxHeader.signatureLength;
        return crxHeader;
      });
    }
  });
}

// Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
function getZip64CentralDirectory(source, zip64CDL) {
  const d64loc = parseBuffer.parse(zip64CDL, [
    ['signature', 4],
    ['diskNumber', 4],
    ['offsetToStartOfCentralDirectory', 8],
    ['numberOfDisks', 4],
  ]);

  if (d64loc.signature != 0x07064b50) {
    throw new Error('invalid zip64 end of central dir locator signature (0x07064b50): 0x' + d64loc.signature.toString(16));
  }

  const dir64 = PullStream();
  source.stream(d64loc.offsetToStartOfCentralDirectory).pipe(dir64);

  return dir64.pull(56);
}

// Zip64 File Format Notes: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
function parseZip64DirRecord (dir64record) {
  const vars = parseBuffer.parse(dir64record, [
    ['signature', 4],
    ['sizeOfCentralDirectory', 8],
    ['version', 2],
    ['versionsNeededToExtract', 2],
    ['diskNumber', 4],
    ['diskStart', 4],
    ['numberOfRecordsOnDisk', 8],
    ['numberOfRecords', 8],
    ['sizeOfCentralDirectory', 8],
    ['offsetToStartOfCentralDirectory', 8],
  ]);

  if (vars.signature != 0x06064b50) {
    throw new Error('invalid zip64 end of central dir locator signature (0x06064b50): 0x0' + vars.signature.toString(16));
  }

  return vars;
}

module.exports = function centralDirectory(source, options) {
  const endDir = PullStream();
  const records = PullStream();
  const tailSize = (options && options.tailSize) || 80;
  let sourceSize,
    crxHeader,
    startOffset,
    vars;

  if (options && options.crx)
    crxHeader = getCrxHeader(source);

  return source.size()
    .then(function(size) {
      sourceSize = size;

      source.stream(Math.max(0, size-tailSize))
        .on('error', function (error) { endDir.emit('error', error); })
        .pipe(endDir);

      return endDir.pull(signature);
    })
    .then(function() {
      return Bluebird.props({directory: endDir.pull(22), crxHeader: crxHeader});
    })
    .then(function(d) {
      const data = d.directory;
      startOffset = d.crxHeader && d.crxHeader.size || 0;

      vars = parseBuffer.parse(data, [
        ['signature', 4],
        ['diskNumber', 2],
        ['diskStart', 2],
        ['numberOfRecordsOnDisk', 2],
        ['numberOfRecords', 2],
        ['sizeOfCentralDirectory', 4],
        ['offsetToStartOfCentralDirectory', 4],
        ['commentLength', 2],
      ]);

      // Is this zip file using zip64 format? Use same check as Go:
      // https://github.com/golang/go/blob/master/src/archive/zip/reader.go#L503
      // For zip64 files, need to find zip64 central directory locator header to extract
      // relative offset for zip64 central directory record.
      if (vars.diskNumber == 0xffff || vars.numberOfRecords == 0xffff ||
        vars.offsetToStartOfCentralDirectory == 0xffffffff) {

        // Offset to zip64 CDL is 20 bytes before normal CDR
        const zip64CDLSize = 20;
        const zip64CDLOffset = sourceSize - (tailSize - endDir.match + zip64CDLSize);
        const zip64CDLStream = PullStream();

        source.stream(zip64CDLOffset).pipe(zip64CDLStream);

        return zip64CDLStream.pull(zip64CDLSize)
          .then(function (d) { return getZip64CentralDirectory(source, d); })
          .then(function (dir64record) {
            vars = parseZip64DirRecord(dir64record);
          });
      } else {
        vars.offsetToStartOfCentralDirectory += startOffset;
      }
    })
    .then(function() {
      if (vars.commentLength) return endDir.pull(vars.commentLength).then(function(comment) {
        vars.comment = comment.toString('utf8');
      });
    })
    .then(function() {
      source.stream(vars.offsetToStartOfCentralDirectory).pipe(records);

      vars.extract = function(opts) {
        if (!opts || !opts.path) throw new Error('PATH_MISSING');
        // make sure path is normalized before using it
        opts.path = path.resolve(path.normalize(opts.path));
        return vars.files.then(function(files) {
          return Bluebird.map(files, async function(entry) {
            // to avoid zip slip (writing outside of the destination), we resolve
            // the target path, and make sure it's nested in the intended
            // destination, or not extract it otherwise.
            const extractPath = path.join(opts.path, entry.path);
            if (extractPath.indexOf(opts.path) != 0) {
              return;
            }

            if (entry.type == 'Directory') {
              await fs.ensureDir(extractPath);
              return;
            }

            await fs.ensureDir(path.dirname(extractPath));

            const writer = opts.getWriter ? opts.getWriter({path: extractPath}) : fs.createWriteStream(extractPath);

            return new Promise(function(resolve, reject) {
              entry.stream(opts.password)
                .on('error', reject)
                .pipe(writer)
                .on('close', resolve)
                .on('error', reject);
            });
          }, { concurrency: opts.concurrency > 1 ? opts.concurrency : 1 });
        });
      };

      vars.files = Bluebird.mapSeries(Array(vars.numberOfRecords), function() {
        return records.pull(46).then(function(data) {
          const vars = parseBuffer.parse(data, [
            ['signature', 4],
            ['versionMadeBy', 2],
            ['versionsNeededToExtract', 2],
            ['flags', 2],
            ['compressionMethod', 2],
            ['lastModifiedTime', 2],
            ['lastModifiedDate', 2],
            ['crc32', 4],
            ['compressedSize', 4],
            ['uncompressedSize', 4],
            ['fileNameLength', 2],
            ['extraFieldLength', 2],
            ['fileCommentLength', 2],
            ['diskNumber', 2],
            ['internalFileAttributes', 2],
            ['externalFileAttributes', 4],
            ['offsetToLocalFileHeader', 4],
          ]);

          vars.offsetToLocalFileHeader += startOffset;
          vars.lastModifiedDateTime = parseDateTime(vars.lastModifiedDate, vars.lastModifiedTime);

          return records.pull(vars.fileNameLength).then(function(fileNameBuffer) {
            vars.pathBuffer = fileNameBuffer;
            vars.path = fileNameBuffer.toString('utf8');
            vars.isUnicode = (vars.flags & 0x800) != 0;
            return records.pull(vars.extraFieldLength);
          })
            .then(function(extraField) {
              vars.extra = parseExtraField(extraField, vars);
              return records.pull(vars.fileCommentLength);
            })
            .then(function(comment) {
              vars.comment = comment;
              vars.type = (vars.uncompressedSize === 0 && /[/\\]$/.test(vars.path)) ? 'Directory' : 'File';
              const padding = options && options.padding || 1000;
              vars.stream = function(_password) {
                const totalSize = 30
              + padding // add an extra buffer
              + (vars.extraFieldLength || 0)
              + (vars.fileNameLength || 0)
              + vars.compressedSize;

                return unzip(source, vars.offsetToLocalFileHeader, _password, vars, totalSize);
              };
              vars.buffer = function(_password) {
                return BufferStream(vars.stream(_password));
              };
              return vars;
            });
        });
      });

      return Bluebird.props(vars);
    });
};
