'use strict'

var parser = exports

var transport = require('../../../spdy-transport')
var base = transport.protocol.base
var utils = base.utils
var constants = require('./').constants

var assert = require('assert')
var util = require('util')

function Parser (options) {
  base.Parser.call(this, options)

  this.isServer = options.isServer

  this.waiting = constants.PREFACE_SIZE
  this.state = 'preface'
  this.pendingHeader = null

  // Header Block queue
  this._lastHeaderBlock = null
  this.maxFrameSize = constants.INITIAL_MAX_FRAME_SIZE
  this.maxHeaderListSize = constants.DEFAULT_MAX_HEADER_LIST_SIZE
}
util.inherits(Parser, base.Parser)

parser.create = function create (options) {
  return new Parser(options)
}

Parser.prototype.setMaxFrameSize = function setMaxFrameSize (size) {
  this.maxFrameSize = size
}

Parser.prototype.setMaxHeaderListSize = function setMaxHeaderListSize (size) {
  this.maxHeaderListSize = size
}

// Only for testing
Parser.prototype.skipPreface = function skipPreface () {
  // Just some number bigger than 3.1, doesn't really matter for HTTP2
  this.setVersion(4)

  // Parse frame header!
  this.state = 'frame-head'
  this.waiting = constants.FRAME_HEADER_SIZE
}

Parser.prototype.execute = function execute (buffer, callback) {
  if (this.state === 'preface') { return this.onPreface(buffer, callback) }

  if (this.state === 'frame-head') {
    return this.onFrameHead(buffer, callback)
  }

  assert(this.state === 'frame-body' && this.pendingHeader !== null)

  var self = this
  var header = this.pendingHeader
  this.pendingHeader = null

  this.onFrameBody(header, buffer, function (err, frame) {
    if (err) {
      return callback(err)
    }

    self.state = 'frame-head'
    self.partial = false
    self.waiting = constants.FRAME_HEADER_SIZE
    callback(null, frame)
  })
}

Parser.prototype.executePartial = function executePartial (buffer, callback) {
  var header = this.pendingHeader

  assert.equal(header.flags & constants.flags.PADDED, 0)

  if (this.window) { this.window.recv.update(-buffer.size) }

  callback(null, {
    type: 'DATA',
    id: header.id,

    // Partial DATA can't be FIN
    fin: false,
    data: buffer.take(buffer.size)
  })
}

Parser.prototype.onPreface = function onPreface (buffer, callback) {
  if (buffer.take(buffer.size).toString() !== constants.PREFACE) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid preface'))
  }

  this.skipPreface()
  callback(null, null)
}

Parser.prototype.onFrameHead = function onFrameHead (buffer, callback) {
  var header = {
    length: buffer.readUInt24BE(),
    control: true,
    type: buffer.readUInt8(),
    flags: buffer.readUInt8(),
    id: buffer.readUInt32BE() & 0x7fffffff
  }

  if (header.length > this.maxFrameSize) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'Frame length OOB'))
  }

  header.control = header.type !== constants.frameType.DATA

  this.state = 'frame-body'
  this.pendingHeader = header
  this.waiting = header.length
  this.partial = !header.control

  // TODO(indutny): eventually support partial padded DATA
  if (this.partial) {
    this.partial = (header.flags & constants.flags.PADDED) === 0
  }

  callback(null, null)
}

Parser.prototype.onFrameBody = function onFrameBody (header, buffer, callback) {
  var frameType = constants.frameType

  if (header.type === frameType.DATA) {
    this.onDataFrame(header, buffer, callback)
  } else if (header.type === frameType.HEADERS) {
    this.onHeadersFrame(header, buffer, callback)
  } else if (header.type === frameType.CONTINUATION) {
    this.onContinuationFrame(header, buffer, callback)
  } else if (header.type === frameType.WINDOW_UPDATE) {
    this.onWindowUpdateFrame(header, buffer, callback)
  } else if (header.type === frameType.RST_STREAM) {
    this.onRSTFrame(header, buffer, callback)
  } else if (header.type === frameType.SETTINGS) {
    this.onSettingsFrame(header, buffer, callback)
  } else if (header.type === frameType.PUSH_PROMISE) {
    this.onPushPromiseFrame(header, buffer, callback)
  } else if (header.type === frameType.PING) {
    this.onPingFrame(header, buffer, callback)
  } else if (header.type === frameType.GOAWAY) {
    this.onGoawayFrame(header, buffer, callback)
  } else if (header.type === frameType.PRIORITY) {
    this.onPriorityFrame(header, buffer, callback)
  } else if (header.type === frameType.X_FORWARDED_FOR) {
    this.onXForwardedFrame(header, buffer, callback)
  } else {
    this.onUnknownFrame(header, buffer, callback)
  }
}

Parser.prototype.onUnknownFrame = function onUnknownFrame (header, buffer, callback) {
  if (this._lastHeaderBlock !== null) {
    callback(this.error(constants.error.PROTOCOL_ERROR,
                        'Received unknown frame in the middle of a header block'))
    return
  }
  callback(null, { type: 'unknown: ' + header.type })
}

