var alloc = Buffer.alloc;

var ZEROS = '0000000000000000000';
var SEVENS = '7777777777777777777';
var ZERO_OFFSET = '0'.charCodeAt(0);
var USTAR_MAGIC = Buffer.from('ustar\x00', 'binary');
var USTAR_VER = Buffer.from('00', 'binary');
var GNU_MAGIC = Buffer.from('ustar\x20', 'binary');
var GNU_VER = Buffer.from('\x20\x00', 'binary');
var MASK = parseInt('7777', 8);
var MAGIC_OFFSET = 257;
var VERSION_OFFSET = 263;

var clamp = function (index, len, defaultValue) {
  if (typeof index !== 'number') return defaultValue;
  index = ~~index; // Coerce to integer.
  if (index >= len) return len;
  if (index >= 0) return index;
  index += len;
  if (index >= 0) return index;
  return 0;
};

var toType = function (flag) {
  switch (flag) {
    case 0:
      return 'file';
    case 1:
      return 'link';
    case 2:
      return 'symlink';
    case 3:
      return 'character-device';
    case 4:
      return 'block-device';
    case 5:
      return 'directory';
    case 6:
      return 'fifo';
    case 7:
      return 'contiguous-file';
    case 72:
      return 'pax-header';
    case 55:
      return 'pax-global-header';
    case 27:
      return 'gnu-long-link-path';
    case 28:
    case 30:
      return 'gnu-long-path';
  }

  return null;
};

var toTypeflag = function (flag) {
  switch (flag) {
    case 'file':
      return 0;
    case 'link':
      return 1;
    case 'symlink':
      return 2;
    case 'character-device':
      return 3;
    case 'block-device':
      return 4;
    case 'directory':
      return 5;
    case 'fifo':
      return 6;
    case 'contiguous-file':
      return 7;
    case 'pax-header':
      return 72;
  }

  return 0;
};

var indexOf = function (block, num, offset, end) {
  for (; offset < end; offset++) {
    if (block[offset] === num) return offset;
  }
  return end;
};

var cksum = function (block) {
  var sum = 8 * 32;
  for (var i = 0; i < 148; i++) sum += block[i];
  for (var j = 156; j < 512; j++) sum += block[j];
  return sum;
};

var encodeOct = function (val, n) {
  val = val.toString(8);
  if (val.length > n) return SEVENS.slice(0, n) + ' ';
  else return ZEROS.slice(0, n - val.length) + val + ' ';
};

/* Copied from the node-tar repo and modified to meet
 * tar-stream coding standard.
 *
 * Source: https://github.com/npm/node-tar/blob/51b6627a1f357d2eb433e7378e5f05e83b7aa6cd/lib/header.js#L349
 */
function parse256(buf) {
  // first byte MUST be either 80 or FF
  // 80 for positive, FF for 2's comp
  var positive;
  if (buf[0] === 0x80) positive = true;
  else if (buf[0] === 0xff) positive = false;
  else return null;

  // build up a base-256 tuple from the least sig to the highest
  var tuple = [];
  for (var i = buf.length - 1; i > 0; i--) {
    var byte = buf[i];
    if (positive) tuple.push(byte);
    else tuple.push(0xff - byte);
  }

  var sum = 0;
  var l = tuple.length;
  for (i = 0; i < l; i++) {
    sum += tuple[i] * Math.pow(256, i);
  }

  return positive ? sum : -1 * sum;
}

var decodeOct = function (val, offset, length) {
  val = val.slice(offset, offset + length);
  offset = 0;

  // If prefixed with 0x80 then parse as a base-256 integer
  if (val[offset] & 0x80) {
    return parse256(val);
  } else {
    // Older versions of tar can prefix with spaces
    while (offset < val.length && val[offset] === 32) offset++;
    var end = clamp(
      indexOf(val, 32, offset, val.length),
      val.length,
      val.length
    );
    while (offset < end && val[offset] === 0) offset++;
    if (end === offset) return 0;
    return parseInt(val.slice(offset, end).toString(), 8);
  }
};

var decodeStr = function (val, offset, length, encoding) {
  return val
    .slice(offset, indexOf(val, 0, offset, offset + length))
    .toString(encoding);
};

var addLength = function (str) {
  var len = Buffer.byteLength(str);
  var digits = Math.floor(Math.log(len) / Math.log(10)) + 1;
  if (len + digits >= Math.pow(10, digits)) digits++;

  return len + digits + str;
};

exports.decodeLongPath = function (buf, encoding) {
  return decodeStr(buf, 0, buf.length, encoding);
};

