// Copyright 2011 Timothy J Fontaine <tjfontaine@gmail.com>
//
// 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

// TODO: change the default UDP packet size that node-dns sends
//       from 4096 to conform to these:
//       - [requestor's payload size](https://tools.ietf.org/html/rfc6891#section-6.2.3)
//       - [responders's payload size](https://tools.ietf.org/html/rfc6891#section-6.2.4)

'use strict';

var consts = require('./consts'),
    BufferCursor = require('buffercursor'),
    BufferCursorOverflow = BufferCursor.BufferCursorOverflow,
    ipaddr = require('ipaddr.js'),
    assert = require('assert'),
    util = require('util');

function assertUndefined(val, msg) {
  assert(typeof val != 'undefined', msg);
}

var Packet = module.exports = function() {
  this.header = {
    id: 0,
    qr: 0,
    opcode: 0,
    aa: 0,
    tc: 0,
    rd: 1,
    ra: 0,
    res1: 0,
    res2: 0,
    res3: 0,
    rcode: 0
  };
  this.question = [];
  this.answer = [];
  this.authority = [];
  this.additional = [];
  this.edns_options = [];   // TODO: DEPRECATED! Use `.edns.options` instead!
  this.payload = undefined; // TODO: DEPRECATED! Use `.edns.payload` instead!
};

var LABEL_POINTER = 0xC0;

var isPointer = function(len) {
  return (len & LABEL_POINTER) === LABEL_POINTER;
};

function nameUnpack(buff) {
  var len, comp, end, pos, part, combine = '';

  len = buff.readUInt8();
  comp = false;
  end = buff.tell();

  while (len !== 0) {
    if (isPointer(len)) {
      len -= LABEL_POINTER;
      len = len << 8;
      pos = len + buff.readUInt8();
      if (!comp)
        end = buff.tell();
      buff.seek(pos);
      len = buff.readUInt8();
      comp = true;
      continue;
    }

    part = buff.toString('ascii', len);

    if (combine.length)
      combine = combine + '.' + part;
    else
      combine = part;

    len = buff.readUInt8();

    if (!comp)
      end = buff.tell();
  }

  buff.seek(end);

  return combine;
}

function namePack(str, buff, index) {
  var offset, dot, part;

  while (str) {
    if (index[str]) {
      offset = (LABEL_POINTER << 8) + index[str];
      buff.writeUInt16BE(offset);
      break;
    } else {
      index[str] = buff.tell();
      dot = str.indexOf('.');
      if (dot > -1) {
        part = str.slice(0, dot);
        str = str.slice(dot + 1);
      } else {
        part = str;
        str = undefined;
      }
      buff.writeUInt8(part.length);
      buff.write(part, part.length, 'ascii');
    }
  }

  if (!str) {
    buff.writeUInt8(0);
  }
}

var
  WRITE_HEADER              = 100001,
  WRITE_TRUNCATE            = 100002,
  WRITE_QUESTION            = 100003,
  WRITE_RESOURCE_RECORD     = 100004,
  WRITE_RESOURCE_WRITE      = 100005,
  WRITE_RESOURCE_DONE       = 100006,
  WRITE_RESOURCE_END        = 100007,
  WRITE_EDNS                = 100008,
  WRITE_END                 = 100009,
  WRITE_A     = consts.NAME_TO_QTYPE.A,
  WRITE_AAAA  = consts.NAME_TO_QTYPE.AAAA,
  WRITE_NS    = consts.NAME_TO_QTYPE.NS,
  WRITE_CNAME = consts.NAME_TO_QTYPE.CNAME,
  WRITE_PTR   = consts.NAME_TO_QTYPE.PTR,
  WRITE_SPF   = consts.NAME_TO_QTYPE.SPF,
  WRITE_MX    = consts.NAME_TO_QTYPE.MX,
  WRITE_SRV   = consts.NAME_TO_QTYPE.SRV,
  WRITE_TXT   = consts.NAME_TO_QTYPE.TXT,
  WRITE_SOA   = consts.NAME_TO_QTYPE.SOA,
  WRITE_OPT   = consts.NAME_TO_QTYPE.OPT,
  WRITE_NAPTR = consts.NAME_TO_QTYPE.NAPTR,
  WRITE_TLSA  = consts.NAME_TO_QTYPE.TLSA;

