var PacketHeader = require('./PacketHeader');
var BigNumber    = require('bignumber.js');
var Buffer       = require('safe-buffer').Buffer;
var BufferList   = require('./BufferList');

var MAX_PACKET_LENGTH    = Math.pow(2, 24) - 1;
var MUL_32BIT            = Math.pow(2, 32);
var PACKET_HEADER_LENGTH = 4;

module.exports = Parser;
function Parser(options) {
  options = options || {};

  this._supportBigNumbers = options.config && options.config.supportBigNumbers;
  this._buffer            = Buffer.alloc(0);
  this._nextBuffers       = new BufferList();
  this._longPacketBuffers = new BufferList();
  this._offset            = 0;
  this._packetEnd         = null;
  this._packetHeader      = null;
  this._packetOffset      = null;
  this._onError           = options.onError || function(err) { throw err; };
  this._onPacket          = options.onPacket || function() {};
  this._nextPacketNumber  = 0;
  this._encoding          = 'utf-8';
  this._paused            = false;
}

Parser.prototype.write = function write(chunk) {
  this._nextBuffers.push(chunk);

  while (!this._paused) {
    var packetHeader = this._tryReadPacketHeader();

    if (!packetHeader) {
      break;
    }

    if (!this._combineNextBuffers(packetHeader.length)) {
      break;
    }

    this._parsePacket(packetHeader);
  }
};

Parser.prototype.append = function append(chunk) {
  if (!chunk || chunk.length === 0) {
    return;
  }

  // Calculate slice ranges
  var sliceEnd    = this._buffer.length;
  var sliceStart  = this._packetOffset === null
    ? this._offset
    : this._packetOffset;
  var sliceLength = sliceEnd - sliceStart;

  // Get chunk data
  var buffer = null;
  var chunks = !(chunk instanceof Array || Array.isArray(chunk)) ? [chunk] : chunk;
  var length = 0;
  var offset = 0;

  for (var i = 0; i < chunks.length; i++) {
    length += chunks[i].length;
  }

  if (sliceLength !== 0) {
    // Create a new Buffer
    buffer = Buffer.allocUnsafe(sliceLength + length);
    offset = 0;

    // Copy data slice
    offset += this._buffer.copy(buffer, 0, sliceStart, sliceEnd);

    // Copy chunks
    for (var i = 0; i < chunks.length; i++) {
      offset += chunks[i].copy(buffer, offset);
    }
  } else if (chunks.length > 1) {
    // Create a new Buffer
    buffer = Buffer.allocUnsafe(length);
    offset = 0;

    // Copy chunks
    for (var i = 0; i < chunks.length; i++) {
      offset += chunks[i].copy(buffer, offset);
    }
  } else {
    // Buffer is the only chunk
    buffer = chunks[0];
  }

  // Adjust data-tracking pointers
  this._buffer       = buffer;
  this._offset       = this._offset - sliceStart;
  this._packetEnd    = this._packetEnd !== null
    ? this._packetEnd - sliceStart
    : null;
  this._packetOffset = this._packetOffset !== null
    ? this._packetOffset - sliceStart
    : null;
};

Parser.prototype.pause = function() {
  this._paused = true;
};

Parser.prototype.resume = function() {
  this._paused = false;

  // nextTick() to avoid entering write() multiple times within the same stack
  // which would cause problems as write manipulates the state of the object.
  process.nextTick(this.write.bind(this));
};

Parser.prototype.peak = function peak(offset) {
  return this._buffer[this._offset + (offset >>> 0)];
};

Parser.prototype.parseUnsignedNumber = function parseUnsignedNumber(bytes) {
  if (bytes === 1) {
    return this._buffer[this._offset++];
  }

  var buffer = this._buffer;
  var offset = this._offset + bytes - 1;
  var value  = 0;

  if (bytes > 4) {
    var err    = new Error('parseUnsignedNumber: Supports only up to 4 bytes');
    err.offset = (this._offset - this._packetOffset - 1);
    err.code   = 'PARSER_UNSIGNED_TOO_LONG';
    throw err;
  }

  while (offset >= this._offset) {
    value = ((value << 8) | buffer[offset]) >>> 0;
    offset--;
  }

  this._offset += bytes;

  return value;
};

Parser.prototype.parseLengthCodedString = function() {
  var length = this.parseLengthCodedNumber();

  if (length === null) {
    return null;
  }

  return this.parseString(length);
};

Parser.prototype.parseLengthCodedBuffer = function() {
  var length = this.parseLengthCodedNumber();

  if (length === null) {
    return null;
  }

  return this.parseBuffer(length);
};