exports.encodePax = function (opts) {
  // TODO: encode more stuff in pax
  var result = '';
  if (opts.name) result += addLength(' path=' + opts.name + '\n');
  if (opts.linkname) result += addLength(' linkpath=' + opts.linkname + '\n');
  var pax = opts.pax;
  if (pax) {
    for (var key in pax) {
      result += addLength(' ' + key + '=' + pax[key] + '\n');
    }
  }
  return Buffer.from(result);
};

exports.decodePax = function (buf) {
  var result = {};

  while (buf.length) {
    var i = 0;
    while (i < buf.length && buf[i] !== 32) i++;
    var len = parseInt(buf.slice(0, i).toString(), 10);
    if (!len) return result;

    var b = buf.slice(i + 1, len - 1).toString();
    var keyIndex = b.indexOf('=');
    if (keyIndex === -1) return result;
    result[b.slice(0, keyIndex)] = b.slice(keyIndex + 1);

    buf = buf.slice(len);
  }

  return result;
};

exports.encode = function (opts) {
  var buf = alloc(512);
  var name = opts.name;
  var prefix = '';

  if (opts.typeflag === 5 && name[name.length - 1] !== '/') name += '/';
  if (Buffer.byteLength(name) !== name.length) return null; // utf-8

  while (Buffer.byteLength(name) > 100) {
    var i = name.indexOf('/');
    if (i === -1) return null;
    prefix += prefix ? '/' + name.slice(0, i) : name.slice(0, i);
    name = name.slice(i + 1);
  }

  if (Buffer.byteLength(name) > 100 || Buffer.byteLength(prefix) > 155)
    return null;
  if (opts.linkname && Buffer.byteLength(opts.linkname) > 100) return null;

  buf.write(name);
  buf.write(encodeOct(opts.mode & MASK, 6), 100);
  buf.write(encodeOct(opts.uid, 6), 108);
  buf.write(encodeOct(opts.gid, 6), 116);
  buf.write(encodeOct(opts.size, 11), 124);
  buf.write(encodeOct((opts.mtime.getTime() / 1000) | 0, 11), 136);

  buf[156] = ZERO_OFFSET + toTypeflag(opts.type);

  if (opts.linkname) buf.write(opts.linkname, 157);

  USTAR_MAGIC.copy(buf, MAGIC_OFFSET);
  USTAR_VER.copy(buf, VERSION_OFFSET);
  if (opts.uname) buf.write(opts.uname, 265);
  if (opts.gname) buf.write(opts.gname, 297);
  buf.write(encodeOct(opts.devmajor || 0, 6), 329);
  buf.write(encodeOct(opts.devminor || 0, 6), 337);

  if (prefix) buf.write(prefix, 345);

  buf.write(encodeOct(cksum(buf), 6), 148);

  return buf;
};

exports.decode = function (buf, filenameEncoding, allowUnknownFormat) {
  var typeflag = buf[156] === 0 ? 0 : buf[156] - ZERO_OFFSET;

  var name = decodeStr(buf, 0, 100, filenameEncoding);
  var mode = decodeOct(buf, 100, 8);
  var uid = decodeOct(buf, 108, 8);
  var gid = decodeOct(buf, 116, 8);
  var size = decodeOct(buf, 124, 12);
  var mtime = decodeOct(buf, 136, 12);
  var type = toType(typeflag);
  var linkname =
    buf[157] === 0 ? null : decodeStr(buf, 157, 100, filenameEncoding);
  var uname = decodeStr(buf, 265, 32);
  var gname = decodeStr(buf, 297, 32);
  var devmajor = decodeOct(buf, 329, 8);
  var devminor = decodeOct(buf, 337, 8);

  var c = cksum(buf);

  // checksum is still initial value if header was null.
  if (c === 8 * 32) return null;

  // valid checksum
  if (c !== decodeOct(buf, 148, 8))
    throw new Error(
      'Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?'
    );

  if (USTAR_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0) {
    // ustar (posix) format.
    // prepend prefix, if present.
    if (buf[345])
      name = decodeStr(buf, 345, 155, filenameEncoding) + '/' + name;
  } else if (
    GNU_MAGIC.compare(buf, MAGIC_OFFSET, MAGIC_OFFSET + 6) === 0 &&
    GNU_VER.compare(buf, VERSION_OFFSET, VERSION_OFFSET + 2) === 0
  ) {
    // 'gnu'/'oldgnu' format. Similar to ustar, but has support for incremental and
    // multi-volume tarballs.
  } else {
    if (!allowUnknownFormat) {
      throw new Error('Invalid tar header: unknown format.');
    }
  }

  // to support old tar versions that use trailing / to indicate dirs
  if (typeflag === 0 && name && name[name.length - 1] === '/') typeflag = 5;

  return {
    name,
    mode,
    uid,
    gid,
    size,
    mtime: new Date(1000 * mtime),
    type,
    linkname,
    uname,
    gname,
    devmajor,
    devminor,
  };
};