function writeHeader(buff, packet) {
  assert(packet.header, 'Packet requires "header"');
  buff.writeUInt16BE(packet.header.id & 0xFFFF);
  var val = 0;
  val += (packet.header.qr << 15) & 0x8000;
  val += (packet.header.opcode << 11) & 0x7800;
  val += (packet.header.aa << 10) & 0x400;
  val += (packet.header.tc << 9) & 0x200;
  val += (packet.header.rd << 8) & 0x100;
  val += (packet.header.ra << 7) & 0x80;
  val += (packet.header.res1 << 6) & 0x40;
  val += (packet.header.res2 << 5) & 0x20;
  val += (packet.header.res3 << 4) & 0x10;
  val += packet.header.rcode & 0xF;
  buff.writeUInt16BE(val & 0xFFFF);
  assert(packet.question.length == 1, 'DNS requires one question');
  // aren't used
  buff.writeUInt16BE(1);
  // answer offset 6
  buff.writeUInt16BE(packet.answer.length & 0xFFFF);
  // authority offset 8
  buff.writeUInt16BE(packet.authority.length & 0xFFFF);
  // additional offset 10
  buff.writeUInt16BE(packet.additional.length & 0xFFFF);
  return WRITE_QUESTION;
}

function writeTruncate(buff, packet, section, val) {
  // XXX FIXME TODO truncation is currently done wrong.
  // Quote rfc2181 section 9
  // The TC bit should not be set merely because some extra information
  // could have been included, but there was insufficient room.  This
  // includes the results of additional section processing.  In such cases
  // the entire RRSet that will not fit in the response should be omitted,
  // and the reply sent as is, with the TC bit clear.  If the recipient of
  // the reply needs the omitted data, it can construct a query for that
  // data and send that separately.
  //
  // TODO IOW only set TC if we hit it in ANSWERS otherwise make sure an
  // entire RRSet is removed during a truncation.
  var pos;

  buff.seek(2);
  val = buff.readUInt16BE();
  val |= (1 << 9) & 0x200;
  buff.seek(2);
  buff.writeUInt16BE(val);
  switch (section) {
    case 'answer':
      pos = 6;
      // seek to authority and clear it and additional out
      buff.seek(8);
      buff.writeUInt16BE(0);
      buff.writeUInt16BE(0);
      break;
    case 'authority':
      pos = 8;
      // seek to additional and clear it out
      buff.seek(10);
      buff.writeUInt16BE(0);
      break;
    case 'additional':
      pos = 10;
      break;
  }
  buff.seek(pos);
  buff.writeUInt16BE(count - 1); // TODO: count not defined!
  buff.seek(last_resource);      // TODO: last_resource not defined!
  return WRITE_END;
}

function writeQuestion(buff, val, label_index) {
  assert(val, 'Packet requires a question');
  assertUndefined(val.name, 'Question requires a "name"');
  assertUndefined(val.type, 'Question requires a "type"');
  assertUndefined(val.class, 'Questionn requires a "class"');
  namePack(val.name, buff, label_index);
  buff.writeUInt16BE(val.type & 0xFFFF);
  buff.writeUInt16BE(val.class & 0xFFFF);
  return WRITE_RESOURCE_RECORD;
}

