'use strict';

var VJS = VJS || {};
VJS.models = VJS.models || {};

/**
 * Define the stack object here
 *
 * @constructor
 * @class
 * @memberOf VJS.models
 * @public
 */
VJS.models.stack = function() {
  /**
   * @member
   * @type {string}
   */
  this._id = '-1';
  /**
   * @member
   * @type {string}
   */
  this._uid = null; // first stack ID -> (0020, 9056)
  /**
   * @member
   * @type {number}
   */
  this._stackID = -1;
  /**
   * @member
   * @type {Array.<VJS.frame.model>}
   */
  this._frame = [];
  /**
   * @member
   * @type {number}
   */
  this._rows = 0;
  /**
   * @member
   * @type {number}
   */
  this._columns = 0;
  /**
   * @member
   * @type {number}
   */
  this._numberOfFrames = 0;
  /**
   * @member
   * @type {Object}
   * @property {number} row
   * @property {number} column
   */
  this._pixelSpacing = {
    'row': 0,
    'column': 0
  };
  this._spacingBetweenSlices = 0;
  /**
   * @member
   * @type {number}
   */
  this._sliceThickness = 0;

  // origin of the first slice of the stack!
  this._origin = null;
  this._halfDimensions = null;
  this._orientation = null;

  this._textureSize = 4096;
  this._nbTextures = 7; // HIGH RES..
  this._rawData = [];
  // this._windowCenter = 0;
  // this._windowWidth = 0;
  this._windowLevel = [0, 0];
  this._windowCenter = 0;
  this._windowWidth = 0;
  this._minMax = [65535, -32768];
  this._invert = false;

  this._ijk2LPS = null;
  this._lps2IJK = null;

  // Slicer values
  this._dimensions = null;
  this._spacing = null;
  this._origin = null;
  this._direction = null;
};

/**
 * here me make sure eveything is ready for visualization.
 * might also have a switch to say what we can view and what we can not view with current stack
 *
 * @public
 */
