var util = require('util')
var os = require('os')

var EventEmitter = require('eventemitter3').EventEmitter
var debug = require('debug')('vnc:connection')

var PixelFormat = require('./pixelformat')

function VncConnection(conn, options) {
  this.options = options

  this._bound = {
    _errorListener: this._errorListener.bind(this)
  , _readableListener: this._readableListener.bind(this)
  , _endListener: this._endListener.bind(this)
  , _closeListener: this._closeListener.bind(this)
  }

  this._buffer = null
  this._state = 0
  this._changeState(VncConnection.STATE_NEED_CLIENT_VERSION)

  this._serverVersion = VncConnection.V3_008
  this._serverSupportedSecurity = [VncConnection.SECURITY_NONE]
  this._serverWidth = this.options.width
  this._serverHeight = this.options.height
  this._serverPixelFormat = new PixelFormat({
    bitsPerPixel: 32
  , depth: 24
  , bigEndianFlag: os.endianness() == 'BE' ? 1 : 0
  , trueColorFlag: 1
  , redMax: 255
  , greenMax: 255
  , blueMax: 255
  , redShift: 16
  , greenShift: 8
  , blueShift: 0
  })
  this._serverName = this.options.name

  this._clientVersion = null
  this._clientShare = false
  this._clientPixelFormat = this._serverPixelFormat
  this._clientEncodingCount = 0
  this._clientEncodings = []
  this._clientCutTextLength = 0

  this.conn = conn
    .on('error', this._bound._errorListener)
    .on('readable', this._bound._readableListener)
    .on('end', this._bound._endListener)
    .on('close', this._bound._closeListener)

  this._writeServerVersion()
  this._read()
}

util.inherits(VncConnection, EventEmitter)

VncConnection.V3_003 = 3003
VncConnection.V3_007 = 3007
VncConnection.V3_008 = 3008

VncConnection.SECURITY_NONE = 1
VncConnection.SECURITY_VNC = 2

VncConnection.SECURITYRESULT_OK = 0
VncConnection.SECURITYRESULT_FAIL = 1

VncConnection.CLIENT_MESSAGE_SETPIXELFORMAT = 0
VncConnection.CLIENT_MESSAGE_SETENCODINGS = 2
VncConnection.CLIENT_MESSAGE_FBUPDATEREQUEST = 3
VncConnection.CLIENT_MESSAGE_KEYEVENT = 4
VncConnection.CLIENT_MESSAGE_POINTEREVENT = 5
VncConnection.CLIENT_MESSAGE_CLIENTCUTTEXT = 6

VncConnection.SERVER_MESSAGE_FBUPDATE = 0

var StateReverse = Object.create(null), State = {
  STATE_NEED_CLIENT_VERSION: 10
, STATE_NEED_CLIENT_SECURITY: 20
, STATE_NEED_CLIENT_INIT: 30
, STATE_NEED_CLIENT_MESSAGE: 40
, STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT: 50
, STATE_NEED_CLIENT_MESSAGE_SETENCODINGS: 60
, STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE: 61
, STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST: 70
, STATE_NEED_CLIENT_MESSAGE_KEYEVENT: 80
, STATE_NEED_CLIENT_MESSAGE_POINTEREVENT: 90
, STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT: 100
, STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE: 101
}

VncConnection.ENCODING_RAW = 0
VncConnection.ENCODING_DESKTOPSIZE = -223

Object.keys(State).map(function(name) {
  VncConnection[name] = State[name]
  StateReverse[State[name]] = name
})

VncConnection.prototype.end = function() {
  this.conn.end()
}

VncConnection.prototype.writeFramebufferUpdate = function(rectangles) {
  var chunk = new Buffer(4)
  chunk[0] = VncConnection.SERVER_MESSAGE_FBUPDATE
  chunk[1] = 0
  chunk.writeUInt16BE(rectangles.length, 2)
  this._write(chunk)

  rectangles.forEach(function(rect) {
    var chunk = new Buffer(12)
    chunk.writeUInt16BE(rect.xPosition, 0)
    chunk.writeUInt16BE(rect.yPosition, 2)
    chunk.writeUInt16BE(rect.width, 4)
    chunk.writeUInt16BE(rect.height, 6)
    chunk.writeInt32BE(rect.encodingType, 8)
    this._write(chunk)

    switch (rect.encodingType) {
    case VncConnection.ENCODING_RAW:
      this._write(rect.data)
      break
    case VncConnection.ENCODING_DESKTOPSIZE:
      this._serverWidth = rect.width
      this._serverHeight = rect.height
      break
    default:
      throw new Error(util.format(
        'Unsupported encoding type', rect.encodingType))
    }
  }, this)
}

VncConnection.prototype._error = function(err) {
  this.emit('error', err)
  this.end()
}

VncConnection.prototype._errorListener = function(err) {
  this._error(err)
}