function writeResource(buff, val, label_index, rdata) {
  assert(val, 'Resource must be defined');
  assertUndefined(val.name, 'Resource record requires "name"');
  assertUndefined(val.type, 'Resource record requires "type"');
  assertUndefined(val.class, 'Resource record requires "class"');
  assertUndefined(val.ttl, 'Resource record requires "ttl"');
  namePack(val.name, buff, label_index);
  buff.writeUInt16BE(val.type & 0xFFFF);
  buff.writeUInt16BE(val.class & 0xFFFF);
  buff.writeUInt32BE(val.ttl & 0xFFFFFFFF);
  rdata.pos = buff.tell();
  buff.writeUInt16BE(0); // if there is rdata, then this value will be updated
                         // to the correct value by 'writeResourceDone'
  return val.type;
}

function writeResourceDone(buff, rdata) {
  var pos = buff.tell();
  buff.seek(rdata.pos);
  buff.writeUInt16BE(pos - rdata.pos - 2);
  buff.seek(pos);
  return WRITE_RESOURCE_RECORD;
}

function writeIp(buff, val) {
  //TODO XXX FIXME -- assert that address is of proper type
  assertUndefined(val.address, 'A/AAAA record requires "address"');
  val = ipaddr.parse(val.address).toByteArray();
  val.forEach(function(b) {
    buff.writeUInt8(b);
  });
  return WRITE_RESOURCE_DONE;
}

function writeCname(buff, val, label_index) {
  assertUndefined(val.data, 'NS/CNAME/PTR record requires "data"');
  namePack(val.data, buff, label_index);
  return WRITE_RESOURCE_DONE;
}

// For <character-string> see: http://tools.ietf.org/html/rfc1035#section-3.3
// For TXT: http://tools.ietf.org/html/rfc1035#section-3.3.14
function writeTxt(buff, val) {
  //TODO XXX FIXME -- split on max char string and loop
  assertUndefined(val.data, 'TXT record requires "data"');
  for (var i=0,len=val.data.length; i<len; i++) {
    var dataLen = Buffer.byteLength(val.data[i], 'utf8');
    buff.writeUInt8(dataLen);
    buff.write(val.data[i], dataLen, 'utf8');
  }
  return WRITE_RESOURCE_DONE;
}

function writeMx(buff, val, label_index) {
  assertUndefined(val.priority, 'MX record requires "priority"');
  assertUndefined(val.exchange, 'MX record requires "exchange"');
  buff.writeUInt16BE(val.priority & 0xFFFF);
  namePack(val.exchange, buff, label_index);
  return WRITE_RESOURCE_DONE;
}

// SRV: https://tools.ietf.org/html/rfc2782
// TODO: SRV fixture failing for '_xmpp-server._tcp.gmail.com.srv.js'
function writeSrv(buff, val, label_index) {
  assertUndefined(val.priority, 'SRV record requires "priority"');
  assertUndefined(val.weight, 'SRV record requires "weight"');
  assertUndefined(val.port, 'SRV record requires "port"');
  assertUndefined(val.target, 'SRV record requires "target"');
  buff.writeUInt16BE(val.priority & 0xFFFF);
  buff.writeUInt16BE(val.weight & 0xFFFF);
  buff.writeUInt16BE(val.port & 0xFFFF);
  namePack(val.target, buff, label_index);
  return WRITE_RESOURCE_DONE;
}

function writeSoa(buff, val, label_index) {
  assertUndefined(val.primary, 'SOA record requires "primary"');
  assertUndefined(val.admin, 'SOA record requires "admin"');
  assertUndefined(val.serial, 'SOA record requires "serial"');
  assertUndefined(val.refresh, 'SOA record requires "refresh"');
  assertUndefined(val.retry, 'SOA record requires "retry"');
  assertUndefined(val.expiration, 'SOA record requires "expiration"');
  assertUndefined(val.minimum, 'SOA record requires "minimum"');
  namePack(val.primary, buff, label_index);
  namePack(val.admin, buff, label_index);
  buff.writeUInt32BE(val.serial & 0xFFFFFFFF);
  buff.writeInt32BE(val.refresh & 0xFFFFFFFF);
  buff.writeInt32BE(val.retry & 0xFFFFFFFF);
  buff.writeInt32BE(val.expiration & 0xFFFFFFFF);
  buff.writeInt32BE(val.minimum & 0xFFFFFFFF);
  return WRITE_RESOURCE_DONE;
}