Parser.prototype.unpadData = function unpadData (header, body, callback) {
  var isPadded = (header.flags & constants.flags.PADDED) !== 0

  if (!isPadded) { return callback(null, body) }

  if (!body.has(1)) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'Not enough space for padding'))
  }

  var pad = body.readUInt8()
  if (!body.has(pad)) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid padding size'))
  }

  var contents = body.clone(body.size - pad)
  body.skip(body.size)
  callback(null, contents)
}

Parser.prototype.onDataFrame = function onDataFrame (header, body, callback) {
  var isEndStream = (header.flags & constants.flags.END_STREAM) !== 0

  if (header.id === 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Received DATA frame with stream=0'))
  }

  // Count received bytes
  if (this.window) {
    this.window.recv.update(-body.size)
  }

  this.unpadData(header, body, function (err, data) {
    if (err) {
      return callback(err)
    }

    callback(null, {
      type: 'DATA',
      id: header.id,
      fin: isEndStream,
      data: data.take(data.size)
    })
  })
}

Parser.prototype.initHeaderBlock = function initHeaderBlock (header,
                                                            frame,
                                                            block,
                                                            callback) {
  if (this._lastHeaderBlock) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Duplicate Stream ID'))
  }

  this._lastHeaderBlock = {
    id: header.id,
    frame: frame,
    queue: [],
    size: 0
  }

  this.queueHeaderBlock(header, block, callback)
}

Parser.prototype.queueHeaderBlock = function queueHeaderBlock (header,
                                                              block,
                                                              callback) {
  var self = this
  var item = this._lastHeaderBlock
  if (!this._lastHeaderBlock || item.id !== header.id) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'No matching stream for continuation'))
  }

  var fin = (header.flags & constants.flags.END_HEADERS) !== 0

  var chunks = block.toChunks()
  for (var i = 0; i < chunks.length; i++) {
    var chunk = chunks[i]
    item.queue.push(chunk)
    item.size += chunk.length
  }

  if (item.size >= self.maxHeaderListSize) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Compressed header list is too large'))
  }

  if (!fin) { return callback(null, null) }
  this._lastHeaderBlock = null

  this.decompress.write(item.queue, function (err, chunks) {
    if (err) {
      return callback(self.error(constants.error.COMPRESSION_ERROR,
                                 err.message))
    }

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

      size += header.name.length + header.value.length + 32
      if (size >= self.maxHeaderListSize) {
        return callback(self.error(constants.error.PROTOCOL_ERROR,
                                   'Header list is too large'))
      }

      if (/[A-Z]/.test(header.name)) {
        return callback(self.error(constants.error.PROTOCOL_ERROR,
                                   'Header name must be lowercase'))
      }

      utils.addHeaderLine(header.name, header.value, headers)
    }

    item.frame.headers = headers
    item.frame.path = headers[':path']

    callback(null, item.frame)
  })
}

Parser.prototype.onHeadersFrame = function onHeadersFrame (header,
                                                          body,
                                                          callback) {
  var self = this

  if (header.id === 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid stream id for HEADERS'))
  }

  this.unpadData(header, body, function (err, data) {
    if (err) { return callback(err) }

    var isPriority = (header.flags & constants.flags.PRIORITY) !== 0
    if (!data.has(isPriority ? 5 : 0)) {
      return callback(self.error(constants.error.FRAME_SIZE_ERROR,
                                 'Not enough data for HEADERS'))
    }

    var exclusive = false
    var dependency = 0
    var weight = constants.DEFAULT_WEIGHT
    if (isPriority) {
      dependency = data.readUInt32BE()
      exclusive = (dependency & 0x80000000) !== 0
      dependency &= 0x7fffffff

      // Weight's range is [1, 256]
      weight = data.readUInt8() + 1
    }

    if (dependency === header.id) {
      return callback(self.error(constants.error.PROTOCOL_ERROR,
                                 'Stream can\'t dependend on itself'))
    }

    var streamInfo = {
      type: 'HEADERS',
      id: header.id,
      priority: {
        parent: dependency,
        exclusive: exclusive,
        weight: weight
      },
      fin: (header.flags & constants.flags.END_STREAM) !== 0,
      writable: true,
      headers: null,
      path: null
    }

    self.initHeaderBlock(header, streamInfo, data, callback)
  })
}

Parser.prototype.onContinuationFrame = function onContinuationFrame (header,
                                                                    body,
                                                                    callback) {
  this.queueHeaderBlock(header, body, callback)
}

Parser.prototype.onRSTFrame = function onRSTFrame (header, body, callback) {
  if (body.size !== 4) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'RST_STREAM length not 4'))
  }

  if (header.id === 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid stream id for RST_STREAM'))
  }

  callback(null, {
    type: 'RST',
    id: header.id,
    code: constants.errorByCode[body.readUInt32BE()]
  })
}