VJS.models.stack.prototype.prepare = function() {

  // dimensions of the stack
  this._numberOfFrames = this._frame.length;

  this.orderFrames();
  var zSpacing = this.zSpacing();

  // prepare the frame
  if (this._frame[0]._pixelSpacing) {
    this._pixelSpacing.row = this._frame[0]._pixelSpacing[0];
    this._pixelSpacing.column = this._frame[0]._pixelSpacing[1];
  } else if (this._frame[0]._pixelAspectRatio) {
    this._pixelSpacing.row = 1.0;
    this._pixelSpacing.column = 1.0 * this._frame[0]._pixelAspectRatio[1] / this._frame[0]._pixelAspectRatio[0];
  } else {
    this._pixelSpacing.row = 1.0;
    this._pixelSpacing.column = 1.0;
  }

  if (!this._frame[0]._imagePosition) {
    this._frame[0]._imagePosition = [0, 0, 0];
  }

  if (!this._frame[0]._imageOrientation) {
    this._frame[0]._imageOrientation = [1, 0, 0, 0, 1, 0];
  }

  this._rows = this._frame[0]._rows;
  this._columns = this._frame[0]._columns;
  this._dimensions = new THREE.Vector3(this._columns, this._rows, this._numberOfFrames);

  this._spacingBetweenSlices = this._frame[0]._spacingBetweenSlices;
  this._sliceThickness = this._frame[0]._sliceThickness;

  for (var i = 0; i < this._frame.length; i++) {

    // check rows consistency
    if (this._rows !== this._frame[i]._rows) {
      // send an error message...
      window.console.log('Numbers of rows in stack\'s frames is not consistent.');
      window.console.log(this);
      window.console.log('First frame had: ', this._rows, ' rows');
      window.console.log('Frame index: ', i, ' has: ', this._frame[i]._rows, ' rows.');
    }

    // check columns consitency
    if (this._columns !== this._frame[i]._columns) {
      // send an error message...
      window.console.log('Numbers of columns in stack\'s frames is not consistent.');
      window.console.log(this);
      window.console.log('First frame had: ', this._columns, ' columns.');
      window.console.log('Frame index: ', i, ' has: ', this.frame[i]._columns, ' columns.');
    }

    // // check for spacing consistency
    // if (this._pixelSpacing.row !== this._frame[i]._pixelSpacing[0] || this._pixelSpacing.column !== this._frame[i]._pixelSpacing[1]) {
    //   // send an error message...
    //   window.console.log('Spacing in stack\'s frames is not consistent.');
    //   window.console.log(this);
    //   window.console.log('First frame had : ', this._pixelSpacing.row, ' x ', this._pixelSpacing.column, ' spacing.');
    //   window.console.log('Frame index : ', i, ' has: ', this._frame[i]._pixelSpacing[0], ' x ', this._frame[i]._pixelSpacing[1], ' spacing.');
    // }

    // // check slice spacing consitency
    // if (this._spacingBetweenSlices !== this._frame[i]._spacingBetweenSlices) {
    //   // send an error message...
    //   window.console.log('Spacing betwen slices in stack\'s frames is not consistent.');
    //   window.console.log(this);
    //   window.console.log('First frame had: ', this._spacingBetweenSlices, ' spacing betwen slices.');
    //   window.console.log('Frame index: ', i, ' has: ', this.frame[i]._spacingBetweenSlices, ' spacing betwen slices.');
    // }

    // // check for slice thickness consistency
    // if (this._sliceThickness !== this._frame[i]._sliceThickness) {
    //   window.console.log('Slice thickness in stack\'s frames is not consistent.');
    //   window.console.log(this);
    //   window.console.log('First frame had: ', this._sliceThickness, ' sliceThickness.');
    //   window.console.log('Frame index: ', i, ' has: ', this._frame[i]._sliceThickness, ' sliceThickness.');
    // }

    // get min/max
    this._minMax[0] = Math.min(this._minMax[0], this._frame[i]._minMax[0]);
    this._minMax[1] = Math.max(this._minMax[1], this._frame[i]._minMax[1]);
  }

  // Origin
  this._origin = new THREE.Vector3(
      this._frame[0]._imagePosition[0],
      this._frame[0]._imagePosition[1],
      this._frame[0]._imagePosition[2]
      );

  // Direction
  var xCosine = new THREE.Vector3(
      this._frame[0]._imageOrientation[0],
      this._frame[0]._imageOrientation[1],
      this._frame[0]._imageOrientation[2]
  );

  var yCosine = new THREE.Vector3(
      this._frame[0]._imageOrientation[3],
      this._frame[0]._imageOrientation[4],
      this._frame[0]._imageOrientation[5]
  );
  var zCosine = new THREE.Vector3(0, 0, 0).crossVectors(xCosine, yCosine).normalize();
  this._direction = new THREE.Matrix4();
  this._direction.set(
      xCosine.x, yCosine.x, zCosine.x, 0,
      xCosine.y, yCosine.y, zCosine.y, 0,
      xCosine.z, yCosine.z, zCosine.z, 0,
      0, 0, 0, 1);

  this._spacing = new THREE.Vector3(
      this._pixelSpacing.row,
      this._pixelSpacing.column,
      zSpacing);

  // half dimensions are useful for faster computations of intersection.
  this._halfDimensions = new THREE.Vector3(
      this._dimensions.x / 2, this._dimensions.y / 2, this._dimensions.z / 2);

  // orientation needed to compute stack BBox interection against slice.
  // always same, might want to remove it.
  var baseX = new THREE.Vector3(1, 0, 0);
  var baseY = new THREE.Vector3(0, 1, 0);
  var baseZ = new THREE.Vector3(0, 0, 1);
  this._orientation = new THREE.Vector3(baseX, baseY, baseZ);

  // IJK to LPS transform.
  // and inverse.
  this._ijk2LPS = new THREE.Matrix4();
  this._ijk2LPS.set(
      xCosine.x * this._spacing.x, yCosine.x * this._spacing.y, zCosine.x * this._spacing.z, this._origin.x,
      xCosine.y * this._spacing.x, yCosine.y * this._spacing.y, zCosine.y * this._spacing.z, this._origin.y,
      xCosine.z * this._spacing.x, yCosine.z * this._spacing.y, zCosine.z * this._spacing.z, this._origin.z,
      0, 0, 0, 1);

  this._lps2IJK = new THREE.Matrix4();
  this._lps2IJK.getInverse(this._ijk2LPS);

  // Get total number of voxels
  var nbVoxels = this._dimensions.x * this._dimensions.y * this._dimensions.z;

  window.console.log('start packing data for WebGL texture...');
  console.time('packData');

  // create Uint8 array to be used to generate textures
  var requiredTextures = Math.ceil(nbVoxels / (this._textureSize * this._textureSize));
  for (var ii = 0; ii < requiredTextures; ii++) {
    // *3 because always create RGB
    this._rawData.push(new Uint8Array(this._textureSize * this._textureSize * 4));
  }

  var frameDimension = this._dimensions.x * this._dimensions.y;
  var textureDimension = this._textureSize * this._textureSize;

  // we can compute frame index before hand and have N loops
  for (var jj = 0; jj < nbVoxels; jj++) {

    var frameIndex = ~~(jj / frameDimension);
    var inFrameIndex = jj % (frameDimension);

    var textureIndex = ~~(jj / textureDimension);
    var inTextureIndex = jj % (textureDimension);
    //if (this._numberOfChannels === 3) {

    //  this._rawData[textureIndex][4 * inTextureIndex] = this._frame[frameIndex]._pixelData[4 * inFrameIndex];
    //  this._rawData[textureIndex][4 * inTextureIndex + 1] = this._frame[frameIndex]._pixelData[4 * inFrameIndex + 1];
    //  this._rawData[textureIndex][4 * inTextureIndex + 2] = this._frame[frameIndex]._pixelData[4 * inFrameIndex + 2];
    //  this._rawData[textureIndex][4 * inTextureIndex + 3] = this._frame[frameIndex]._pixelData[4 * inFrameIndex + 3];

    //} else {
      //

      var rawValue = this._frame[frameIndex]._pixelData[inFrameIndex];

      // get most significant (msb) and less significant (lsb) bytes
      // deal with sign?
      // deal with number of channels
      // deal with image type (single/multi channel)
      // >> or >>> ?
      // https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Unsigned_right_shift

      /*jshint bitwise: false*/
      var lsb = rawValue & 0xFF;
      var msb = (rawValue >> 8) & 0xFF;

      // add 
      this._rawData[textureIndex][4 * inTextureIndex] = msb;
      this._rawData[textureIndex][4 * inTextureIndex + 1] = lsb;
      //this._rawData[textureIndex][4 * inTextureIndex + 2] = frameIndex;
      //this._rawData[textureIndex][4 * inTextureIndex + 3] = frameIndex;

    //}

  }

  console.timeEnd('packData');

  // default window level based on min/max for now...
  // could use the frame's windowWidth and center...
  var width = this._minMax[1] - this._minMax[0];
  var center = this._minMax[0] + width / 2;

  this._windowWidth = width;
  this._windowCenter = center;
  this._windowLevel = [center, width];

  // need to pass min/max
  this._bitsAllocated = this._frame[0]._bitsAllocated;

  window.console.log('done preparing stack');
};

