/*!
 * protoreader.js - protobufs for bcoin
 * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

const assert = require('assert');
const BufferReader = require('../utils/reader');

/*
 * Constants
 */

const wireType = {
  VARINT: 0,
  FIXED64: 1,
  DELIMITED: 2,
  START_GROUP: 3,
  END_GROUP: 4,
  FIXED32: 5
};

/**
 * ProtoBuf Reader
 * @alias module:utils.ProtoReader
 * @constructor
 */

function ProtoReader(data, zeroCopy) {
  if (!(this instanceof ProtoReader))
    return new ProtoReader(data, zeroCopy);

  BufferReader.call(this, data, zeroCopy);
}

Object.setPrototypeOf(ProtoReader.prototype, BufferReader.prototype);

ProtoReader.prototype.readVarint = function readVarint() {
  const {size, value} = _readVarint(this.data, this.offset);
  this.offset += size;
  return value;
};

ProtoReader.prototype.readFieldValue = function readFieldValue(tag, opt) {
  const field = this.readField(tag, opt);

  if (!field)
    return -1;

  assert(field.value != null);

  return field.value;
};

ProtoReader.prototype.readFieldU64 = function readFieldU64(tag, opt) {
  const field = this.readField(tag, opt);

  if (!field)
    return -1;

  assert(field.type === wireType.VARINT || field.type === wireType.FIXED64);

  return field.value;
};

ProtoReader.prototype.readFieldU32 = function readFieldU32(tag, opt) {
  const field = this.readField(tag, opt);

  if (!field)
    return -1;

  assert(field.type === wireType.VARINT || field.type === wireType.FIXED32);

  return field.value;
};

ProtoReader.prototype.readFieldBytes = function readFieldBytes(tag, opt) {
  const field = this.readField(tag, opt);

  if (!field)
    return null;

  assert(field.data);

  return field.data;
};

ProtoReader.prototype.readFieldString = function readFieldString(tag, opt, enc) {
  const field = this.readField(tag, opt);

  if (!field)
    return null;

  assert(field.data);

  return field.data.toString(enc || 'utf8');
};

ProtoReader.prototype.nextTag = function nextTag() {
  if (this.left() === 0)
    return -1;

  const field = this.readField();

  this.seek(-field.size);

  return field.tag;
};

ProtoReader.prototype.readField = function readField(tag, opt) {
  const offset = this.offset;
  const header = this.readVarint();
  const field = new Field(header);

  if (tag != null && field.tag !== tag) {
    assert(opt, 'Non-optional field not present.');
    this.offset = offset;
    return null;
  }

  switch (field.type) {
    case wireType.VARINT:
      field.value = this.readVarint();
      break;
    case wireType.FIXED64:
      field.value = this.readU64();
      break;
    case wireType.DELIMITED:
      field.data = this.readVarBytes();
      break;
    case wireType.START_GROUP:
      field.group = [];
      for (;;) {
        const inner = this.readField();
        if (inner.type === wireType.END_GROUP)
          break;
        field.group.push(inner);
      }
      break;
    case wireType.END_GROUP:
      assert(false, 'Unexpected end group.');
      break;
    case wireType.FIXED32:
      field.value = this.readU32();
      break;
    default:
      assert(false, 'Bad wire type.');
      break;
  }

  field.size = this.offset - offset;

  return field;
};

/*
 * Encoding
 */

function _readVarint(data, off) {
  let num = 0;
  let ch = 0x80;
  let size = 0;

  while (ch & 0x80) {
    if (off >= data.length) {
      num = 0;
      break;
    }

    ch = data[off++];

    // Optimization for javascript insanity.
    switch (size) {
      case 0:
      case 1:
      case 2:
      case 3:
        num += (ch & 0x7f) << (7 * size);
        break;
      case 4:
        num += (ch & 0x7f) * (1 << (7 * size));
        break;
      default:
        num += (ch & 0x7f) * Math.pow(2, 7 * size);
        break;
    }

    size++;

    assert(size < 7, 'Number exceeds 2^53-1.');
  }

  return new Varint(size, num);
}

/*
 * Helpers
 */

function Field(header) {
  this.tag = header >>> 3;
  this.type = header & 7;
  this.size = 0;
  this.value = 0;
  this.data = null;
  this.group = null;
}

function Varint(size, value) {
  this.size = size;
  this.value = value;
}

/*
 * Expose
 */

module.exports = ProtoReader;