Parser.prototype._validateSettings = function _validateSettings (settings) {
  if (settings['enable_push'] !== undefined &&
      settings['enable_push'] !== 0 &&
      settings['enable_push'] !== 1) {
    return this.error(constants.error.PROTOCOL_ERROR,
                      'SETTINGS_ENABLE_PUSH must be 0 or 1')
  }

  if (settings['initial_window_size'] !== undefined &&
      (settings['initial_window_size'] > constants.MAX_INITIAL_WINDOW_SIZE ||
       settings['initial_window_size'] < 0)) {
    return this.error(constants.error.FLOW_CONTROL_ERROR,
                      'SETTINGS_INITIAL_WINDOW_SIZE is OOB')
  }

  if (settings['max_frame_size'] !== undefined &&
      (settings['max_frame_size'] > constants.ABSOLUTE_MAX_FRAME_SIZE ||
       settings['max_frame_size'] < constants.INITIAL_MAX_FRAME_SIZE)) {
    return this.error(constants.error.PROTOCOL_ERROR,
                      'SETTINGS_MAX_FRAME_SIZE is OOB')
  }

  return undefined
}

Parser.prototype.onSettingsFrame = function onSettingsFrame (header,
                                                            body,
                                                            callback) {
  if (header.id !== 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid stream id for SETTINGS'))
  }

  var isAck = (header.flags & constants.flags.ACK) !== 0
  if (isAck && body.size !== 0) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'SETTINGS with ACK and non-zero length'))
  }

  if (isAck) {
    return callback(null, { type: 'ACK_SETTINGS' })
  }

  if (body.size % 6 !== 0) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'SETTINGS length not multiple of 6'))
  }

  var settings = {}
  while (!body.isEmpty()) {
    var id = body.readUInt16BE()
    var value = body.readUInt32BE()
    var name = constants.settingsIndex[id]

    if (name) {
      settings[name] = value
    }
  }

  var err = this._validateSettings(settings)
  if (err !== undefined) {
    return callback(err)
  }

  callback(null, {
    type: 'SETTINGS',
    settings: settings
  })
}

Parser.prototype.onPushPromiseFrame = function onPushPromiseFrame (header,
                                                                  body,
                                                                  callback) {
  if (header.id === 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid stream id for PUSH_PROMISE'))
  }

  var self = this
  this.unpadData(header, body, function (err, data) {
    if (err) {
      return callback(err)
    }

    if (!data.has(4)) {
      return callback(self.error(constants.error.FRAME_SIZE_ERROR,
                                 'PUSH_PROMISE length less than 4'))
    }

    var streamInfo = {
      type: 'PUSH_PROMISE',
      id: header.id,
      fin: false,
      promisedId: data.readUInt32BE() & 0x7fffffff,
      headers: null,
      path: null
    }

    self.initHeaderBlock(header, streamInfo, data, callback)
  })
}

Parser.prototype.onPingFrame = function onPingFrame (header, body, callback) {
  if (body.size !== 8) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'PING length != 8'))
  }

  if (header.id !== 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid stream id for PING'))
  }

  var ack = (header.flags & constants.flags.ACK) !== 0
  callback(null, { type: 'PING', opaque: body.take(body.size), ack: ack })
}

Parser.prototype.onGoawayFrame = function onGoawayFrame (header,
                                                        body,
                                                        callback) {
  if (!body.has(8)) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'GOAWAY length < 8'))
  }

  if (header.id !== 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid stream id for GOAWAY'))
  }

  var frame = {
    type: 'GOAWAY',
    lastId: body.readUInt32BE(),
    code: constants.goawayByCode[body.readUInt32BE()]
  }

  if (body.size !== 0) { frame.debug = body.take(body.size) }

  callback(null, frame)
}

Parser.prototype.onPriorityFrame = function onPriorityFrame (header,
                                                            body,
                                                            callback) {
  if (body.size !== 5) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'PRIORITY length != 5'))
  }

  if (header.id === 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Invalid stream id for PRIORITY'))
  }

  var dependency = body.readUInt32BE()

  // Again the range is from 1 to 256
  var weight = body.readUInt8() + 1

  if (dependency === header.id) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'Stream can\'t dependend on itself'))
  }

  callback(null, {
    type: 'PRIORITY',
    id: header.id,
    priority: {
      exclusive: (dependency & 0x80000000) !== 0,
      parent: dependency & 0x7fffffff,
      weight: weight
    }
  })
}

Parser.prototype.onWindowUpdateFrame = function onWindowUpdateFrame (header,
                                                                    body,
                                                                    callback) {
  if (body.size !== 4) {
    return callback(this.error(constants.error.FRAME_SIZE_ERROR,
                               'WINDOW_UPDATE length != 4'))
  }

  var delta = body.readInt32BE()
  if (delta === 0) {
    return callback(this.error(constants.error.PROTOCOL_ERROR,
                               'WINDOW_UPDATE delta == 0'))
  }

  callback(null, {
    type: 'WINDOW_UPDATE',
    id: header.id,
    delta: delta
  })
}

Parser.prototype.onXForwardedFrame = function onXForwardedFrame (header,
                                                                body,
                                                                callback) {
  callback(null, {
    type: 'X_FORWARDED_FOR',
    host: body.take(body.size).toString()
  })
}
