// Copyright (c) 2012 Kuba Niegowski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

'use strict';


var constants = require('./constants'),
  CrcCalculator = require('./crc');


var Parser = module.exports = function(options, dependencies) {

  this._options = options;
  options.checkCRC = options.checkCRC !== false;

  this._hasIHDR = false;
  this._hasIEND = false;

  // input flags/metadata
  this._palette = [];
  this._colorType = 0;

  this._chunks = {};
  this._chunks[constants.TYPE_IHDR] = this._handleIHDR.bind(this);
  this._chunks[constants.TYPE_IEND] = this._handleIEND.bind(this);
  this._chunks[constants.TYPE_IDAT] = this._handleIDAT.bind(this);
  this._chunks[constants.TYPE_PLTE] = this._handlePLTE.bind(this);
  this._chunks[constants.TYPE_tRNS] = this._handleTRNS.bind(this);
  this._chunks[constants.TYPE_gAMA] = this._handleGAMA.bind(this);

  this.read = dependencies.read;
  this.error = dependencies.error;
  this.metadata = dependencies.metadata;
  this.gamma = dependencies.gamma;
  this.parsed = dependencies.parsed;
  this.createData = dependencies.createData;
  this.inflateData = dependencies.inflateData;
    this.finished = dependencies.finished;
};

var colorTypeToBppMap = {
  0: 1,
  2: 3,
  3: 1,
  4: 2,
  6: 4
};

Parser.prototype.start = function() {
  this.read(constants.PNG_SIGNATURE.length,
    this._parseSignature.bind(this)
  );
};

Parser.prototype._parseSignature = function(data) {

  var signature = constants.PNG_SIGNATURE;

  for (var i = 0; i < signature.length; i++) {
    if (data[i] != signature[i]) {
      this.error(new Error('Invalid file signature'));
      return;
    }
  }
  this.read(8, this._parseChunkBegin.bind(this));
};

Parser.prototype._parseChunkBegin = function(data) {

  // chunk content length
  var length = data.readUInt32BE(0);

  // chunk type
  var type = data.readUInt32BE(4),
    name = '';
  for (var i = 4; i < 8; i++) {
    name += String.fromCharCode(data[i]);
  }

  //console.log('chunk ', name, length);

  // chunk flags
  var ancillary  = !!(data[4] & 0x20),  // or critical
    priv       = !!(data[5] & 0x20),  // or public
    safeToCopy = !!(data[7] & 0x20);  // or unsafe

  if (!this._hasIHDR && type != constants.TYPE_IHDR) {
    this.error(new Error('Expected IHDR on beggining'));
    return;
  }

  this._crc = new CrcCalculator();
  this._crc.write(new Buffer(name));

  if (this._chunks[type]) {
    return this._chunks[type](length);

  } else if (!ancillary) {
    this.error(new Error('Unsupported critical chunk type ' + name));
    return;
  } else {
    this.read(length + 4, this._skipChunk.bind(this));
  }
};

Parser.prototype._skipChunk = function(data) {
  this.read(8, this._parseChunkBegin.bind(this));
};

Parser.prototype._handleChunkEnd = function() {
  this.read(4, this._parseChunkEnd.bind(this));
};

Parser.prototype._parseChunkEnd = function(data) {

  var fileCrc = data.readInt32BE(0),
    calcCrc = this._crc.crc32();

  // check CRC
  if (this._options.checkCRC && calcCrc != fileCrc) {
    this.error(new Error('Crc error - ' + fileCrc + " - " + calcCrc));
    return;
  }

  if (!this._hasIEND) {
    this.read(8, this._parseChunkBegin.bind(this));
  }
};

Parser.prototype._handleIHDR = function(length) {
  this.read(length, this._parseIHDR.bind(this));
};
Parser.prototype._parseIHDR = function(data) {

  this._crc.write(data);

  var width = data.readUInt32BE(0),
    height = data.readUInt32BE(4),
    depth = data[8],
    colorType = data[9], // bits: 1 palette, 2 color, 4 alpha
    compr = data[10],
    filter = data[11],
    interlace = data[12];

  // console.log('    width', width, 'height', height,
  //     'depth', depth, 'colorType', colorType,
  //     'compr', compr, 'filter', filter, 'interlace', interlace
  // );

  if (depth !== 8 && depth !== 4 && depth !== 2 && depth !== 1 && depth !== 16) {
    this.error(new Error('Unsupported bit depth ' + depth));
    return;
  }
  if (!(colorType in colorTypeToBppMap)) {
    this.error(new Error('Unsupported color type'));
    return;
  }
  if (compr != 0) {
    this.error(new Error('Unsupported compression method'));
    return;
  }
  if (filter != 0) {
    this.error(new Error('Unsupported filter method'));
    return;
  }
  if (interlace != 0 && interlace !== 1) {
    this.error(new Error('Unsupported interlace method'));
    return;
  }

  this._colorType = colorType;

  var bpp = colorTypeToBppMap[this._colorType];
  this.createData(width, height, bpp, depth, interlace);

  this._hasIHDR = true;

  this.metadata({
    width: width,
    height: height,
    depth: depth,
    interlace: interlace,
    palette: !!(colorType & constants.COLOR_PALETTE),
    color: !!(colorType & constants.COLOR_COLOR),
    alpha: !!(colorType & constants.COLOR_ALPHA)
  });

  this._handleChunkEnd();
};