/**
 * Order frames based on theirs dimensionIndexValues
 */
VJS.models.stack.prototype.orderFrameOnDimensionIndices = function(a, b) {

  if ('_dimensionIndexValues' in a && Object.prototype.toString.call(a._dimensionIndexValues) === '[object Array]' && '_dimensionIndexValues' in b && Object.prototype.toString.call(b._dimensionIndexValues) === '[object Array]') {
    for (var i = 0; i < a._dimensionIndexValues.length; i++) {
      if (parseInt(a._dimensionIndexValues[i]) > parseInt(b._dimensionIndexValues[i])) {
        //window.console.log(a._dimensionIndexValues[i] + ' > ' + b._dimensionIndexValues[i]);
        //window.console.log(typeof a._dimensionIndexValues[i] + ' > ' + typeof b._dimensionIndexValues[i]);
        return 1;
      }
      if (parseInt(a._dimensionIndexValues[i]) < parseInt(b._dimensionIndexValues[i])) {
        //window.console.log(a._dimensionIndexValues[i] + ' < ' + b._dimensionIndexValues[i]);
        //window.console.log(typeof a._dimensionIndexValues[i] + ' < ' + typeof b._dimensionIndexValues[i]);
        return -1;
      }
    }
  } else {
    window.console.log('One of the frames doesn\'t have a _dimensionIndexValues array.');
    window.console.log(a);
    window.console.log(b);
  }

  return 0;
};

VJS.models.stack.prototype.orderFrames = function() {
  // order the frames based on theirs dimension indices
  // first index is the most important.
  // 1,1,1,1 willl be first
  // 1,1,2,1 will be next
  // 1,1,2,3 will be next
  // 1,1,3,1 wil be next
  if (this._frame[0]._dimensionIndexValues) {
    this._frame.sort(VJS.models.stack.prototype.orderFrameOnDimensionIndices);
  } else if (this._frame[0]._imagePosition && this._frame[0]._imageOrientation) {
    // ORDERING BASED ON IMAGE POSITION
    var xCosine = new THREE.Vector3(
      this._frame[0]._imageOrientation[0],
      this._frame[0]._imageOrientation[1],
      this._frame[0]._imageOrientation[2]
      );

    var yCosine = new THREE.Vector3(
      this._frame[0]._imageOrientation[3],
      this._frame[0]._imageOrientation[4],
      this._frame[0]._imageOrientation[5]
    );

    var zCosine = new THREE.Vector3(0, 0, 0).crossVectors(xCosine, yCosine).normalize();

    // compute and sort by dist in this series
    this._frame.map(this._computeDistance.bind(null, zCosine));
    this._frame.sort(this._sortDistance);

  } else {
    // else slice location
    // image number
    // ORDERING BASED ON instance number
    // _ordering = 'instance_number';
    // first_image.sort(function(a,b){return a["instance_number"]-b["instance_number"]});
  }
};

