/* eslint-disable required-modules */
'use strict';

// Naïve DNS parser/serializer.

const assert = require('assert');
const os = require('os');

const types = {
  A: 1,
  AAAA: 28,
  NS: 2,
  CNAME: 5,
  SOA: 6,
  PTR: 12,
  MX: 15,
  TXT: 16,
  ANY: 255
};

const classes = {
  IN: 1
};

function readDomainFromPacket(buffer, offset) {
  assert.ok(offset < buffer.length);
  const length = buffer[offset];
  if (length === 0) {
    return { nread: 1, domain: '' };
  } else if ((length & 0xC0) === 0) {
    offset += 1;
    const chunk = buffer.toString('ascii', offset, offset + length);
    // Read the rest of the domain.
    const { nread, domain } = readDomainFromPacket(buffer, offset + length);
    return {
      nread: 1 + length + nread,
      domain: domain ? `${chunk}.${domain}` : chunk
    };
  } else {
    // Pointer to another part of the packet.
    assert.strictEqual(length & 0xC0, 0xC0);
    // eslint-disable-next-line
    const pointeeOffset = buffer.readUInt16BE(offset) &~ 0xC000;
    return {
      nread: 2,
      domain: readDomainFromPacket(buffer, pointeeOffset)
    };
  }
}

function parseDNSPacket(buffer) {
  assert.ok(buffer.length > 12);

  const parsed = {
    id: buffer.readUInt16BE(0),
    flags: buffer.readUInt16BE(2),
  };

  const counts = [
    ['questions', buffer.readUInt16BE(4)],
    ['answers', buffer.readUInt16BE(6)],
    ['authorityAnswers', buffer.readUInt16BE(8)],
    ['additionalRecords', buffer.readUInt16BE(10)]
  ];

  let offset = 12;
  for (const [ sectionName, count ] of counts) {
    parsed[sectionName] = [];
    for (let i = 0; i < count; ++i) {
      const { nread, domain } = readDomainFromPacket(buffer, offset);
      offset += nread;

      const type = buffer.readUInt16BE(offset);

      const rr = {
        domain,
        cls: buffer.readUInt16BE(offset + 2),
      };
      offset += 4;

      for (const name in types) {
        if (types[name] === type)
          rr.type = name;
      }

      if (sectionName !== 'questions') {
        rr.ttl = buffer.readInt32BE(offset);
        const dataLength = buffer.readUInt16BE(offset);
        offset += 6;

        switch (type) {
          case types.A:
            assert.strictEqual(dataLength, 4);
            rr.address = `${buffer[offset + 0]}.${buffer[offset + 1]}.` +
                         `${buffer[offset + 2]}.${buffer[offset + 3]}`;
            break;
          case types.AAAA:
            assert.strictEqual(dataLength, 16);
            rr.address = buffer.toString('hex', offset, offset + 16)
                               .replace(/(.{4}(?!$))/g, '$1:');
            break;
          case types.TXT:
          {
            let position = offset;
            rr.entries = [];
            while (position < offset + dataLength) {
              const txtLength = buffer[offset];
              rr.entries.push(buffer.toString('utf8',
                                              position + 1,
                                              position + 1 + txtLength));
              position += 1 + txtLength;
            }
            assert.strictEqual(position, offset + dataLength);
            break;
          }
          case types.MX:
          {
            rr.priority = buffer.readInt16BE(buffer, offset);
            offset += 2;
            const { nread, domain } = readDomainFromPacket(buffer, offset);
            rr.exchange = domain;
            assert.strictEqual(nread, dataLength);
            break;
          }
          case types.NS:
          case types.CNAME:
          case types.PTR:
          {
            const { nread, domain } = readDomainFromPacket(buffer, offset);
            rr.value = domain;
            assert.strictEqual(nread, dataLength);
            break;
          }
          case types.SOA:
          {
            const mname = readDomainFromPacket(buffer, offset);
            const rname = readDomainFromPacket(buffer, offset + mname.nread);
            rr.nsname = mname.domain;
            rr.hostmaster = rname.domain;
            const trailerOffset = offset + mname.nread + rname.nread;
            rr.serial = buffer.readUInt32BE(trailerOffset);
            rr.refresh = buffer.readUInt32BE(trailerOffset + 4);
            rr.retry = buffer.readUInt32BE(trailerOffset + 8);
            rr.expire = buffer.readUInt32BE(trailerOffset + 12);
            rr.minttl = buffer.readUInt32BE(trailerOffset + 16);

            assert.strictEqual(trailerOffset + 20, dataLength);
            break;
          }
          default:
            throw new Error(`Unknown RR type ${rr.type}`);
        }
        offset += dataLength;
      }

      parsed[sectionName].push(rr);

      assert.ok(offset <= buffer.length);
    }
  }

  assert.strictEqual(offset, buffer.length);
  return parsed;
}