Parser.prototype.parseLengthCodedNumber = function parseLengthCodedNumber() {
  if (this._offset >= this._buffer.length) {
    var err    = new Error('Parser: read past end');
    err.offset = (this._offset - this._packetOffset);
    err.code   = 'PARSER_READ_PAST_END';
    throw err;
  }

  var bits = this._buffer[this._offset++];

  if (bits <= 250) {
    return bits;
  }

  switch (bits) {
    case 251:
      return null;
    case 252:
      return this.parseUnsignedNumber(2);
    case 253:
      return this.parseUnsignedNumber(3);
    case 254:
      break;
    default:
      var err    = new Error('Unexpected first byte' + (bits ? ': 0x' + bits.toString(16) : ''));
      err.offset = (this._offset - this._packetOffset - 1);
      err.code   = 'PARSER_BAD_LENGTH_BYTE';
      throw err;
  }

  var low = this.parseUnsignedNumber(4);
  var high = this.parseUnsignedNumber(4);
  var value;

  if (high >>> 21) {
    value = BigNumber(MUL_32BIT).times(high).plus(low).toString();

    if (this._supportBigNumbers) {
      return value;
    }

    var err    = new Error(
      'parseLengthCodedNumber: JS precision range exceeded, ' +
      'number is >= 53 bit: "' + value + '"'
    );
    err.offset = (this._offset - this._packetOffset - 8);
    err.code   = 'PARSER_JS_PRECISION_RANGE_EXCEEDED';
    throw err;
  }

  value = low + (MUL_32BIT * high);

  return value;
};

Parser.prototype.parseFiller = function(length) {
  return this.parseBuffer(length);
};

Parser.prototype.parseNullTerminatedBuffer = function() {
  var end      = this._nullByteOffset();
  var value    = this._buffer.slice(this._offset, end);
  this._offset = end + 1;

  return value;
};

Parser.prototype.parseNullTerminatedString = function() {
  var end      = this._nullByteOffset();
  var value    = this._buffer.toString(this._encoding, this._offset, end);
  this._offset = end + 1;

  return value;
};

Parser.prototype._nullByteOffset = function() {
  var offset = this._offset;

  while (this._buffer[offset] !== 0x00) {
    offset++;

    if (offset >= this._buffer.length) {
      var err    = new Error('Offset of null terminated string not found.');
      err.offset = (this._offset - this._packetOffset);
      err.code   = 'PARSER_MISSING_NULL_BYTE';
      throw err;
    }
  }

  return offset;
};

Parser.prototype.parsePacketTerminatedBuffer = function parsePacketTerminatedBuffer() {
  var length = this._packetEnd - this._offset;
  return this.parseBuffer(length);
};

Parser.prototype.parsePacketTerminatedString = function() {
  var length = this._packetEnd - this._offset;
  return this.parseString(length);
};

Parser.prototype.parseBuffer = function(length) {
  var response = Buffer.alloc(length);
  this._buffer.copy(response, 0, this._offset, this._offset + length);

  this._offset += length;
  return response;
};

Parser.prototype.parseString = function(length) {
  var offset = this._offset;
  var end = offset + length;
  var value = this._buffer.toString(this._encoding, offset, end);

  this._offset = end;
  return value;
};

Parser.prototype.parseGeometryValue = function() {
  var buffer = this.parseLengthCodedBuffer();
  var offset = 4;

  if (buffer === null || !buffer.length) {
    return null;
  }

  function parseGeometry() {
    var result = null;
    var byteOrder = buffer.readUInt8(offset); offset += 1;
    var wkbType = byteOrder ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4;
    switch (wkbType) {
      case 1: // WKBPoint
        var x = byteOrder ? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8;
        var y = byteOrder ? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8;
        result = {x: x, y: y};
        break;
      case 2: // WKBLineString
        var numPoints = byteOrder ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4;
        result = [];
        for (var i = numPoints; i > 0; i--) {
          var x = byteOrder ? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8;
          var y = byteOrder ? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8;
          result.push({x: x, y: y});
        }
        break;
      case 3: // WKBPolygon
        var numRings = byteOrder ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4;
        result = [];
        for (var i = numRings; i > 0; i--) {
          var numPoints = byteOrder ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4;
          var line = [];
          for (var j = numPoints; j > 0; j--) {
            var x = byteOrder ? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8;
            var y = byteOrder ? buffer.readDoubleLE(offset) : buffer.readDoubleBE(offset); offset += 8;
            line.push({x: x, y: y});
          }
          result.push(line);
        }
        break;
      case 4: // WKBMultiPoint
      case 5: // WKBMultiLineString
      case 6: // WKBMultiPolygon
      case 7: // WKBGeometryCollection
        var num = byteOrder ? buffer.readUInt32LE(offset) : buffer.readUInt32BE(offset); offset += 4;
        var result = [];
        for (var i = num; i > 0; i--) {
          result.push(parseGeometry());
        }
        break;
    }
    return result;
  }
  return parseGeometry();
};

