text: /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
  /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
  /* Copyright 2012 Mozilla Foundation
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  /* globals assert, MissingDataException, isInt, NetworkManager, Promise,
             isEmptyObj, createPromiseCapability */
  
  'use strict';
  
  var ChunkedStream = (function ChunkedStreamClosure() {
    function ChunkedStream(length, chunkSize, manager) {
      this.bytes = new Uint8Array(length);
      this.start = 0;
      this.pos = 0;
      this.end = length;
      this.chunkSize = chunkSize;
      this.loadedChunks = [];
      this.numChunksLoaded = 0;
      this.numChunks = Math.ceil(length / chunkSize);
      this.manager = manager;
      this.progressiveDataLength = 0;
      this.lastSuccessfulEnsureByteChunk = -1;  // a single-entry cache
    }
  
    // required methods for a stream. if a particular stream does not
    // implement these, an error should be thrown
    ChunkedStream.prototype = {
  
      getMissingChunks: function ChunkedStream_getMissingChunks() {
        var chunks = [];
        for (var chunk = 0, n = this.numChunks; chunk < n; ++chunk) {
          if (!this.loadedChunks[chunk]) {
            chunks.push(chunk);
          }
        }
        return chunks;
      },
  
      getBaseStreams: function ChunkedStream_getBaseStreams() {
        return [this];
      },
  
      allChunksLoaded: function ChunkedStream_allChunksLoaded() {
        return this.numChunksLoaded === this.numChunks;
      },
  
      onReceiveData: function ChunkedStream_onReceiveData(begin, chunk) {
        var end = begin + chunk.byteLength;
  
        assert(begin % this.chunkSize === 0, 'Bad begin offset: ' + begin);
        // Using this.length is inaccurate here since this.start can be moved
        // See ChunkedStream.moveStart()
        var length = this.bytes.length;
        assert(end % this.chunkSize === 0 || end === length,
               'Bad end offset: ' + end);
  
        this.bytes.set(new Uint8Array(chunk), begin);
        var chunkSize = this.chunkSize;
        var beginChunk = Math.floor(begin / chunkSize);
        var endChunk = Math.floor((end - 1) / chunkSize) + 1;
        var curChunk;
  
        for (curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
          if (!this.loadedChunks[curChunk]) {
            this.loadedChunks[curChunk] = true;
            ++this.numChunksLoaded;
          }
        }
      },
  
      onReceiveProgressiveData:
          function ChunkedStream_onReceiveProgressiveData(data) {
        var position = this.progressiveDataLength;
        var beginChunk = Math.floor(position / this.chunkSize);
  
        this.bytes.set(new Uint8Array(data), position);
        position += data.byteLength;
        this.progressiveDataLength = position;
        var endChunk = position >= this.end ? this.numChunks :
                       Math.floor(position / this.chunkSize);
        var curChunk;
        for (curChunk = beginChunk; curChunk < endChunk; ++curChunk) {
          if (!this.loadedChunks[curChunk]) {
            this.loadedChunks[curChunk] = true;
            ++this.numChunksLoaded;
          }
        }
      },
  
      ensureByte: function ChunkedStream_ensureByte(pos) {
        var chunk = Math.floor(pos / this.chunkSize);
        if (chunk === this.lastSuccessfulEnsureByteChunk) {
          return;
        }
  
        if (!this.loadedChunks[chunk]) {
          throw new MissingDataException(pos, pos + 1);
        }
        this.lastSuccessfulEnsureByteChunk = chunk;
      },
  
      ensureRange: function ChunkedStream_ensureRange(begin, end) {
        if (begin >= end) {
          return;
        }
  
        if (end <= this.progressiveDataLength) {
          return;
        }
  
        var chunkSize = this.chunkSize;
        var beginChunk = Math.floor(begin / chunkSize);
        var endChunk = Math.floor((end - 1) / chunkSize) + 1;
        for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
          if (!this.loadedChunks[chunk]) {
            throw new MissingDataException(begin, end);
          }
        }
      },
  
      nextEmptyChunk: function ChunkedStream_nextEmptyChunk(beginChunk) {
        var chunk, n;
        for (chunk = beginChunk, n = this.numChunks; chunk < n; ++chunk) {
          if (!this.loadedChunks[chunk]) {
            return chunk;
          }
        }
        // Wrap around to beginning
        for (chunk = 0; chunk < beginChunk; ++chunk) {
          if (!this.loadedChunks[chunk]) {
            return chunk;
          }
        }
        return null;
      },
  
      hasChunk: function ChunkedStream_hasChunk(chunk) {
        return !!this.loadedChunks[chunk];
      },
  
      get length() {
        return this.end - this.start;
      },
  
      get isEmpty() {
        return this.length === 0;
      },
  
      getByte: function ChunkedStream_getByte() {
        var pos = this.pos;
        if (pos >= this.end) {
          return -1;
        }
        this.ensureByte(pos);
        return this.bytes[this.pos++];
      },
  
      getUint16: function ChunkedStream_getUint16() {
        var b0 = this.getByte();
        var b1 = this.getByte();
        return (b0 << 8) + b1;
      },
  
      getInt32: function ChunkedStream_getInt32() {
        var b0 = this.getByte();
        var b1 = this.getByte();
        var b2 = this.getByte();
        var b3 = this.getByte();
        return (b0 << 24) + (b1 << 16) + (b2 << 8) + b3;
      },
  
      // returns subarray of original buffer
      // should only be read
      getBytes: function ChunkedStream_getBytes(length) {
        var bytes = this.bytes;
        var pos = this.pos;
        var strEnd = this.end;
  
        if (!length) {
          this.ensureRange(pos, strEnd);
          return bytes.subarray(pos, strEnd);
        }
  
        var end = pos + length;
        if (end > strEnd) {
          end = strEnd;
        }
        this.ensureRange(pos, end);
  
        this.pos = end;
        return bytes.subarray(pos, end);
      },
  
      peekByte: function ChunkedStream_peekByte() {
        var peekedByte = this.getByte();
        this.pos--;
        return peekedByte;
      },
  
      peekBytes: function ChunkedStream_peekBytes(length) {
        var bytes = this.getBytes(length);
        this.pos -= bytes.length;
        return bytes;
      },
  
      getByteRange: function ChunkedStream_getBytes(begin, end) {
        this.ensureRange(begin, end);
        return this.bytes.subarray(begin, end);
      },
  
      skip: function ChunkedStream_skip(n) {
        if (!n) {
          n = 1;
        }
        this.pos += n;
      },
  
      reset: function ChunkedStream_reset() {
        this.pos = this.start;
      },
  
      moveStart: function ChunkedStream_moveStart() {
        this.start = this.pos;
      },
  
      makeSubStream: function ChunkedStream_makeSubStream(start, length, dict) {
        this.ensureRange(start, start + length);
  
        function ChunkedStreamSubstream() {}
        ChunkedStreamSubstream.prototype = Object.create(this);
        ChunkedStreamSubstream.prototype.getMissingChunks = function() {
          var chunkSize = this.chunkSize;
          var beginChunk = Math.floor(this.start / chunkSize);
          var endChunk = Math.floor((this.end - 1) / chunkSize) + 1;
          var missingChunks = [];
          for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
            if (!this.loadedChunks[chunk]) {
              missingChunks.push(chunk);
            }
          }
          return missingChunks;
        };
        var subStream = new ChunkedStreamSubstream();
        subStream.pos = subStream.start = start;
        subStream.end = start + length || this.end;
        subStream.dict = dict;
        return subStream;
      },
  
      isStream: true
    };
  
    return ChunkedStream;
  })();
  
  var ChunkedStreamManager = (function ChunkedStreamManagerClosure() {
  
    function ChunkedStreamManager(length, chunkSize, url, args) {
      this.stream = new ChunkedStream(length, chunkSize, this);
      this.length = length;
      this.chunkSize = chunkSize;
      this.url = url;
      this.disableAutoFetch = args.disableAutoFetch;
      var msgHandler = this.msgHandler = args.msgHandler;
  
      if (args.chunkedViewerLoading) {
        msgHandler.on('OnDataRange', this.onReceiveData.bind(this));
        msgHandler.on('OnDataProgress', this.onProgress.bind(this));
        this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) {
          msgHandler.send('RequestDataRange', { begin: begin, end: end });
        };
      } else {
  
        var getXhr = function getXhr() {
  //#if B2G
  //      return new XMLHttpRequest({ mozSystem: true });
  //#else
          return new XMLHttpRequest();
  //#endif
        };
        this.networkManager = new NetworkManager(this.url, {
          getXhr: getXhr,
          httpHeaders: args.httpHeaders,
          withCredentials: args.withCredentials
        });
        this.sendRequest = function ChunkedStreamManager_sendRequest(begin, end) {
          this.networkManager.requestRange(begin, end, {
            onDone: this.onReceiveData.bind(this),
            onProgress: this.onProgress.bind(this)
          });
        };
      }
  
      this.currRequestId = 0;
  
      this.chunksNeededByRequest = {};
      this.requestsByChunk = {};
      this.callbacksByRequest = {};
      this.progressiveDataLength = 0;
  
      this._loadedStreamCapability = createPromiseCapability();
  
      if (args.initialData) {
        this.onReceiveData({chunk: args.initialData});
      }
    }
  
    ChunkedStreamManager.prototype = {
      onLoadedStream: function ChunkedStreamManager_getLoadedStream() {
        return this._loadedStreamCapability.promise;
      },
  
      // Get all the chunks that are not yet loaded and groups them into
      // contiguous ranges to load in as few requests as possible
      requestAllChunks: function ChunkedStreamManager_requestAllChunks() {
        var missingChunks = this.stream.getMissingChunks();
        this.requestChunks(missingChunks);
        return this._loadedStreamCapability.promise;
      },
  
      requestChunks: function ChunkedStreamManager_requestChunks(chunks,
                                                                 callback) {
        var requestId = this.currRequestId++;
  
        var chunksNeeded;
        var i, ii;
        this.chunksNeededByRequest[requestId] = chunksNeeded = {};
        for (i = 0, ii = chunks.length; i < ii; i++) {
          if (!this.stream.hasChunk(chunks[i])) {
            chunksNeeded[chunks[i]] = true;
          }
        }
  
        if (isEmptyObj(chunksNeeded)) {
          if (callback) {
            callback();
          }
          return;
        }
  
        this.callbacksByRequest[requestId] = callback;
  
        var chunksToRequest = [];
        for (var chunk in chunksNeeded) {
          chunk = chunk | 0;
          if (!(chunk in this.requestsByChunk)) {
            this.requestsByChunk[chunk] = [];
            chunksToRequest.push(chunk);
          }
          this.requestsByChunk[chunk].push(requestId);
        }
  
        if (!chunksToRequest.length) {
          return;
        }
  
        var groupedChunksToRequest = this.groupChunks(chunksToRequest);
  
        for (i = 0; i < groupedChunksToRequest.length; ++i) {
          var groupedChunk = groupedChunksToRequest[i];
          var begin = groupedChunk.beginChunk * this.chunkSize;
          var end = Math.min(groupedChunk.endChunk * this.chunkSize, this.length);
          this.sendRequest(begin, end);
        }
      },
  
      getStream: function ChunkedStreamManager_getStream() {
        return this.stream;
      },
  
      // Loads any chunks in the requested range that are not yet loaded
      requestRange: function ChunkedStreamManager_requestRange(
                        begin, end, callback) {
  
        end = Math.min(end, this.length);
  
        var beginChunk = this.getBeginChunk(begin);
        var endChunk = this.getEndChunk(end);
  
        var chunks = [];
        for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
          chunks.push(chunk);
        }
  
        this.requestChunks(chunks, callback);
      },
  
      requestRanges: function ChunkedStreamManager_requestRanges(ranges,
                                                                 callback) {
        ranges = ranges || [];
        var chunksToRequest = [];
  
        for (var i = 0; i < ranges.length; i++) {
          var beginChunk = this.getBeginChunk(ranges[i].begin);
          var endChunk = this.getEndChunk(ranges[i].end);
          for (var chunk = beginChunk; chunk < endChunk; ++chunk) {
            if (chunksToRequest.indexOf(chunk) < 0) {
              chunksToRequest.push(chunk);
            }
          }
        }
  
        chunksToRequest.sort(function(a, b) { return a - b; });
        this.requestChunks(chunksToRequest, callback);
      },
  
      // Groups a sorted array of chunks into as few continguous larger
      // chunks as possible
      groupChunks: function ChunkedStreamManager_groupChunks(chunks) {
        var groupedChunks = [];
        var beginChunk = -1;
        var prevChunk = -1;
        for (var i = 0; i < chunks.length; ++i) {
          var chunk = chunks[i];
  
          if (beginChunk < 0) {
            beginChunk = chunk;
          }
  
          if (prevChunk >= 0 && prevChunk + 1 !== chunk) {
            groupedChunks.push({ beginChunk: beginChunk,
                                 endChunk: prevChunk + 1 });
            beginChunk = chunk;
          }
          if (i + 1 === chunks.length) {
            groupedChunks.push({ beginChunk: beginChunk,
                                 endChunk: chunk + 1 });
          }
  
          prevChunk = chunk;
        }
        return groupedChunks;
      },
  
      onProgress: function ChunkedStreamManager_onProgress(args) {
        var bytesLoaded = (this.stream.numChunksLoaded * this.chunkSize +
                           args.loaded);
        this.msgHandler.send('DocProgress', {
          loaded: bytesLoaded,
          total: this.length
        });
      },
  
      onReceiveData: function ChunkedStreamManager_onReceiveData(args) {
        var chunk = args.chunk;
        var isProgressive = args.begin === undefined;
        var begin = isProgressive ? this.progressiveDataLength : args.begin;
        var end = begin + chunk.byteLength;
  
        var beginChunk = Math.floor(begin / this.chunkSize);
        var endChunk = end < this.length ? Math.floor(end / this.chunkSize) :
                                           Math.ceil(end / this.chunkSize);
  
        if (isProgressive) {
          this.stream.onReceiveProgressiveData(chunk);
          this.progressiveDataLength = end;
        } else {
          this.stream.onReceiveData(begin, chunk);
        }
  
        if (this.stream.allChunksLoaded()) {
          this._loadedStreamCapability.resolve(this.stream);
        }
  
        var loadedRequests = [];
        var i, requestId;
        for (chunk = beginChunk; chunk < endChunk; ++chunk) {
          // The server might return more chunks than requested
          var requestIds = this.requestsByChunk[chunk] || [];
          delete this.requestsByChunk[chunk];
  
          for (i = 0; i < requestIds.length; ++i) {
            requestId = requestIds[i];
            var chunksNeeded = this.chunksNeededByRequest[requestId];
            if (chunk in chunksNeeded) {
              delete chunksNeeded[chunk];
            }
  
            if (!isEmptyObj(chunksNeeded)) {
              continue;
            }
  
            loadedRequests.push(requestId);
          }
        }
  
        // If there are no pending requests, automatically fetch the next
        // unfetched chunk of the PDF
        if (!this.disableAutoFetch && isEmptyObj(this.requestsByChunk)) {
          var nextEmptyChunk;
          if (this.stream.numChunksLoaded === 1) {
            // This is a special optimization so that after fetching the first
            // chunk, rather than fetching the second chunk, we fetch the last
            // chunk.
            var lastChunk = this.stream.numChunks - 1;
            if (!this.stream.hasChunk(lastChunk)) {
              nextEmptyChunk = lastChunk;
            }
          } else {
            nextEmptyChunk = this.stream.nextEmptyChunk(endChunk);
          }
          if (isInt(nextEmptyChunk)) {
            this.requestChunks([nextEmptyChunk]);
          }
        }
  
        for (i = 0; i < loadedRequests.length; ++i) {
          requestId = loadedRequests[i];
          var callback = this.callbacksByRequest[requestId];
          delete this.callbacksByRequest[requestId];
          if (callback) {
            callback();
          }
        }
  
        this.msgHandler.send('DocProgress', {
          loaded: this.stream.numChunksLoaded * this.chunkSize,
          total: this.length
        });
      },
  
      onError: function ChunkedStreamManager_onError(err) {
        this._loadedStreamCapability.reject(err);
      },
  
      getBeginChunk: function ChunkedStreamManager_getBeginChunk(begin) {
        var chunk = Math.floor(begin / this.chunkSize);
        return chunk;
      },
  
      getEndChunk: function ChunkedStreamManager_getEndChunk(end) {
        if (end % this.chunkSize === 0) {
          return end / this.chunkSize;
        }
  
        // 0 -> 0
        // 1 -> 1
        // 99 -> 1
        // 100 -> 1
        // 101 -> 2
        var chunk = Math.floor((end - 1) / this.chunkSize) + 1;
        return chunk;
      }
    };
  
    return ChunkedStreamManager;
  })();
  
  