VncConnection.prototype._endListener = function() {
  this.emit('end')
}

VncConnection.prototype._closeListener = function() {
  this.emit('close')
}

VncConnection.prototype._writeServerVersion = function() {
  // Yes, we could just format the string instead. Didn't feel like it.
  switch (this._serverVersion) {
  case VncConnection.V3_003:
    this._write(new Buffer('RFB 003.003\n'))
    break
  case VncConnection.V3_007:
    this._write(new Buffer('RFB 003.007\n'))
    break
  case VncConnection.V3_008:
    this._write(new Buffer('RFB 003.008\n'))
    break
  }
}

VncConnection.prototype._writeSupportedSecurity = function() {
  var chunk = new Buffer(1 + this._serverSupportedSecurity.length)

  chunk[0] = this._serverSupportedSecurity.length
  this._serverSupportedSecurity.forEach(function(security, i) {
    chunk[1 + i] = security
  })

  this._write(chunk)
}

VncConnection.prototype._writeSelectedSecurity = function() {
  var chunk = new Buffer(4)
  chunk.writeUInt32BE(VncConnection.SECURITY_NONE, 0)
  this._write(chunk)
}

VncConnection.prototype._writeSecurityResult = function(result, reason) {
  var chunk
  switch (result) {
  case VncConnection.SECURITYRESULT_OK:
    chunk = new Buffer(4)
    chunk.writeUInt32BE(result, 0)
    this._write(chunk)
    break
  case VncConnection.SECURITYRESULT_FAIL:
    chunk = new Buffer(4 + 4 + reason.length)
    chunk.writeUInt32BE(result, 0)
    chunk.writeUInt32BE(reason.length, 4)
    chunk.write(reason, 8, reason.length)
    this._write(chunk)
    break
  }
}

VncConnection.prototype._writeServerInit = function() {
  debug('server pixel format', this._serverPixelFormat)
  var chunk = new Buffer(2 + 2 + 16 + 4 + this._serverName.length)
  chunk.writeUInt16BE(this._serverWidth, 0)
  chunk.writeUInt16BE(this._serverHeight, 2)
  chunk[4] = this._serverPixelFormat.bitsPerPixel
  chunk[5] = this._serverPixelFormat.depth
  chunk[6] = this._serverPixelFormat.bigEndianFlag
  chunk[7] = this._serverPixelFormat.trueColorFlag
  chunk.writeUInt16BE(this._serverPixelFormat.redMax, 8)
  chunk.writeUInt16BE(this._serverPixelFormat.greenMax, 10)
  chunk.writeUInt16BE(this._serverPixelFormat.blueMax, 12)
  chunk[14] = this._serverPixelFormat.redShift
  chunk[15] = this._serverPixelFormat.greenShift
  chunk[16] = this._serverPixelFormat.blueShift
  chunk[17] = 0 // padding
  chunk[18] = 0 // padding
  chunk[19] = 0 // padding
  chunk.writeUInt32BE(this._serverName.length, 20)
  chunk.write(this._serverName, 24, this._serverName.length)
  this._write(chunk)
}

VncConnection.prototype._readableListener = function() {
  this._read()
}

