/**
 * Untar library code based on the tar-async project (MIT License):
 * https://github.com/beatgammit/tar-async
 */

// Production steps of ECMA-262, Edition 5, 15.4.4.18
// Reference: http://es5.github.com/#x15.4.4.18
if (!Array.prototype.forEach) {
  Array.prototype.forEach = function (callback, thisArg) {
    var T, k;

    if (this == null) {
      throw new TypeError(" this is null or not defined");
    }

    // 1. Let O be the result of calling ToObject passing the |this| value as the argument.
    var O = Object(this);

    // 2. Let lenValue be the result of calling the Get internal method of O with the argument "length".
    // 3. Let len be ToUint32(lenValue).
    var len = O.length >>> 0;

    // 4. If IsCallable(callback) is false, throw a TypeError exception.
    // See: http://es5.github.com/#x9.11
    if (typeof callback !== "function") {
      throw new TypeError(callback + " is not a function");
    }

    // 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
    if (arguments.length > 1) {
      T = thisArg;
    }

    // 6. Let k be 0
    k = 0;

    // 7. Repeat, while k < len
    while (k < len) {

      var kValue;

      // a. Let Pk be ToString(k).
      //   This is implicit for LHS operands of the in operator
      // b. Let kPresent be the result of calling the HasProperty internal method of O with argument Pk.
      //   This step can be combined with c
      // c. If kPresent is true, then
      if (k in O) {

        // i. Let kValue be the result of calling the Get internal method of O with argument Pk.
        kValue = O[k];

        // ii. Call the Call internal method of callback with T as the this value and
        // argument list containing kValue, k, and O.
        callback.call(T, kValue, k, O);
      }
      // d. Increase k by 1.
      k++;
    }
    // 8. return undefined
  };
}


// Polyfill: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some
if (!Array.prototype.some) {
  Array.prototype.some = function(fun /*, thisArg */)
  {
    'use strict';

    if (this === void 0 || this === null)
      throw new TypeError();

    var t = Object(this);
    var len = t.length >>> 0;
    if (typeof fun !== 'function')
      throw new TypeError();

    var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
    for (var i = 0; i < len; i++)
    {
      if (i in t && fun.call(thisArg, t[i], i, t))
        return true;
    }

    return false;
  };
}


