'use strict';

var interlaceUtils = require('./interlace');

function getByteWidth(width, bpp, depth) {
  var byteWidth = width * bpp;
  if (depth !== 8) {
    byteWidth = Math.ceil(byteWidth / (8 / depth));
  }
  return byteWidth;
}

function paethPredictor(left, above, upLeft) {

  var paeth = left + above - upLeft;
  var pLeft = Math.abs(paeth - left);
  var pAbove = Math.abs(paeth - above);
  var pUpLeft = Math.abs(paeth - upLeft);

  if (pLeft <= pAbove && pLeft <= pUpLeft) {
    return left;
  }
  if (pAbove <= pUpLeft) {
    return above;
  }
  return upLeft;
}


var Filter = module.exports = function(width, height, Bpp, depth, interlace, options, dependencies) {

  this._width = width;
  this._height = height;
  this._Bpp = Bpp; //TODO rename
  this._depth = depth;
  this._options = options;

  if (!('filterType' in options) || options.filterType === -1) {
    options.filterType = [0, 1, 2, 3, 4];
  }
  else if (typeof options.filterType === 'number') {
    options.filterType = [options.filterType];
  }

  this._filters = {
    0: this._filterNone.bind(this),
    1: this._filterSub.bind(this),
    2: this._filterUp.bind(this),
    3: this._filterAvg.bind(this),
    4: this._filterPaeth.bind(this)
  };

  this.read = dependencies.read;
  this.write = dependencies.write;
  this.complete = dependencies.complete;

  this._imageIndex = 0;
  this._images = [];
  if (interlace) {
    var passes = interlaceUtils.getImagePasses(width, height);
    for (var i = 0; i < passes.length; i++) {
      this._images.push({
        byteWidth: getByteWidth(passes[i].width, Bpp, depth),
        height: passes[i].height,
        lineIndex: 0
      });
    }
  }
  else {
    this._images.push({
      byteWidth: getByteWidth(width, Bpp, depth),
      height: height,
      lineIndex: 0
    });
  }
};

Filter.prototype.start = function() {
  this.read(this._images[this._imageIndex].byteWidth + 1, this._reverseFilterLine.bind(this));
};

Filter.prototype._reverseFilterLine = function(rawData) {

  var currentImage = this._images[this._imageIndex];
  var line = new Buffer(currentImage.byteWidth);

  var filter = rawData[0];

  // when filtering the line we look at the pixel to the left
  // the spec also says it is done on a byte level regardless of the number of pixels
  // so if the depth is byte compatible (8 or 16) we subtract the bpp in order to compare back
  // a pixel rather than just a different byte part. However if we are sub byte, we ignore.
  var xComparison;
  if (this._depth === 8) {
    xComparison = this._Bpp;
  }
  else if (this._depth === 16) {
    xComparison = this._Bpp * 2;
  }
  else {
    xComparison = 1;
  }
  var xBiggerThan = xComparison - 1;

  for (var x = 0; x < currentImage.byteWidth; x++) {
    var rawByte = rawData[1 + x];
    switch (filter) {
      case 0:
        line[x] = rawByte;
        break;
      case 1:
        var f1Left = x > xBiggerThan ? line[x - xComparison] : 0;
        line[x] = rawByte + f1Left;
        break;
      case 2:
        var f2Up = this._lastLine ? this._lastLine[x] : 0;
        line[x] = rawByte + f2Up;
        break;
      case 3:
        var f3Up = this._lastLine ? this._lastLine[x] : 0;
        var f3Left = x > xBiggerThan ? line[x - xComparison] : 0;
        var f3Add = Math.floor((f3Left + f3Up) / 2);
        line[x] = rawByte + f3Add;
        break;
      case 4:
        var f4Up = this._lastLine ? this._lastLine[x] : 0;
        var f4Left = x > xBiggerThan ? line[x - xComparison] : 0;
        var f4UpLeft = x > xBiggerThan && this._lastLine
          ? this._lastLine[x - xComparison] : 0;
        var f4Add = paethPredictor(f4Left, f4Up, f4UpLeft);
        line[x] = rawByte + f4Add;
        break;
      default:
        throw new Error('Unrecognised filter type - ' + filter);
    }

    //if (x === 5) {
    //    console.log("R", line[3], "G", line[4], "B", line[5]);
    //}
  }

  this.write(line);

  currentImage.lineIndex++;
  if (currentImage.lineIndex >= currentImage.height) {
    this._lastLine = null;
    this._imageIndex++;
    currentImage = this._images[this._imageIndex];
  }
  else {
    this._lastLine = line;
  }

  if (currentImage) {
    this.read(currentImage.byteWidth + 1, this._reverseFilterLine.bind(this));
  }
  else {
    this.complete(this._width, this._height);
  }
};

