/**
* untar.js
*
* Licensed under the MIT License
*
* Copyright(c) 2011 Google Inc.
*
* Reference Documentation:
*
* TAR format: http://www.gnu.org/software/automake/manual/tar/Standard.html
*/

// This file expects to be invoked as a Worker (see onmessage below).
importScripts('bytestream.js');
importScripts('archive.js');

const UnarchiveState = {
  NOT_STARTED: 0,
  UNARCHIVING: 1,
  WAITING: 2,
  FINISHED: 3,
};

// State - consider putting these into a class.
let unarchiveState = UnarchiveState.NOT_STARTED;
let bytestream = null;
let allLocalFiles = null;
let logToConsole = false;

// Progress variables.
let currentFilename = "";
let currentFileNumber = 0;
let currentBytesUnarchivedInFile = 0;
let currentBytesUnarchived = 0;
let totalUncompressedBytesInArchive = 0;
let totalFilesInArchive = 0;

// Helper functions.
const info = function(str) {
  postMessage(new bitjs.archive.UnarchiveInfoEvent(str));
};
const err = function(str) {
  postMessage(new bitjs.archive.UnarchiveErrorEvent(str));
};

// Removes all characters from the first zero-byte in the string onwards.
var readCleanString = function(bstr, numBytes) {
  var str = bstr.readString(numBytes);
  var zIndex = str.indexOf(String.fromCharCode(0));
  return zIndex != -1 ? str.substr(0, zIndex) : str;
};


const postProgress = function() {
  postMessage(new bitjs.archive.UnarchiveProgressEvent(
      currentFilename,
      currentFileNumber,
      currentBytesUnarchivedInFile,
      currentBytesUnarchived,
      totalUncompressedBytesInArchive,
      totalFilesInArchive,
      bytestream.getNumBytesRead(),
  ));
};


class TarLocalFile {
  // takes a ByteStream and parses out the local file information
  constructor(bstream) {
    this.isValid = false;

    let bytesRead = 0;

    // Read in the header block
    this.name = readCleanString(bstream, 100);
    this.mode = readCleanString(bstream, 8);
    this.uid = readCleanString(bstream, 8);
    this.gid = readCleanString(bstream, 8);
    this.size = parseInt(readCleanString(bstream, 12), 8);
    this.mtime = readCleanString(bstream, 12);
    this.chksum = readCleanString(bstream, 8);
    this.typeflag = readCleanString(bstream, 1);
    this.linkname = readCleanString(bstream, 100);
    this.maybeMagic = readCleanString(bstream, 6);
    
    if (this.maybeMagic == "ustar") {
      this.version = readCleanString(bstream, 2);
      this.uname = readCleanString(bstream, 32);
      this.gname = readCleanString(bstream, 32);
      this.devmajor = readCleanString(bstream, 8);
      this.devminor = readCleanString(bstream, 8);
      this.prefix = readCleanString(bstream, 155);

      if (this.prefix.length) {
        this.name = this.prefix + this.name;
      }
      bstream.readBytes(12); // 512 - 500in
    } else {
      bstream.readBytes(255); // 512 - 257
    }

    bytesRead += 512;

    // Done header, now rest of blocks are the file contents.
    this.filename = this.name;
    this.fileData = null;

    info("Untarring file '" + this.filename + "'");
    info("  size = " + this.size);
    info("  typeflag = " + this.typeflag);

    // A regular file.
    if (this.typeflag == 0) {
      info("  This is a regular file.");
      const sizeInBytes = parseInt(this.size);
      this.fileData = new Uint8Array(bstream.readBytes(sizeInBytes));
      bytesRead += sizeInBytes;
      if (this.name.length > 0 && this.size > 0 && this.fileData && this.fileData.buffer) {
        this.isValid = true;
      }

      // Round up to 512-byte blocks.
      const remaining = 512 - bytesRead % 512;
      if (remaining > 0 && remaining < 512) {
        bstream.readBytes(remaining);
      }
    } else if (this.typeflag == 5) {
       info("  This is a directory.")
    }
  }
}

const untar = function() {
  let bstream = bytestream.tee();

  // While we don't encounter an empty block, keep making TarLocalFiles.
  while (bstream.peekNumber(4) != 0) {
    const oneLocalFile = new TarLocalFile(bstream);
    if (oneLocalFile && oneLocalFile.isValid) {
      // If we make it to this point and haven't thrown an error, we have successfully
      // read in the data for a local file, so we can update the actual bytestream.
      bytestream = bstream.tee();

      allLocalFiles.push(oneLocalFile);
      totalUncompressedBytesInArchive += oneLocalFile.size;

      // update progress
      currentFilename = oneLocalFile.filename;
      currentFileNumber = totalFilesInArchive++;
      currentBytesUnarchivedInFile = oneLocalFile.size;
      currentBytesUnarchived += oneLocalFile.size;
      postMessage(new bitjs.archive.UnarchiveExtractEvent(oneLocalFile));
      postProgress();
    }
  }
  totalFilesInArchive = allLocalFiles.length;

  postProgress();

  bytestream = bstream.tee();
};

// event.data.file has the first ArrayBuffer.
// event.data.bytes has all subsequent ArrayBuffers.
onmessage = function(event) {
  const bytes = event.data.file || event.data.bytes;
  logToConsole = !!event.data.logToConsole;

  // This is the very first time we have been called. Initialize the bytestream.
  if (!bytestream) {
    bytestream = new bitjs.io.ByteStream(bytes);
  } else {
    bytestream.push(bytes);
  }

  if (unarchiveState === UnarchiveState.NOT_STARTED) {
    currentFilename = "";
    currentFileNumber = 0;
    currentBytesUnarchivedInFile = 0;
    currentBytesUnarchived = 0;
    totalUncompressedBytesInArchive = 0;
    totalFilesInArchive = 0;
    allLocalFiles = [];
  
    postMessage(new bitjs.archive.UnarchiveStartEvent());

    unarchiveState = UnarchiveState.UNARCHIVING;

    postProgress();
  }

  if (unarchiveState === UnarchiveState.UNARCHIVING ||
    unarchiveState === UnarchiveState.WAITING) {
    try {
      untar();
      unarchiveState = UnarchiveState.FINISHED;
      postMessage(new bitjs.archive.UnarchiveFinishEvent());
    } catch (e) {
      if (typeof e === 'string' && e.startsWith('Error!  Overflowed')) {
        // Overrun the buffer.
        unarchiveState = UnarchiveState.WAITING;
      } else {
        console.error('Found an error while untarring');
        console.dir(e);
        throw e;
      }
    }
  }
};