VncConnection.prototype._read = function() {
  var chunk, lo, hi
  while (this._append(this.conn.read())) {
    do {
      debug('state', StateReverse[this._state])
      chunk = null
      switch (this._state) {
      case VncConnection.STATE_NEED_CLIENT_VERSION:
        if ((chunk = this._consume(12))) {
          if ((this._clientVersion = this._parseVersion(chunk)) === null) {
            this.end()
            return
          }
          debug('client version', this._clientVersion)
          this._writeSupportedSecurity()
          this._changeState(VncConnection.STATE_NEED_CLIENT_SECURITY)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_SECURITY:
        if ((chunk = this._consume(1))) {
          if ((this._clientSecurity = this._parseSecurity(chunk)) === null) {
            this._writeSecurityResult(
              VncConnection.SECURITYRESULT_FAIL, 'Unsupported security type')
            this.end()
            return
          }
          debug('client security', this._clientSecurity)
          this._writeSecurityResult(VncConnection.SECURITYRESULT_OK)
          this.emit('authenticated')
          this._changeState(VncConnection.STATE_NEED_CLIENT_INIT)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_INIT:
        if ((chunk = this._consume(1))) {
          this._clientShare = chunk[0]
          debug('client shareFlag', this._clientShare)
          this._writeServerInit()
          this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE:
        if ((chunk = this._consume(1))) {
          switch (chunk[0]) {
          case VncConnection.CLIENT_MESSAGE_SETPIXELFORMAT:
            this._changeState(
              VncConnection.STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT)
            break
          case VncConnection.CLIENT_MESSAGE_SETENCODINGS:
            this._changeState(
              VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS)
            break
          case VncConnection.CLIENT_MESSAGE_FBUPDATEREQUEST:
            this._changeState(
              VncConnection.STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST)
            break
          case VncConnection.CLIENT_MESSAGE_KEYEVENT:
            this._changeState(
              VncConnection.STATE_NEED_CLIENT_MESSAGE_KEYEVENT)
            break
          case VncConnection.CLIENT_MESSAGE_POINTEREVENT:
            this._changeState(
              VncConnection.STATE_NEED_CLIENT_MESSAGE_POINTEREVENT)
            break
          case VncConnection.CLIENT_MESSAGE_CLIENTCUTTEXT:
            this._changeState(
              VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT)
            break
          default:
            this._error(new Error(util.format(
              'Unsupported message type %d', chunk[0])))
            return
          }
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT:
        if ((chunk = this._consume(19))) {
          // [0b, 3b) padding
          this._clientPixelFormat = new PixelFormat({
            bitsPerPixel: chunk[3]
          , depth: chunk[4]
          , bigEndianFlag: chunk[5]
          , trueColorFlag: chunk[6]
          , redMax: chunk.readUInt16BE(7, true)
          , greenMax: chunk.readUInt16BE(9, true)
          , blueMax: chunk.readUInt16BE(11, true)
          , redShift: chunk[13]
          , greenShift: chunk[14]
          , blueShift: chunk[15]
          })
          // [16b, 19b) padding
          debug('client pixel format', this._clientPixelFormat)
          this.emit('formatchange', this._clientPixelFormat)
          this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS:
        if ((chunk = this._consume(3))) {
          // [0b, 1b) padding
          this._clientEncodingCount = chunk.readUInt16BE(1, true)
          this._changeState(
            VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE:
        lo = 0
        hi = 4 * this._clientEncodingCount
        if ((chunk = this._consume(hi))) {
          this._clientEncodings = []
          while (lo < hi) {
            this._clientEncodings.push(chunk.readInt32BE(lo, true))
            lo += 4
          }
          debug('client encodings', this._clientEncodings)
          this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST:
        if ((chunk = this._consume(9))) {
          this.emit('fbupdaterequest', {
            incremental: chunk[0]
          , xPosition: chunk.readUInt16BE(1, true)
          , yPosition: chunk.readUInt16BE(3, true)
          , width: chunk.readUInt16BE(5, true)
          , height: chunk.readUInt16BE(7, true)
          })
          this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE_KEYEVENT:
        if ((chunk = this._consume(7))) {
          // downFlag = chunk[0]
          // [1b, 3b) padding
          // key = chunk.readUInt32BE(3, true)
          this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE_POINTEREVENT:
        if ((chunk = this._consume(5))) {
          this.emit('pointer', {
            buttonMask: chunk[0]
          , xPosition: chunk.readUInt16BE(1, true) / this._serverWidth
          , yPosition: chunk.readUInt16BE(3, true) / this._serverHeight
          })
          this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT:
        if ((chunk = this._consume(7))) {
          // [0b, 3b) padding
          this._clientCutTextLength = chunk.readUInt32BE(3)
          this._changeState(
            VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE)
        }
        break
      case VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE:
        if ((chunk = this._consume(this._clientCutTextLength))) {
          // value = chunk
          this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
        }
        break
      default:
        throw new Error(util.format('Impossible state %d', this._state))
      }
    }
    while (chunk)
  }
}

VncConnection.prototype._parseVersion = function(chunk) {
  if (chunk.equals(new Buffer('RFB 003.008\n'))) {
    return VncConnection.V3_008
  }

  if (chunk.equals(new Buffer('RFB 003.007\n'))) {
    return VncConnection.V3_007
  }

  if (chunk.equals(new Buffer('RFB 003.003\n'))) {
    return VncConnection.V3_003
  }

  return null
}

VncConnection.prototype._parseSecurity = function(chunk) {
  switch (chunk[0]) {
  case VncConnection.SECURITY_NONE:
  case VncConnection.SECURITY_VNC:
    return chunk[0]
  default:
    return null
  }
}

VncConnection.prototype._changeState = function(state) {
  this._state = state
}

VncConnection.prototype._append = function(chunk) {
  if (!chunk) {
    return false
  }

  debug('in', chunk)

  if (this._buffer) {
    this._buffer = Buffer.concat(
      [this._buffer, chunk], this._buffer.length + chunk.length)
  }
  else {
    this._buffer = chunk
  }

  return true
}

VncConnection.prototype._consume = function(n) {
  var chunk

  if (!this._buffer) {
    return null
  }

  if (n < this._buffer.length) {
    chunk = this._buffer.slice(0, n)
    this._buffer = this._buffer.slice(n)
    return chunk
  }

  if (n === this._buffer.length) {
    chunk = this._buffer
    this._buffer = null
    return chunk
  }

  return null
}

VncConnection.prototype._write = function(chunk) {
  debug('out', chunk)
  this.conn.write(chunk)
}

module.exports = VncConnection