// http://tools.ietf.org/html/rfc3403#section-4.1
function writeNaptr(buff, val, label_index) {
  assertUndefined(val.order, 'NAPTR record requires "order"');
  assertUndefined(val.preference, 'NAPTR record requires "preference"');
  assertUndefined(val.flags, 'NAPTR record requires "flags"');
  assertUndefined(val.service, 'NAPTR record requires "service"');
  assertUndefined(val.regexp, 'NAPTR record requires "regexp"');
  assertUndefined(val.replacement, 'NAPTR record requires "replacement"');
  buff.writeUInt16BE(val.order & 0xFFFF);
  buff.writeUInt16BE(val.preference & 0xFFFF);
  buff.writeUInt8(val.flags.length);
  buff.write(val.flags, val.flags.length, 'ascii');
  buff.writeUInt8(val.service.length);
  buff.write(val.service, val.service.length, 'ascii');
  buff.writeUInt8(val.regexp.length);
  buff.write(val.regexp, val.regexp.length, 'ascii');
  namePack(val.replacement, buff, label_index);
  return WRITE_RESOURCE_DONE;
}

// https://tools.ietf.org/html/rfc6698
function writeTlsa(buff, val) {
  assertUndefined(val.usage, 'TLSA record requires "usage"');
  assertUndefined(val.selector, 'TLSA record requires "selector"');
  assertUndefined(val.matchingtype, 'TLSA record requires "matchingtype"');
  assertUndefined(val.buff, 'TLSA record requires "buff"');
  buff.writeUInt8(val.usage);
  buff.writeUInt8(val.selector);
  buff.writeUInt8(val.matchingtype);
  buff.copy(val.buff);
  return WRITE_RESOURCE_DONE;
}

function makeEdns(packet) {
  packet.edns = {
    name: '',
    type: consts.NAME_TO_QTYPE.OPT,
    class: packet.payload,
    options: [],
    ttl: 0
  };
  packet.edns_options = packet.edns.options; // TODO: 'edns_options' is DEPRECATED!
  packet.additional.push(packet.edns);
  return WRITE_HEADER;
}

function writeOpt(buff, val) {
  var opt;
  for (var i=0, len=val.options.length; i<len; i++) {
    opt = val.options[i];
    buff.writeUInt16BE(opt.code);
    buff.writeUInt16BE(opt.data.length);
    buff.copy(opt.data);
  }
  return WRITE_RESOURCE_DONE;
}