function writeIPv6(ip) {
  const parts = ip.replace(/^:|:$/g, '').split(':');
  const buf = Buffer.alloc(16);

  let offset = 0;
  for (const part of parts) {
    if (part === '') {
      offset += 16 - 2 * (parts.length - 1);
    } else {
      buf.writeUInt16BE(parseInt(part, 16), offset);
      offset += 2;
    }
  }

  return buf;
}

function writeDomainName(domain) {
  return Buffer.concat(domain.split('.').map((label) => {
    assert(label.length < 64);
    return Buffer.concat([
      Buffer.from([label.length]),
      Buffer.from(label, 'ascii')
    ]);
  }).concat([Buffer.alloc(1)]));
}

function writeDNSPacket(parsed) {
  const buffers = [];
  const kStandardResponseFlags = 0x8180;

  buffers.push(new Uint16Array([
    parsed.id,
    parsed.flags === undefined ? kStandardResponseFlags : parsed.flags,
    parsed.questions && parsed.questions.length,
    parsed.answers && parsed.answers.length,
    parsed.authorityAnswers && parsed.authorityAnswers.length,
    parsed.additionalRecords && parsed.additionalRecords.length,
  ]));

  for (const q of parsed.questions) {
    assert(types[q.type]);
    buffers.push(writeDomainName(q.domain));
    buffers.push(new Uint16Array([
      types[q.type],
      q.cls === undefined ? classes.IN : q.cls
    ]));
  }

  for (const rr of [].concat(parsed.answers,
                             parsed.authorityAnswers,
                             parsed.additionalRecords)) {
    if (!rr) continue;

    assert(types[rr.type]);
    buffers.push(writeDomainName(rr.domain));
    buffers.push(new Uint16Array([
      types[rr.type],
      rr.cls === undefined ? classes.IN : rr.cls
    ]));
    buffers.push(new Int32Array([rr.ttl]));

    const rdLengthBuf = new Uint16Array(1);
    buffers.push(rdLengthBuf);

    switch (rr.type) {
      case 'A':
        rdLengthBuf[0] = 4;
        buffers.push(new Uint8Array(rr.address.split('.')));
        break;
      case 'AAAA':
        rdLengthBuf[0] = 16;
        buffers.push(writeIPv6(rr.address));
        break;
      case 'TXT':
        const total = rr.entries.map((s) => s.length).reduce((a, b) => a + b);
        // Total length of all strings + 1 byte each for their lengths.
        rdLengthBuf[0] = rr.entries.length + total;
        for (const txt of rr.entries) {
          buffers.push(new Uint8Array([Buffer.byteLength(txt)]));
          buffers.push(Buffer.from(txt));
        }
        break;
      case 'MX':
        rdLengthBuf[0] = 2;
        buffers.push(new Uint16Array([rr.priority]));
        // fall through
      case 'NS':
      case 'CNAME':
      case 'PTR':
      {
        const domain = writeDomainName(rr.exchange || rr.value);
        rdLengthBuf[0] += domain.length;
        buffers.push(domain);
        break;
      }
      case 'SOA':
      {
        const mname = writeDomainName(rr.nsname);
        const rname = writeDomainName(rr.hostmaster);
        rdLengthBuf[0] = mname.length + rname.length + 20;
        buffers.push(mname, rname);
        buffers.push(new Uint32Array([
          rr.serial, rr.refresh, rr.retry, rr.expire, rr.minttl
        ]));
        break;
      }
      default:
        throw new Error(`Unknown RR type ${rr.type}`);
    }
  }

  return Buffer.concat(buffers.map((typedArray) => {
    const buf = Buffer.from(typedArray.buffer,
                            typedArray.byteOffset,
                            typedArray.byteLength);
    if (os.endianness() === 'LE') {
      if (typedArray.BYTES_PER_ELEMENT === 2) buf.swap16();
      if (typedArray.BYTES_PER_ELEMENT === 4) buf.swap32();
    }
    return buf;
  }));
}

module.exports = { types, classes, writeDNSPacket, parseDNSPacket };