Parser.prototype._handlePLTE = function(length) {
  this.read(length, this._parsePLTE.bind(this));
};
Parser.prototype._parsePLTE = function(data) {

  this._crc.write(data);

  var entries = Math.floor(data.length / 3);
  // console.log('Palette:', entries);

  for (var i = 0; i < entries; i++) {
    this._palette.push([
      data.readUInt8(i * 3),
      data.readUInt8(i * 3 + 1),
      data.readUInt8(i * 3 + 2 ),
      0xff
    ]);
  }

  this._handleChunkEnd();
};

Parser.prototype._handleTRNS = function(length) {
  this.read(length, this._parseTRNS.bind(this));
};
Parser.prototype._parseTRNS = function(data) {

  this._crc.write(data);

  // palette
  if (this._colorType == 3) {
    if (this._palette.length == 0) {
      this.error(new Error('Transparency chunk must be after palette'));
      return;
    }
    if (data.length > this._palette.length) {
      this.error(new Error('More transparent colors than palette size'));
      return;
    }
    for (var i = 0; i < this._palette.length; i++) {
      this._palette[i][3] = i < data.length ? data.readUInt8(i) : 0xff;
    }
  }

  // for colorType 0 (grayscale) and 2 (rgb)
  // there might be one gray/color defined as transparent
  if (this._colorType === 0) {
    // grey, 2 bytes
    this._transColor = [data.readUInt16BE(0)];
  }
  if (this._colorType === 2) {
    this._transColor = [data.readUInt16BE(0), data.readUInt16BE(2), data.readUInt16BE(4)];
  }

  this._handleChunkEnd();
};

Parser.prototype._handleGAMA = function(length) {
  this.read(length, this._parseGAMA.bind(this));
};
Parser.prototype._parseGAMA = function(data) {

  this._crc.write(data);
  this.gamma(data.readUInt32BE(0) / 100000);

  this._handleChunkEnd();
};

Parser.prototype._handleIDAT = function(length) {
  this.read(-length, this._parseIDAT.bind(this, length));
};
Parser.prototype._parseIDAT = function(length, data) {

  this._crc.write(data);

  if (this._colorType == 3 && this._palette.length == 0)
    throw new Error('Expected palette not found');

  this.inflateData(data);
  length -= data.length;

  if (length > 0)
    this._handleIDAT(length);
  else
    this._handleChunkEnd();
};


Parser.prototype._handleIEND = function(length) {
  this.read(length, this._parseIEND.bind(this));
};
Parser.prototype._parseIEND = function(data) {

  this._crc.write(data);

  this._hasIEND = true;
  this._handleChunkEnd();

  this.finished();
};

Parser.prototype.reverseFiltered = function(indata, depth, width, height) {

  var outdata = indata; // only different for 16 bits

  if (this._colorType == 3) { // paletted
    //console.log("paletted");
    // use values from palette
    var pxLineLength = width << 2;

    for (var y = 0; y < height; y++) {
      var pxRowPos = y * pxLineLength;

      for (var x = 0; x < width; x++) {
        var pxPos = pxRowPos + (x << 2),
          color = this._palette[indata[pxPos]];

        if (!color) {
          throw new Error("index " + indata[pxPos] + " not in palette");
        }

        for (var i = 0; i < 4; i++)
          indata[pxPos + i] = color[i];
      }
    }
  } else {
    var pxLineLength = width << 2;

    if (this._transColor) {
      for (var y = 0; y < height; y++) {
        var pxRowPos = y * pxLineLength;

        for (var x = 0; x < width; x++) {
          var pxPos = pxRowPos + (x << 2);
          var makeTrans = false;
          //console.log(pxPos);
          if (this._transColor.length === 1) {
            if (this._transColor[0] === indata[pxPos]) {
              makeTrans = true;
            }
          } else if (this._transColor[0] === indata[pxPos] && this._transColor[1] === indata[pxPos + 1] && this._transColor[2] === indata[pxPos + 2]) {
            makeTrans = true;
          }
          if (makeTrans) {
            for (var i = 0; i < 4; i++) {
              indata[pxPos + i] = 0;
            }
          }
        }
      }
    }
    if (depth !== 8) {
      if (depth === 16) {
        outdata = new Buffer(width * height * 4);
      }
      //console.log("adjusting");
      var maxOutSample = 255;
      var maxInSample = Math.pow(2, depth) - 1;

      for (var y = 0; y < height; y++) {
        var pxRowPos = y * pxLineLength;

        for (var x = 0; x < width; x++) {
          var pxPos = pxRowPos + (x << 2);
          for (var i = 0; i < 4; i++)
            outdata[pxPos + i] = Math.floor((indata[pxPos + i] * maxOutSample) / maxInSample + 0.5);
        }
      }
    }
  }
  return outdata;
};