Packet.write = function(buff, packet) {
  var state = WRITE_HEADER,
      val,
      section,
      count,
      rdata,
      last_resource,
      label_index = {};

  buff = new BufferCursor(buff);

  // the existence of 'edns' in a packet indicates that a proper OPT record exists
  // in 'additional' and that all of the other fields in packet (that are parsed by
  // 'parseOpt') are properly set. If it does not exist, we assume that the user
  // is requesting that we create one for them.
  if (typeof packet.edns_version !== 'undefined' && typeof packet.edns === "undefined")
    state = makeEdns(packet);

  // TODO: this is unnecessarily inefficient. rewrite this using a
  //       function table instead. (same for Packet.parse too).
  while (true) {
    try {
      switch (state) {
        case WRITE_HEADER:
          state = writeHeader(buff, packet);
          break;
        case WRITE_TRUNCATE:
          state = writeTruncate(buff, packet, section, last_resource);
          break;
        case WRITE_QUESTION:
          state = writeQuestion(buff, packet.question[0], label_index);
          section = 'answer';
          count = 0;
          break;
        case WRITE_RESOURCE_RECORD:
          last_resource = buff.tell();
          if (packet[section].length == count) {
            switch (section) {
              case 'answer':
                section = 'authority';
                state = WRITE_RESOURCE_RECORD;
                break;
              case 'authority':
                section = 'additional';
                state = WRITE_RESOURCE_RECORD;
                break;
              case 'additional':
                state = WRITE_END;
                break;
            }
            count = 0;
          } else {
            state = WRITE_RESOURCE_WRITE;
          }
          break;
        case WRITE_RESOURCE_WRITE:
          rdata = {};
          val = packet[section][count];
          state = writeResource(buff, val, label_index, rdata);
          break;
        case WRITE_RESOURCE_DONE:
          count += 1;
          state = writeResourceDone(buff, rdata);
          break;
        case WRITE_A:
        case WRITE_AAAA:
          state = writeIp(buff, val);
          break;
        case WRITE_NS:
        case WRITE_CNAME:
        case WRITE_PTR:
          state = writeCname(buff, val, label_index);
          break;
        case WRITE_SPF:
        case WRITE_TXT:
          state = writeTxt(buff, val);
          break;
        case WRITE_MX:
          state = writeMx(buff, val, label_index);
          break;
        case WRITE_SRV:
          state = writeSrv(buff, val, label_index);
          break;
        case WRITE_SOA:
          state = writeSoa(buff, val, label_index);
          break;
        case WRITE_OPT:
          state = writeOpt(buff, val);
          break;
        case WRITE_NAPTR:
          state = writeNaptr(buff, val, label_index);
          break;
        case WRITE_TLSA:
          state = writeTlsa(buff, val);
          break;
        case WRITE_END:
          return buff.tell();
        default:
          if (typeof val.data !== 'object')
            throw new Error('Packet.write Unknown State: ' + state);
          // write unhandled RR type
          buff.copy(val.data);
          state = WRITE_RESOURCE_DONE;
      }
    } catch (e) {
      if (e instanceof BufferCursorOverflow) {
        state = WRITE_TRUNCATE;
      } else {
        throw e;
      }
    }
  }
};

function parseHeader(msg, packet) {
  packet.header.id = msg.readUInt16BE();
  var val = msg.readUInt16BE();
  packet.header.qr = (val & 0x8000) >> 15;
  packet.header.opcode = (val & 0x7800) >> 11;
  packet.header.aa = (val & 0x400) >> 10;
  packet.header.tc = (val & 0x200) >> 9;
  packet.header.rd = (val & 0x100) >> 8;
  packet.header.ra = (val & 0x80) >> 7;
  packet.header.res1 = (val & 0x40) >> 6;
  packet.header.res2 = (val & 0x20) >> 5;
  packet.header.res3 = (val & 0x10) >> 4;
  packet.header.rcode = (val & 0xF);
  packet.question = new Array(msg.readUInt16BE());
  packet.answer = new Array(msg.readUInt16BE());
  packet.authority = new Array(msg.readUInt16BE());
  packet.additional = new Array(msg.readUInt16BE());
  return PARSE_QUESTION;
}

function parseQuestion(msg, packet) {
  var val = {};
  val.name = nameUnpack(msg);
  val.type = msg.readUInt16BE();
  val.class = msg.readUInt16BE();
  packet.question[0] = val;
  assert(packet.question.length === 1);
  // TODO handle qdcount > 1 in practice no one sends this
  return PARSE_RESOURCE_RECORD;
}

function parseRR(msg, val, rdata) {
  val.name = nameUnpack(msg);
  val.type = msg.readUInt16BE();
  val.class = msg.readUInt16BE();
  val.ttl = msg.readUInt32BE();
  rdata.len = msg.readUInt16BE();
  return val.type;
}

function parseA(val, msg) {
  var address = '' +
    msg.readUInt8() +
    '.' + msg.readUInt8() +
    '.' + msg.readUInt8() +
    '.' + msg.readUInt8();
  val.address = address;
  return PARSE_RESOURCE_DONE;
}

