text: 'use strict';
  
  const Transform = require('stream').Transform; // transform catches eos and _flush gets called
  const ApplicationHttpReader = require('./ApplicationHttpReader');
  const Cache = require('./Cache');
  const DataReader = require('./DataReader');
  const MultipartParser = require('./MultipartReader');
  const DeserializationError = require('../errors/DeserializationError');
  
  const DATA_EVENTS = DataReader.EVENTS;
  const REQUEST_EVENTS = ApplicationHttpReader.EVENTS;
  const PART_EVENTS = require('./PartReader').EVENTS;
  const MULTIPART_EVENTS = MultipartParser.EVENTS;
  
  /**
   * States
   */
  const STATES = {
      START: 0,
      PROCESS: 1,
      FINISHED: 2
  };
  
  /**
   * Events
   */
  const EVENTS = {
      UNCONSUMED: 'reader.unconsumed',
      START: 'reader.start',
      END: 'reader.end',
  
      REQUEST_START: REQUEST_EVENTS.START,
      REQUEST_REQUESTLINE: REQUEST_EVENTS.REQUEST_LINE,
      REQUEST_HEADERS: REQUEST_EVENTS.HEADERS,
      REQUEST_BODY_START: REQUEST_EVENTS.BODY_START,
      REQUEST_BODY_DATA: ApplicationHttpReader.BODY_PREFIX + '.' + DATA_EVENTS.DATA,
      REQUEST_BODY_END: REQUEST_EVENTS.BODY_END,
      REQUEST_END: REQUEST_EVENTS.END,
      PART_START: PART_EVENTS.START,
      PART_HEADERS: PART_EVENTS.HEADERS,
      MULTIPART_START: MULTIPART_EVENTS.START,
      MULTIPART_END: MULTIPART_EVENTS.END
  };
  
  
  /**
   * Main entry class for reading requests. Allows registering various readers for mime types (e.g. multipart/mixed,
   * application/http and application/octet-stream).
   * Implement as transform to catch the end of input event {@see _flush}.
   * Events are emitted to the emitter object given to constructor.
   *
   * Usage example:
   * const reader = new Reader(partReader, emitter)
   * request.pipe(reader)
   *
   * @extends Transform
   */
  class ContentDeserializer extends Transform {
      /**
       * @param {Object} reader - Reader used when starting reading
       * @param {EventEmitter} emitter - Emitter to be used for reading events
       */
      constructor(reader, emitter) {
          super();
  
          this._cache = new Cache(Buffer.from([]));
  
          // State Info
          this._state = STATES.START;
  
          // Stack info
          this._mimetypeReader = null;
          this._mimetypeReaderStack = [];
          this._mimetypeReaderFactories = new Map();
          this._addMimeReaderFactories('application/http', ApplicationHttpReader.createInstance);
          this._addMimeReaderFactories('application/octet-stream', DataReader.createInstance);
          this._addMimeReaderFactories('multipart/mixed', MultipartParser.createInstance);
          this._fallBackReaderFactory = DataReader.createInstance;
  
          this.pushReader(reader);
  
          this._emitter = emitter;
      }
  
      /**
       * Overwritten _transform implementation
       *
       * @param {Buffer} buffer - Chunk of bytes
       * @param {string} enc - Encoding of the chunk
       * @param {Function} next - Callback function
       * @private
       */
      _transform(buffer, enc, next) {
          try {
              this._cache.append(buffer);
              this.readCache(false);
              next();
          } catch (err) {
              next(err);
          }
      }
  
      /**
       * Overwritten _flush method to catch the end of the source stream.
       * @param {Function} next - Callback function
       * @private
       */
      _flush(next) {
          try {
              this.readCache(true); // last
              next();
          } catch (err) {
              next(err);
          }
      }
  
      /**
       * Read data from cache until the catch is empty. The data is passed to the current reader until the
       * processor is finished or more data is required by the reader.
       *
       * @param {boolean} last whether this reader is the last one
       * @returns {boolean} whether more data is needed
       * @throws {Error} In case of internal stack error
       * @throws {Error} In case last === true and more data is required
       */
      readCache(last) {
          let needMoreData = false;
  
          // this._cache.setConsumedBytes(0);
  
          while (!needMoreData && this._state !== STATES.FINISHED) {
              switch (this._state) {
                  case STATES.START:
                      this._emitter.emit(EVENTS.START);
                      this._state = STATES.PROCESS;
                      break;
                  case STATES.PROCESS:
                      needMoreData = this._mimetypeReader.readCache(this, this._cache, last);
                      if (needMoreData === false) {
                          this.popReader();
                      }
  
  
                      if (!this._mimetypeReader) {
                          this._state = STATES.FINISHED;
                      }
                      break;
                  default:
                      throw new DeserializationError('default reached');
              }
          }
  
          this._cache.shrink();
  
          // Ensure proper cleanup
  
          if (last) {
              if (needMoreData) throw new DeserializationError('More data required to process multipart');
              if (this._cache.getLength()) {
                  this._emitter.emit(EVENTS.UNCONSUMED, this._cache.getBytes(this._cache.getLength()));
              }
              this._emitter.emit(EVENTS.END);
          }
          return needMoreData;
      }
  
      /**
       * Register a reader factory to a mime type.
       * @param {string} mimetype - Mimetype (e.g. multipart/mixed)
       * @param {Function} factory - Factory for the reader which can parse this mime type payload
       * @private
       */
      _addMimeReaderFactories(mimetype, factory) {
          this._mimetypeReaderFactories[mimetype] = factory;
      }
  
      /**
       * Push a reader to the readers stack.
       * @param {Reader} mimetypeReader the reader
       */
      pushReader(mimetypeReader) {
          if (this._mimetypeReader) {
              this._mimetypeReaderStack.push(this._mimetypeReader);
          }
          this._mimetypeReader = mimetypeReader;
      }
  
      /**
       * Pop a reader from the readers stack.
       */
      popReader() {
          this._mimetypeReader = this._mimetypeReaderStack.pop();
          // this._mimetypeReader may be null
      }
  
      /**
       * Determine next reader depending on a content-type header.
       * @param {Object} headers HTTP headers, only "content-type" header is used
       * @returns {Reader} the next reader
       */
      getNextReader(headers) {
          // default content-type as defined in [RFC7231] 3.1.1.5.  Content-Type
          const mimeType = headers['content-type'] && headers['content-type'].getMimeType() || 'application/octet-stream';
  
          const nextReader = this.createReader(mimeType, headers);
          if (!nextReader) throw new DeserializationError('missing mimetype Reader for ' + mimeType);
          return nextReader;
      }
  
      /**
       * Create a reader.
       * @param {string} mimeType type information used to create this reader
       * @param {Object} headers the headers
       * @returns {Reader} the created reader
       */
      createReader(mimeType, headers) {
          let factory = this._mimetypeReaderFactories[mimeType] || this._fallBackReaderFactory;
          return factory(mimeType, headers);
      }
  }
  
  ContentDeserializer.EVENTS = EVENTS;
  
  module.exports = ContentDeserializer;
  