text: import { CompressedFile } from "./compressed-file.js";
  
  import { Worker, File, workerPath } from './shim/browser.js';
  
  export class Archive{
  
      /**
       * Initialize libarchivejs
       * @param {Object} options 
       */
      static init(options = {}){
          Archive._options = {
              workerUrl: workerPath,
              ...options
          };
          return Archive._options;
      }
  
      /**
       * Creates new archive instance from browser native File object
       * @param {File} file
       * @param {object} options
       * @returns {Archive}
       */
      static open(file, options = null){
          options =   options || 
                      Archive._options || 
                      Archive.init() && console.warn('Automatically initializing using options: ', Archive._options);
          const arch = new Archive(file,options);
          return arch.open();
      }
  
      /**
       * Create new archive
       * @param {File} file 
       * @param {Object} options 
       */
      constructor(file,options){
          this._worker = new Worker(options.workerUrl);
          this._worker.addEventListener('message', this._workerMsg.bind(this));
          this._callbacks = [];
          this._content = {};
          this._processed = 0;
          this._file = file;
      }
  
      /**
       * Prepares file for reading
       * @returns {Promise<Archive>} archive instance
       */
      async open(){
          await this._postMessage({type: 'HELLO'},(resolve,reject,msg) => {
              if( msg.type === 'READY' ){
                  resolve();
              }
          });
          return await this._postMessage({type: 'OPEN', file: this._file}, (resolve,reject,msg) => {
              if(msg.type === 'OPENED'){
                  resolve(this);
              }
          });
      }
      
      /**
       * Terminate worker to free up memory
       */
      close() {
          this._worker.terminate();
          this._worker = null;
      }
  
      /**
       * detect if archive has encrypted data
       * @returns {boolean|null} null if could not be determined
       */
      hasEncryptedData(){
          return this._postMessage({type: 'CHECK_ENCRYPTION'}, 
              (resolve,reject,msg) => {
                  if( msg.type === 'ENCRYPTION_STATUS' ){
                      resolve(msg.status);
                  }
              }
          );
      }
  
      /**
       * set password to be used when reading archive
       */
      usePassword(archivePassword){
          return this._postMessage({type: 'SET_PASSPHRASE', passphrase: archivePassword},
              (resolve,reject,msg) => {
                  if( msg.type === 'PASSPHRASE_STATUS' ){
                      resolve(msg.status);
                  }
              }
          );
      }
  
      /**
       * Returns object containing directory structure and file information 
       * @returns {Promise<object>}
       */
      getFilesObject(){
          if( this._processed > 0 ){
              return Promise.resolve().then( () => this._content );
          }
          return this._postMessage({type: 'LIST_FILES'}, (resolve,reject,msg) => {
              if( msg.type === 'ENTRY' ){
                  const entry = msg.entry;
                  const [ target, prop ] = this._getProp(this._content,entry.path);
                  if( entry.type === 'FILE' ){
                      target[prop] = new CompressedFile(entry.fileName,entry.size,entry.path,this);                    
                  }
                  return true;
              }else if( msg.type === 'END' ){
                  this._processed = 1;
                  resolve(this._cloneContent(this._content));
              }
          });
      }
  
      getFilesArray(){
          return this.getFilesObject().then( (obj) => {
              return this._objectToArray(obj);
          });
      }
  
      extractSingleFile(target){
          // Prevent extraction if worker already terminated
          if( this._worker === null ){
              throw new Error("Archive already closed");
          }
  
          return this._postMessage({type: 'EXTRACT_SINGLE_FILE', target: target}, 
              (resolve,reject,msg) => {
                  if( msg.type === 'FILE' ){
                      const file = new File([msg.entry.fileData], msg.entry.fileName, {
                          type: 'application/octet-stream'
                      });
                      resolve(file);
                  }
              }
          );
      }
  
      /**
       * Returns object containing directory structure and extracted File objects 
       * @param {Function} extractCallback
       * 
       */
      extractFiles(extractCallback){
          if( this._processed > 1 ){
              return Promise.resolve().then( () => this._content );
          }
          return this._postMessage({type: 'EXTRACT_FILES'}, (resolve,reject,msg) => {
              if( msg.type === 'ENTRY' ){
                  const [ target, prop ] = this._getProp(this._content,msg.entry.path);
                  if( msg.entry.type === 'FILE' ){
                      target[prop] = new File([msg.entry.fileData], msg.entry.fileName, {
                          type: 'application/octet-stream'
                      });
                      if (extractCallback !== undefined) {
                          setTimeout(extractCallback.bind(null,{
                              file: target[prop],
                              path: msg.entry.path,
                          }));
                      }
                  }
                  return true;
              }else if( msg.type === 'END' ){
                  this._processed = 2;
                  this._worker.terminate();
                  resolve(this._cloneContent(this._content));
              }
          });
      }
  
      _cloneContent(obj){
          if( obj instanceof File || obj instanceof CompressedFile || obj === null ) return obj;
          const o = {};
          for( const prop of Object.keys(obj) ){
              o[prop] = this._cloneContent(obj[prop]);
          }
          return o;
      }
  
      _objectToArray(obj,path = ''){
          const files = [];
          for( const key of Object.keys(obj) ){
              if( obj[key] instanceof File || obj[key] instanceof CompressedFile || obj[key] === null ){
                  files.push({
                      file: obj[key] || key,
                      path: path
                  });
              }else{
                  files.push( ...this._objectToArray(obj[key],`${path}${key}/`) );
              }
          }
          return files;
      }
  
      _getProp(obj,path){
          const parts = path.split('/');
          if( parts[parts.length -1] === '' ) parts.pop();
          let cur = obj, prev = null;
          for( const part of parts ){
              cur[part] = cur[part] || {};
              prev = cur;
              cur = cur[part];
          }
          return [ prev, parts[parts.length-1] ];
      }
  
      _postMessage(msg,callback){
          this._worker.postMessage(msg);
          return new Promise((resolve,reject) => {
              this._callbacks.push( this._msgHandler.bind(this,callback,resolve,reject) );
          });
      }
  
      _msgHandler(callback,resolve,reject,msg){
          if( msg.type === 'BUSY' ){
              reject('worker is busy');
          }else if( msg.type === 'ERROR' ){
              reject(msg.error);
          }else{
              return callback(resolve,reject,msg);
          }
      }
  
      _workerMsg({data: msg}){
          const callback = this._callbacks[this._callbacks.length -1];
          const next = callback(msg);
          if( !next ){
              this._callbacks.pop();
          }
      }
  
  }
  