Parser.prototype.reachedPacketEnd = function() {
  return this._offset === this._packetEnd;
};

Parser.prototype.incrementPacketNumber = function() {
  var currentPacketNumber = this._nextPacketNumber;
  this._nextPacketNumber = (this._nextPacketNumber + 1) % 256;

  return currentPacketNumber;
};

Parser.prototype.resetPacketNumber = function() {
  this._nextPacketNumber = 0;
};

Parser.prototype.packetLength = function packetLength() {
  if (!this._packetHeader) {
    return null;
  }

  return this._packetHeader.length + this._longPacketBuffers.size;
};

Parser.prototype._combineNextBuffers = function _combineNextBuffers(bytes) {
  var length = this._buffer.length - this._offset;

  if (length >= bytes) {
    return true;
  }

  if ((length + this._nextBuffers.size) < bytes) {
    return false;
  }

  var buffers     = [];
  var bytesNeeded = bytes - length;

  while (bytesNeeded > 0) {
    var buffer = this._nextBuffers.shift();
    buffers.push(buffer);
    bytesNeeded -= buffer.length;
  }

  this.append(buffers);
  return true;
};

Parser.prototype._combineLongPacketBuffers = function _combineLongPacketBuffers() {
  if (!this._longPacketBuffers.size) {
    return;
  }

  // Calculate bytes
  var remainingBytes      = this._buffer.length - this._offset;
  var trailingPacketBytes = this._buffer.length - this._packetEnd;

  // Create buffer
  var buf    = null;
  var buffer = Buffer.allocUnsafe(remainingBytes + this._longPacketBuffers.size);
  var offset = 0;

  // Copy long buffers
  while ((buf = this._longPacketBuffers.shift())) {
    offset += buf.copy(buffer, offset);
  }

  // Copy remaining bytes
  this._buffer.copy(buffer, offset, this._offset);

  this._buffer       = buffer;
  this._offset       = 0;
  this._packetEnd    = this._buffer.length - trailingPacketBytes;
  this._packetOffset = 0;
};

Parser.prototype._parsePacket = function _parsePacket(packetHeader) {
  this._packetEnd    = this._offset + packetHeader.length;
  this._packetOffset = this._offset;

  if (packetHeader.length === MAX_PACKET_LENGTH) {
    this._longPacketBuffers.push(this._buffer.slice(this._packetOffset, this._packetEnd));
    this._advanceToNextPacket();
    return;
  }

  this._combineLongPacketBuffers();

  var hadException = true;
  try {
    this._onPacket(packetHeader);
    hadException = false;
  } catch (err) {
    if (!err || typeof err.code !== 'string' || err.code.substr(0, 7) !== 'PARSER_') {
      throw err; // Rethrow non-MySQL errors
    }

    // Pass down parser errors
    this._onError(err);
    hadException = false;
  } finally {
    this._advanceToNextPacket();

    // If there was an exception, the parser while loop will be broken out
    // of after the finally block. So schedule a blank write to re-enter it
    // to continue parsing any bytes that may already have been received.
    if (hadException) {
      process.nextTick(this.write.bind(this));
    }
  }
};

Parser.prototype._tryReadPacketHeader = function _tryReadPacketHeader() {
  if (this._packetHeader) {
    return this._packetHeader;
  }

  if (!this._combineNextBuffers(PACKET_HEADER_LENGTH)) {
    return null;
  }

  this._packetHeader = new PacketHeader(
    this.parseUnsignedNumber(3),
    this.parseUnsignedNumber(1)
  );

  if (this._packetHeader.number !== this._nextPacketNumber) {
    var err = new Error(
      'Packets out of order. Got: ' + this._packetHeader.number + ' ' +
      'Expected: ' + this._nextPacketNumber
    );

    err.code  = 'PROTOCOL_PACKETS_OUT_OF_ORDER';
    err.fatal = true;

    this._onError(err);
  }

  this.incrementPacketNumber();

  return this._packetHeader;
};

Parser.prototype._advanceToNextPacket = function() {
  this._offset       = this._packetEnd;
  this._packetHeader = null;
  this._packetEnd    = null;
  this._packetOffset = null;
};
