const assert = require('assert');

const { parseSignature } = require('./signature');
const put = require('@nornagon/put');
const Marshallers = require('./marshallers');
const align = require('./align').align;

module.exports = function marshall (signature, data, offset, fds) {
  if (typeof offset === 'undefined') offset = 0;
  const tree = parseSignature(signature);
  if (!Array.isArray(data) || data.length !== tree.length) {
    throw new Error(
      `message body does not match message signature. Body:${JSON.stringify(
        data
      )}, signature:${signature}`
    );
  }
  const putstream = put();
  putstream._offset = offset;
  const buf = writeStruct(putstream, tree, data, fds).buffer();
  return buf;
};

// TODO: serialise JS objects as a{sv}
// function writeHash(ps, treeKey, treeVal, data) {
//
// }

function writeStruct (ps, tree, data, fds) {
  if (tree.length !== data.length) {
    throw new Error('Invalid struct data');
  }
  for (let i = 0; i < tree.length; ++i) {
    write(ps, tree[i], data[i], fds);
  }
  return ps;
}

function write (ps, ele, data, fds) {
  switch (ele.type) {
    case '(':
    case '{':
      align(ps, 8);
      writeStruct(ps, ele.child, data, fds);
      break;
    case 'a': {
      // array serialisation:
      // length of array body aligned at 4 byte boundary
      // (optional 4 bytes to align first body element on 8-byte boundary if element
      // body
      const arrPut = put();
      arrPut._offset = ps._offset;
      const _offset = arrPut._offset;
      writeSimple(arrPut, 'u', 0); // array length placeholder
      const lengthOffset = arrPut._offset - 4 - _offset;
      // we need to align here because alignment is not included in array length
      if (['x', 't', 'd', '{', '('].indexOf(ele.child[0].type) !== -1) { align(arrPut, 8); }
      const startOffset = arrPut._offset;
      for (let i = 0; i < data.length; ++i) { write(arrPut, ele.child[0], data[i], fds); }
      const arrBuff = arrPut.buffer();
      const length = arrPut._offset - startOffset;
      // lengthOffset in the range 0 to 3 depending on number of align bytes padded _before_ arrayLength
      arrBuff.writeUInt32LE(length, lengthOffset);
      ps.put(arrBuff);
      ps._offset += arrBuff.length;
      break;
    } case 'v': {
      // TODO: allow serialisation of simple types as variants, e. g 123 -> ['u', 123], true -> ['b', 1], 'abc' -> ['s', 'abc']
      assert.strictEqual(data.length, 2, 'variant data should be [signature, data]');
      const signatureEle = {
        type: 'g',
        child: []
      };
      write(ps, signatureEle, data[0], fds);
      const tree = parseSignature(data[0]);
      assert(tree.length === 1);
      write(ps, tree[0], data[1], fds);
      break;
    } case 'h': {
      if (fds) {
        const idx = fds.push(data);
        return writeSimple(ps, ele.type, idx - 1);
      }
    }
    default:
      return writeSimple(ps, ele.type, data);
  }
}

const stringTypes = ['g', 'o', 's'];

function writeSimple (ps, type, data) {
  if (typeof data === 'undefined') {
    throw new Error(
      "Serialisation of JS 'undefined' type is not supported by d-bus"
    );
  }
  if (data === null) { throw new Error('Serialisation of null value is not supported by d-bus'); }

  if (Buffer.isBuffer(data)) data = data.toString(); // encoding?
  if (stringTypes.indexOf(type) !== -1 && typeof data !== 'string') {
    throw new Error(
      `Expected string or buffer argument, got ${JSON.stringify(
        data
      )} of type '${type}'`
    );
  }

  const simpleMarshaller = Marshallers.MakeSimpleMarshaller(type);
  simpleMarshaller.marshall(ps, data);
  return ps;
}
