text: 'use strict';
  
  // A.2.1.
  
  /**
   * Functions to help with checking and adding data to databins
   * @param {number} classId - type of databin it is (what number?)
   * @param {number} inClassId - type of databin...?
   * @param {object} jpipFactory
   * @returns {object}
   */
  module.exports = function JpipDatabinParts(
      classId, inClassId, jpipFactory) {
  
      var self = this;
  
      var parts = []; // ??
      var databinLengthIfKnown = null; // Length of databin
      var loadedBytes = 0; // Loaded bytes
      
      var cachedData = []; // What data is cached
      
      this.getDatabinLengthIfKnown = function() {
          return databinLengthIfKnown;
      };
      
      this.getLoadedBytes = function getLoadedBytes() {
          return loadedBytes;
      };
      
      // Returns true if all databins are loaded - to what though??
      this.isAllDatabinLoaded = function isAllDatabinLoaded() {
          var result;
          
          switch (parts.length) {
              case 0:
                  result = databinLengthIfKnown === 0;
                  break;
                  
              case 1:
                  result =
                      parts[0].getOffset() === 0 &&
                      parts[0].getLength() === databinLengthIfKnown;
                  break;
              
              default:
                  result = false;
                  break;
          }
          
          return result;
      };
      
      // Return cached data as object
      this.getCachedData = function getCachedData(key) {
          var obj = cachedData[key];
          if (obj === undefined) {
              obj = {};
              cachedData[key] = obj;
          }
          
          return obj;
      };
      
      this.getClassId = function getClassId() {
          return classId;
      };
      
      this.getInClassId = function getInClassId() {
          return inClassId;
      };
      
      this.copyToCompositeArray = function copyToCompositeArray(result, rangeOptions) {
          var dummyResultStartOffset = 0;
          var params = getParamsForCopyBytes(dummyResultStartOffset, rangeOptions);
          
          if (params.resultWithoutCopy !== undefined) {
              return params.resultWithoutCopy;
          }
          
          var maxLengthCopied = iterateRange(
              params.databinStartOffset,
              params.maxLengthToCopy,
              function addPartToResultInCopyToCompositeArray(part, minOffsetInPart, maxOffsetInPart) {
                  part.copyToOtherAtTheEnd(
                      result,
                      minOffsetInPart,
                      maxOffsetInPart);
              });
          
          return maxLengthCopied;
      };
      
      this.copyBytes = function(resultArray, resultStartOffset, rangeOptions) {
          var params = getParamsForCopyBytes(resultStartOffset, rangeOptions);
          
          if (params.resultWithoutCopy !== undefined) {
              return params.resultWithoutCopy;
          }
          
          var resultArrayOffsetInDatabin = params.databinStartOffset - params.resultStartOffset;
          
          var actualCopyBytes = resultArray.isDummyBufferForLengthCalculation ?
              function() { } :
              function addPartToResultInCopyBytes(part, minOffsetInPart, maxOffsetInPart) {
                  part.copyToArray(
                      resultArray,
                      resultArrayOffsetInDatabin,
                      minOffsetInPart,
                      maxOffsetInPart);
              };
              
          var maxLengthCopied = iterateRange(
              params.databinStartOffset,
              params.maxLengthToCopy,
              actualCopyBytes);
          
          return maxLengthCopied;
      };
      
      this.getExistingRanges = function() {
          var result = new Array(parts.length);
          
          for (var i = 0; i < parts.length; ++i) {
              result[i] = {
                  start: parts[i].getOffset(),
                  length: parts[i].getLength()
                  };
          }
          
          return result;
      };
      
      // Add data to databin
      this.addData = function(header, message) {
          if (header.isLastByteInDatabin) {
              databinLengthIfKnown = header.messageOffsetFromDatabinStart + header.messageBodyLength;
          }
          
          if (header.messageBodyLength === 0) {
              return;
          }
  
          var newPart = jpipFactory.createCompositeArray(
              header.messageOffsetFromDatabinStart);
  
          var endOffsetInMessage = header.bodyStart + header.messageBodyLength;
          newPart.pushSubArray(message.subarray(header.bodyStart, endOffsetInMessage));
  
          // Find where to push the new message
          
          var indexFirstPartAfter = findFirstPartAfterOffset(header.messageOffsetFromDatabinStart);
          var indexFirstPartNearOrAfter = indexFirstPartAfter;
  
          if (indexFirstPartAfter > 0) {
              var previousPart = parts[indexFirstPartAfter - 1];
              var previousPartEndOffset =
                  previousPart.getOffset() + previousPart.getLength();
              
              if (previousPartEndOffset === header.messageOffsetFromDatabinStart) {
                  // Can merge also previous part
                  --indexFirstPartNearOrAfter;
              }
          }
  
          if (indexFirstPartNearOrAfter >= parts.length) {
              parts.push(newPart);
              loadedBytes += header.messageBodyLength;
              
              return;
          }
          
          var firstPartNearOrAfter = parts[indexFirstPartNearOrAfter];
          var endOffsetInDatabin =
              header.messageOffsetFromDatabinStart + header.messageBodyLength;
          if (firstPartNearOrAfter.getOffset() > endOffsetInDatabin) {
              // Not found an overlapping part, push a new
              // part in the middle of the parts array
                  
              for (var i = parts.length; i > indexFirstPartNearOrAfter; --i) {
                  parts[i] = parts[i - 1];
              }
              
              parts[indexFirstPartNearOrAfter] = newPart;
              loadedBytes += header.messageBodyLength;
  
              return;
          }
          
          // Merge first and last overlapping parts - all the rest (if any) are in the middle of the new part
          
          var bytesAlreadySaved = firstPartNearOrAfter.getLength();
  
          var shouldSwap =
              firstPartNearOrAfter.getOffset() > header.messageOffsetFromDatabinStart;
          if (shouldSwap) {
              parts[indexFirstPartNearOrAfter] = newPart;
              newPart = firstPartNearOrAfter;
              
              firstPartNearOrAfter = parts[indexFirstPartNearOrAfter];
          }
  
          newPart.copyToOther(firstPartNearOrAfter);
          
          var endOffset =
              firstPartNearOrAfter.getOffset() + firstPartNearOrAfter.getLength();
          
          var partToMergeIndex;
          for (partToMergeIndex = indexFirstPartNearOrAfter;
              partToMergeIndex < parts.length - 1;
              ++partToMergeIndex) {
              
              if (endOffset < parts[partToMergeIndex + 1].getOffset()) {
                  break;
              }
              
              bytesAlreadySaved += parts[partToMergeIndex + 1].getLength();
          }
          
          var partsToDelete = partToMergeIndex - indexFirstPartNearOrAfter;
          if (partsToDelete > 0) {
              parts[partToMergeIndex].copyToOther(firstPartNearOrAfter);
              
              // Delete all middle and merged parts except 1
              
              for (var j = indexFirstPartNearOrAfter + 1; j < parts.length - partsToDelete; ++j) {
                  parts[j] = parts[j + partsToDelete];
              }
              
              parts.length -= partsToDelete;
          }
          
          loadedBytes += firstPartNearOrAfter.getLength() - bytesAlreadySaved;
      };
      
      function getParamsForCopyBytes(resultStartOffset, rangeOptions) {
          var forceCopyAllRange = false;
          var databinStartOffset = 0;
          var maxLengthToCopy;
          
          if (rangeOptions !== undefined) {
              forceCopyAllRange = !!rangeOptions.forceCopyAllRange;
              databinStartOffset = rangeOptions.databinStartOffset;
              maxLengthToCopy = rangeOptions.maxLengthToCopy;
              
              if (databinStartOffset === undefined) {
                  databinStartOffset = 0;
              }
          }
          
          if (resultStartOffset === undefined) {
              resultStartOffset = 0;
          }
          
          if (maxLengthToCopy === 0) {
              return { resultWithoutCopy: 0 };
          }
          
          if ((databinLengthIfKnown !== null) && (databinStartOffset >= databinLengthIfKnown)) {
              return { resultWithoutCopy: (!!maxLengthToCopy && forceCopyAllRange ? null : 0) };
          }
          
          var firstRelevantPartIndex = findFirstPartAfterOffset(databinStartOffset);
          
          if (firstRelevantPartIndex === parts.length) {
              return { resultWithoutCopy: (forceCopyAllRange ? null : 0) };
          }
          
          if (forceCopyAllRange) {
              var isAllRequestedRangeExist =
                  isAllRangeExist(databinStartOffset, maxLengthToCopy, firstRelevantPartIndex);
              
              if (!isAllRequestedRangeExist) {
                  return { resultWithoutCopy: null };
              }
          }
          
          var params = {
              databinStartOffset: databinStartOffset,
              maxLengthToCopy: maxLengthToCopy,
              resultStartOffset: resultStartOffset
              };
          
          return params;
      }
      
      function isAllRangeExist(
          databinStartOffset, maxLengthToCopy, firstRelevantPartIndex) {
          
          if (parts[firstRelevantPartIndex].getOffset() > databinStartOffset) {
              return false;
          }
          
          if (maxLengthToCopy) {
              var unusedElements =
                  databinStartOffset - parts[firstRelevantPartIndex].getOffset();
              var availableLength =
                  parts[firstRelevantPartIndex].getLength() - unusedElements;
              
              var isUntilMaxLengthExist = availableLength >= maxLengthToCopy;
              return isUntilMaxLengthExist;
          }
          
          if (databinLengthIfKnown === null ||
              firstRelevantPartIndex < parts.length - 1) {
              
              return false;
          }
          
          var lastPart = parts[parts.length - 1];
          var endOffsetRecieved = lastPart.getOffset() + lastPart.getLength();
          
          var isUntilEndOfDatabinExist = endOffsetRecieved === databinLengthIfKnown;
          return isUntilEndOfDatabinExist;
      }
      
      function iterateRange(
              databinStartOffset,
              maxLengthToCopy,
          addSubPartToResult) {
          
          var minOffsetInDatabinToCopy = databinStartOffset;
          
          var maxOffsetInDatabinToCopy;
          if (maxLengthToCopy !== undefined) {
              maxOffsetInDatabinToCopy = databinStartOffset + maxLengthToCopy;
          } else {
              var lastPart = parts[parts.length - 1];
              maxOffsetInDatabinToCopy = lastPart.getOffset() + lastPart.getLength();
          }
                  
          var lastCopiedPart = null;
          
          for (var i = 0; i < parts.length; ++i) {
              if (parts[i].getOffset() >= maxOffsetInDatabinToCopy) {
                  break;
              }
              
              var currentMinOffsetInDatabinToCopy = Math.max(
                  minOffsetInDatabinToCopy, parts[i].getOffset());
              var currentMaxOffsetInDatabinToCopy = Math.min(
                  maxOffsetInDatabinToCopy, parts[i].getOffset() + parts[i].getLength());
          
              addSubPartToResult(
                  parts[i],
                  currentMinOffsetInDatabinToCopy,
                  currentMaxOffsetInDatabinToCopy);
              
              lastCopiedPart = parts[i];
          }
          
          if (lastCopiedPart === null) {
              return 0;
          }
          
          var lastOffsetCopied = Math.min(
              lastCopiedPart.getOffset() + lastCopiedPart.getLength(),
              maxOffsetInDatabinToCopy);
          
          var maxLengthCopied = lastOffsetCopied - databinStartOffset;
          return maxLengthCopied;
      }
  
      function findFirstPartAfterOffset(offset) {
          var index;
          for (index = 0; index < parts.length; ++index) {
              if (parts[index].getOffset() + parts[index].getLength() > offset) {
                  break;
              }
          }
          
          return index;
      }
      
      return this;
  };