VJS.models.stack.prototype._computeDistance = function(normal, frame) {
  frame._dist = frame._imagePosition[0] * normal.x +
  frame._imagePosition[1] * normal.y +
  frame._imagePosition[2] * normal.z;
  return frame;
};

VJS.models.stack.prototype._sortDistance = function(a, b) {return a._dist - b._dist;};

VJS.models.stack.prototype.zSpacing = function() {
  // Spacing
  // can not be 0 if not matrix can not be inverted.
  var zSpacing = 1;

  if (this._numberOfFrames > 1) {
    if (this._spacingBetweenSlices) {
      zSpacing = this._spacingBetweenSlices;
    } else if (this._frame[0]._sliceThickness) {
      zSpacing = this._frame[0]._sliceThickness;
    } else {
      var xCosine = new THREE.Vector3(
        this._frame[0]._imageOrientation[0],
        this._frame[0]._imageOrientation[1],
        this._frame[0]._imageOrientation[2]
      );

      var yCosine = new THREE.Vector3(
        this._frame[0]._imageOrientation[3],
        this._frame[0]._imageOrientation[4],
        this._frame[0]._imageOrientation[5]
      );

      var zCosine = new THREE.Vector3(0, 0, 0).crossVectors(xCosine, yCosine).normalize();

      // compute and sort by dist in this series
      this._frame.map(this._computeDistance.bind(null, zCosine));
      this._frame.sort(this._sortDistance);

      zSpacing = this._frame[1]._dist - this._frame[0]._dist;
    }
  }

  if (zSpacing === 0) {
    zSpacing = 1;
  }

  return zSpacing;
};

VJS.models.stack.prototype.merge = function(stack) {
  // try to merge imageHelper with current image.
  // same image if same Series UID?
  // could use concatenation if available, to already know if image is complete!
  var sameStackID = false;
  if (this._stackID === stack._stackID) {
    sameStackID = true;

    // Make sure image information is consisent?
    // re-compute it?
    var frame = stack._frame;
    // Merge Stacks (N against N)
    // try to match all stack to current stacks, if not add it to stacks list!
    for (var i = 0; i < frame.length; i++) {
      // test stack against existing stack
      for (var j = 0; j < this._frame.length; j++) {
        // test dimension
        // dimension index value not defined!
        if (
          
          // dimension index is unique
          (this._frame[j]._dimensionIndexValues &&
            frame[i]._dimensionIndexValues &&
            this._frame[j]._dimensionIndexValues.join() === frame[i]._dimensionIndexValues.join()) ||
          
          // instance number is unique?
          (this._frame[j]._instanceNumber &&
            frame[i]._instanceNumber &&
            this._frame[j]._instanceNumber === frame[i]._instanceNumber) ||
          
          // imagePosition + imageOrientation is unique
          (this._frame[j]._imagePosition &&
            frame[i]._imagePosition &&
            this._frame[j]._imagePosition.join() === frame[i]._imagePosition.join() &&
            this._frame[j]._imageOrientation &&
            frame[i]._imageOrientation &&
            this._frame[j]._imageOrientation.join() === frame[i]._imageOrientation.join() &&
            // FOR DIFFUSION.. same position but different instance numbers...
            this._frame[j]._instanceNumber &&
            frame[i]._instanceNumber &&
            this._frame[j]._instanceNumber === frame[i]._instanceNumber) //||

          // _pixelData length is unique...? imageSOP?
          // (this._frame[j]._pixelData &&
          //   frame[i]._pixelData &&
          //   this._frame[j]._pixelData.length === frame[i]._pixelData.length)

          ) {

          window.console.log('BREAKING!');
          window.console.log(frame[i], this._frame[j]);
          break;
         
        } else if (j === this._frame.length - 1) {

          window.console.log('PUSHING FRAME TO STACK!');
          this._frame.push(frame[i]);
          break;

        }

      }

    }
  }

  return sameStackID;
};



/*** Exports ***/

var moduleType = typeof module;
if ((moduleType !== 'undefined') && module.exports) {
    module.exports = VJS.models.stack;
}