function parseAAAA(val, msg) {
  var address = '';
  var compressed = false;

  for (var i = 0; i < 8; i++) {
    if (i > 0) address += ':';
    // TODO zero compression
    address += msg.readUInt16BE().toString(16);
  }
  val.address = address;
  return PARSE_RESOURCE_DONE;
}

function parseCname(val, msg) {
  val.data = nameUnpack(msg);
  return PARSE_RESOURCE_DONE;
}

function parseTxt(val, msg, rdata) {
  val.data = [];
  var end = msg.tell() + rdata.len;
  while (msg.tell() != end) {
    var len = msg.readUInt8();
    val.data.push(msg.toString('utf8', len));
  }
  return PARSE_RESOURCE_DONE;
}

function parseMx(val, msg, rdata) {
  val.priority = msg.readUInt16BE();
  val.exchange = nameUnpack(msg);
  return PARSE_RESOURCE_DONE;
}

// TODO: SRV fixture failing for '_xmpp-server._tcp.gmail.com.srv.js'
//       https://tools.ietf.org/html/rfc2782
function parseSrv(val, msg) {
  val.priority = msg.readUInt16BE();
  val.weight = msg.readUInt16BE();
  val.port = msg.readUInt16BE();
  val.target = nameUnpack(msg);
  return PARSE_RESOURCE_DONE;
}

function parseSoa(val, msg) {
  val.primary = nameUnpack(msg);
  val.admin = nameUnpack(msg);
  val.serial = msg.readUInt32BE();
  val.refresh = msg.readInt32BE();
  val.retry = msg.readInt32BE();
  val.expiration = msg.readInt32BE();
  val.minimum = msg.readInt32BE();
  return PARSE_RESOURCE_DONE;
}

// http://tools.ietf.org/html/rfc3403#section-4.1
function parseNaptr(val, msg) {
  val.order = msg.readUInt16BE();
  val.preference = msg.readUInt16BE();
  var len = msg.readUInt8();
  val.flags = msg.toString('ascii', len);
  len = msg.readUInt8();
  val.service = msg.toString('ascii', len);
  len = msg.readUInt8();
  val.regexp = msg.toString('ascii', len);
  val.replacement = nameUnpack(msg);
  return PARSE_RESOURCE_DONE;
}

function parseTlsa(val, msg, rdata) {
  val.usage = msg.readUInt8();
  val.selector = msg.readUInt8();
  val.matchingtype = msg.readUInt8();
  val.buff = msg.slice(rdata.len - 3).buffer; // 3 because of the 3 UInt8s above.
  return PARSE_RESOURCE_DONE;
}

// https://tools.ietf.org/html/rfc6891#section-6.1.2
// https://tools.ietf.org/html/rfc2671#section-4.4
//       - [payload size selection](https://tools.ietf.org/html/rfc6891#section-6.2.5)
function parseOpt(val, msg, rdata, packet) {
  // assert first entry in additional
  rdata.buf = msg.slice(rdata.len);

  val.rcode = ((val.ttl & 0xFF000000) >> 20) + packet.header.rcode;
  val.version = (val.ttl >> 16) & 0xFF;
  val.do = (val.ttl >> 15) & 1;
  val.z = val.ttl & 0x7F;
  val.options = [];

  packet.edns = val;
  packet.edns_version = val.version; // TODO: return BADVERS for unsupported version! (Section 6.1.3)

  // !! BEGIN DEPRECATION NOTICE !!
  // THESE FIELDS MAY BE REMOVED IN THE FUTURE!
  packet.edns_options = val.options;
  packet.payload = val.class;
  // !! END DEPRECATION NOTICE !!

  while (!rdata.buf.eof()) {
    val.options.push({
      code: rdata.buf.readUInt16BE(),
      data: rdata.buf.slice(rdata.buf.readUInt16BE()).buffer
    });
  }
  return PARSE_RESOURCE_DONE;
}