//TODO pull out
Filter.prototype.filter = function(pxData) {

  var rawData = new Buffer(((this._width << 2) + 1) * this._height);

  for (var y = 0; y < this._height; y++) {

    // find best filter for this line (with lowest sum of values)
    var filterTypes = this._options.filterType;
    var min = Infinity;
    var sel = 0;

    for (var i = 0; i < filterTypes.length; i++) {
      var sum = this._filters[filterTypes[i]](pxData, y, null);
      if (sum < min) {
        sel = filterTypes[i];
        min = sum;
      }
    }

    this._filters[sel](pxData, y, rawData);
  }
  return rawData;
};

Filter.prototype._filterNone = function(pxData, y, rawData) {

  var pxRowLength = this._width << 2;
  var rawRowLength = pxRowLength + 1;
  var sum = 0;

  if (!rawData) {
    for (var x = 0; x < pxRowLength; x++) {
      sum += Math.abs(pxData[y * pxRowLength + x]);
    }

  }
  else {
    rawData[y * rawRowLength] = 0;
    pxData.copy(rawData, rawRowLength * y + 1, pxRowLength * y, pxRowLength * (y + 1));
  }

  return sum;
};

Filter.prototype._filterSub = function(pxData, y, rawData) {

  var pxRowLength = this._width << 2;
  var rawRowLength = pxRowLength + 1;
  var sum = 0;

  if (rawData) {
    rawData[y * rawRowLength] = 1;
  }

  for (var x = 0; x < pxRowLength; x++) {

    var left = x >= 4 ? pxData[y * pxRowLength + x - 4] : 0;
    var val = pxData[y * pxRowLength + x] - left;

    if (!rawData) {
      sum += Math.abs(val);
    }
    else {
      rawData[y * rawRowLength + 1 + x] = val;
    }
  }
  return sum;
};

Filter.prototype._filterUp = function(pxData, y, rawData) {

  var pxRowLength = this._width << 2;
  var rawRowLength = pxRowLength + 1;
  var sum = 0;

  if (rawData) {
    rawData[y * rawRowLength] = 2;
  }

  for (var x = 0; x < pxRowLength; x++) {

    var up = y > 0 ? pxData[(y - 1) * pxRowLength + x] : 0;
    var val = pxData[y * pxRowLength + x] - up;

    if (!rawData) {
      sum += Math.abs(val);
    }
    else {
      rawData[y * rawRowLength + 1 + x] = val;
    }
  }
  return sum;
};

Filter.prototype._filterAvg = function(pxData, y, rawData) {

  var pxRowLength = this._width << 2;
  var rawRowLength = pxRowLength + 1;
  var sum = 0;

  if (rawData) {
    rawData[y * rawRowLength] = 3;
  }

  for (var x = 0; x < pxRowLength; x++) {

    var left = x >= 4 ? pxData[y * pxRowLength + x - 4] : 0;
    var up = y > 0 ? pxData[(y - 1) * pxRowLength + x] : 0;
    var val = pxData[y * pxRowLength + x] - ((left + up) >> 1);

    if (!rawData) {
      sum += Math.abs(val);
    }
    else {
      rawData[y * rawRowLength + 1 + x] = val;
    }
  }
  return sum;
};

Filter.prototype._filterPaeth = function(pxData, y, rawData) {

  var pxRowLength = this._width << 2;
  var rawRowLength = pxRowLength + 1;
  var sum = 0;

  if (rawData) {
    rawData[y * rawRowLength] = 4;
  }

  for (var x = 0; x < pxRowLength; x++) {

    var left = x >= 4 ? pxData[y * pxRowLength + x - 4] : 0;
    var up = y > 0 ? pxData[(y - 1) * pxRowLength + x] : 0;
    var upLeft = x >= 4 && y > 0 ? pxData[(y - 1) * pxRowLength + x - 4] : 0;
    var val = pxData[y * pxRowLength + x] - paethPredictor(left, up, upLeft);

    if (!rawData) {
      sum += Math.abs(val);
    }
    else {
      rawData[y * rawRowLength + 1 + x] = val;
    }
  }
  return sum;
};