(function() {

	function pad(num, bytes, base) {
		num = num.toString(base || 8);
		return "000000000000".substr(num.length + 12 - bytes) + num;
	}

  /*
    struct posix_header {             // byte offset
	  char name[100];               //   0
	  char mode[8];                 // 100
	  char uid[8];                  // 108
	  char gid[8];                  // 116
	  char size[12];                // 124
	  char mtime[12];               // 136
	  char chksum[8];               // 148
	  char typeflag;                // 156
	  char linkname[100];           // 157
	  char magic[6];                // 257
	  char version[2];              // 263
	  char uname[32];               // 265
	  char gname[32];               // 297
	  char devmajor[8];             // 329
	  char devminor[8];             // 337
	  char prefix[155];             // 345
    // 500
    };
  */

	var headerFormat = [
		{
			'field': 'filename',
			'length': 100,
			'type': 'string'
		},
		{
			'field': 'mode',
			'length': 8,
			'type': 'number'
		},
		{
			'field': 'uid',
			'length': 8,
			'type': 'number'
		},
		{
			'field': 'gid',
			'length': 8,
			'type': 'number'
		},
		{
			'field': 'size',
			'length': 12,
			'type': 'number'
		},
		{
			'field': 'mtime',
			'length': 12,
			'type': 'number'
		},
		{
			'field': 'checksum',
			'length': 8,
			'type': 'number'
		},
		{
			'field': 'type',
			'length': 1,
			'type': 'number'
		},
		{
			'field': 'linkName',
			'length': 100,
			'type': 'string'
		},
		{
			'field': 'ustar',
			'length': 8,
			'type': 'string'
		},
		{
			'field': 'owner',
			'length': 32,
			'type': 'string'
		},
		{
			'field': 'group',
			'length': 32,
			'type': 'string'
		},
		{
			'field': 'majorNumber',
			'length': 8,
			'type': 'number'
		},
		{
			'field': 'minorNumber',
			'length': 8,
			'type': 'number'
		},
		{
			'field': 'filenamePrefix',
			'length': 155,
			'type': 'string'
		},
		{
			'field': 'padding',
			'length': 12
		}
	];

  function clean(length) {
		var i, buffer = new Buffer(length);
		for (i = 0; i < length; i += 1) {
			buffer[i] = 0;
		}
		return buffer;
  }

	function formatHeader(data) {
		var buffer = [];
		offset = 0;

		headerFormat.forEach(function (value) {
      var v = data[value.field] || "";
      for (var i = 0; i < v.length; ++i) {
			  buffer[offset + i] = v[i];
      }
			offset += value.length;
		});

		return buffer;
	}

	var totalRead = 0,
	recordSize = 512,
	fileBuffer,
	leftToRead,
	fileTypes = [
		'normal', 'hard-link', 'symbolic-link', 'character-special', 'block-special', 'directory', 'fifo', 'contiguous-file'
	];

	function filterDecoder(input) {
		var filter = [];
		if (!input) {
			return [0, 7];
		}

		if (typeof input === 'string') {
			input = [].push(input);
		}

		if (!(input instanceof Array)) {
			console.error('Invalid fileType. Only Arrays or strings are accepted');
			return;
		}

		input.forEach(function (i) {
			var index = fileTypes.indexOf(i);
			if (index < 0) {
				console.error('Filetype not valid. Ignoring input:', i);
				return;
			}

			filter.push(i);
		});

		return filter;
	}

	function readInt(value) {
		return parseInt(value.replace(/^0*/, ''), 8) || 0;
	}

	function readString(buf) {
    var str = '';
    for (var i = 0; i < buf.length; ++i) {
      if (buf[i] == 0) { break; }
      str += String.fromCharCode(buf[i]);
    }
    return str;
	}

	function doHeader(buf, cb) {
		var data = {}, offset = 0, checksum = 0;

		function updateChecksum(value) {
			var i, length;

			for (i = 0, length = value.length; i < length; i += 1) {
				checksum += value.charCodeAt(i);
			}
		}

		headerFormat.some(function (field) {
			var tBuf = buf.subarray(offset, offset + field.length),
			tString = String.fromCharCode.apply(null, tBuf);

			offset += field.length;

			if (field.field === 'ustar' && !/ustar/.test(tString)) {
				// end the loop if not using the extended header
				return true;
			} else if (field.field === 'checksum') {
				updateChecksum('        ');
			} else {
				updateChecksum(tString);
			}

			if (field.type === 'string') {
				data[field.field] = readString(tBuf);
			} else if (field.type === 'number') {
				data[field.field] = readInt(tString);
			}
		});

		if (checksum !== data.checksum) {
			cb.call(this, 'Checksum not equal', checksum, data.checksum);
      return false;
		}

		cb.call(this, null, data, recordSize);
    return true;
	}

  function readTarFile(state, data) {
    var fileBuffer = new Uint8Array(data.size);
    fileBuffer.set(state.buffer.subarray(0, data.size));
    state.files.push({
      'meta': data,
      'buffer': fileBuffer
    });
  }

  function removeTrailingNulls(state) {
		// If we're not an even multiple, account for trailing nulls
		if (state.totalRead % recordSize) {
			var bytesBuffer = recordSize - (state.totalRead % recordSize);

			// If we don't have enough bytes to account for the nulls
			if (state.buffer.length < bytesBuffer) {
				state.totalRead += bytesBuffer;
				return;
			}

			// Throw away trailing nulls
			state.buffer = state.buffer.subarray(bytesBuffer);
			state.totalRead += bytesBuffer;
		}
  }

  function processTar(state) {
    if (state.totalRead == 0) {
      // Remove trailing nulls.
      removeTrailingNulls(state);
    }

	  // Check to see if/when we are done.
		if (state.buffer.length < recordSize) {
      state.cb('done', state.totalRead, state.files, null);
			return;
		}

    state.cb('working', state.totalRead, state.files, null);

		doHeader.call(this, state.buffer, function (err, data, rOffset) {
			if (err) {
				if (rOffset === 0) {
          state.cb('done', state.totalRead, state.files, null);
					return;
				}
				return state.cb('error', state.totalRead, state.files, err);
			}

			// Update total; rOffset should always be 512
			state.totalRead += rOffset;
			state.buffer = state.buffer.subarray(rOffset);

      // Read the tar file contents.
      readTarFile(state, data);

      // Update the total and offset.
      state.totalRead += data.size;
			state.buffer = state.buffer.subarray(data.size);

      // Remove trailing nulls.
      removeTrailingNulls(state);

      if (state.buffer.length > 0) {
        setTimeout(function() {
          processTar(state);
        }, 0);
      } else {
        state.cb('done', state.totalRead, state.files, null);
      }
		});
  }

	/*
	 * Extract data from an input.
	 *
   * @param data The data, in Uint8Array form.
	 */
	function Untar(data) {
    this.data = data;
	}

	Untar.prototype.process = function(cb, opt_filter) {
	  return processTar({
      'cb': cb,
      'buffer': this.data,
      'fileTypes': filterDecoder(opt_filter || []),
      'totalRead': 0,
      'files': []
    });
	};

  window.Untar = Untar;

})();