var
  PARSE_HEADER          = 100000,
  PARSE_QUESTION        = 100001,
  PARSE_RESOURCE_RECORD = 100002,
  PARSE_RR_UNPACK       = 100003,
  PARSE_RESOURCE_DONE   = 100004,
  PARSE_END             = 100005,
  PARSE_A     = consts.NAME_TO_QTYPE.A,
  PARSE_NS    = consts.NAME_TO_QTYPE.NS,
  PARSE_CNAME = consts.NAME_TO_QTYPE.CNAME,
  PARSE_SOA   = consts.NAME_TO_QTYPE.SOA,
  PARSE_PTR   = consts.NAME_TO_QTYPE.PTR,
  PARSE_MX    = consts.NAME_TO_QTYPE.MX,
  PARSE_TXT   = consts.NAME_TO_QTYPE.TXT,
  PARSE_AAAA  = consts.NAME_TO_QTYPE.AAAA,
  PARSE_SRV   = consts.NAME_TO_QTYPE.SRV,
  PARSE_NAPTR = consts.NAME_TO_QTYPE.NAPTR,
  PARSE_OPT   = consts.NAME_TO_QTYPE.OPT,
  PARSE_SPF   = consts.NAME_TO_QTYPE.SPF,
  PARSE_TLSA  = consts.NAME_TO_QTYPE.TLSA;
  

Packet.parse = function(msg) {
  var state,
      pos,
      val,
      rdata,
      section,
      count;

  var packet = new Packet();

  pos = 0;
  state = PARSE_HEADER;

  msg = new BufferCursor(msg);

  while (true) {
    switch (state) {
      case PARSE_HEADER:
        state = parseHeader(msg, packet);
        break;
      case PARSE_QUESTION:
        state = parseQuestion(msg, packet);
        section = 'answer';
        count = 0;
        break;
      case PARSE_RESOURCE_RECORD:
        // console.log('PARSE_RESOURCE_RECORD: count = %d, %s.len = %d', count, section, packet[section].length);
        if (count === packet[section].length) {
          switch (section) {
            case 'answer':
              section = 'authority';
              count = 0;
              break;
            case 'authority':
              section = 'additional';
              count = 0;
              break;
            case 'additional':
              state = PARSE_END;
              break;
          }
        } else {
          state = PARSE_RR_UNPACK;
        }
        break;
      case PARSE_RR_UNPACK:
        val = {};
        rdata = {};
        state = parseRR(msg, val, rdata);
        break;
      case PARSE_RESOURCE_DONE:
        packet[section][count++] = val;
        state = PARSE_RESOURCE_RECORD;
        break;
      case PARSE_A:
        state = parseA(val, msg);
        break;
      case PARSE_AAAA:
        state = parseAAAA(val, msg);
        break;
      case PARSE_NS:
      case PARSE_CNAME:
      case PARSE_PTR:
        state = parseCname(val, msg);
        break;
      case PARSE_SPF:
      case PARSE_TXT:
        state = parseTxt(val, msg, rdata);
        break;
      case PARSE_MX:
        state = parseMx(val, msg);
        break;
      case PARSE_SRV:
        state = parseSrv(val, msg);
        break;
      case PARSE_SOA:
        state = parseSoa(val, msg);
        break;
      case PARSE_OPT:
        state = parseOpt(val, msg, rdata, packet);
        break;
      case PARSE_NAPTR:
        state = parseNaptr(val, msg);
        break;
      case PARSE_TLSA:
        state = parseTlsa(val, msg, rdata);
        break;
      case PARSE_END:
        return packet;
      default:
        //console.log(state, val);
        val.data = msg.slice(rdata.len);
        state = PARSE_RESOURCE_DONE;
        break;
    }
  }
};
