// Copyright (c) 2023, 2024, Oracle and/or its affiliates.

//-----------------------------------------------------------------------------
//
// This software is dual-licensed to you under the Universal Permissive License
// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
// either license.
//
// If you elect to accept the software under the Apache License, Version 2.0,
// the following applies:
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//-----------------------------------------------------------------------------

'use strict';

const { BaseBuffer, GrowableBuffer } = require('./buffer.js');
const { Buffer } = require('buffer');
const constants = require("./constants.js");
const errors = require("../../errors.js");
const types = require("../../types.js");
const util = require("util");
const vector = require("./vector.js");
const nodbUtil = require("../../util.js");

/**
 * Class used for decoding
 */
class OsonDecoder extends BaseBuffer {

  //---------------------------------------------------------------------------
  // _decodeContainerNode()
  //
  // Decodes a container node (object or array) from the tree segment and
  // returns the JavaScript equivalent.
  //---------------------------------------------------------------------------
  _decodeContainerNode(nodeType) {

    // determine the number of children by examining the 4th and 5th most
    // significant bits of the node type; determine the offsets in the tree
    // segment to the field ids array and the value offsets array
    let container, offsetsPos, fieldIdsPos;
    const containerOffset = this.pos - this.treeSegPos - 1;
    let numChildren = this._getNumChildren(nodeType);
    const isObject = ((nodeType & 0x40) === 0);
    if (numChildren === undefined) {
      const offset = this._getOffset(nodeType);
      offsetsPos = this.pos;
      this.pos = this.treeSegPos + offset;
      const sharedNodeType = this.readUInt8();
      numChildren = this._getNumChildren(sharedNodeType);
      container = (isObject) ? {} : new Array(numChildren);
      fieldIdsPos = this.pos;
    } else if (isObject) {
      container = {};
      fieldIdsPos = this.pos;
      offsetsPos = this.pos + this.fieldIdLength * numChildren;
    } else {
      container = new Array(numChildren);
      offsetsPos = this.pos;
    }

    for (let i = 0; i < numChildren; i++) {
      let name;
      if (isObject) {
        let fieldId;
        if (this.fieldIdLength === 1) {
          fieldId = this.buf[fieldIdsPos];
        } else if (this.fieldIdLength == 2) {
          fieldId = this.buf.readUInt16BE(fieldIdsPos);
        } else {
          fieldId = this.buf.readUInt32BE(fieldIdsPos);
        }
        name = this.fieldNames[fieldId - 1];
        fieldIdsPos += this.fieldIdLength;
      }
      this.pos = offsetsPos;
      let offset = this._getOffset(nodeType);
      if (this.relativeOffsets) {
        offset += containerOffset;
      }
      offsetsPos = this.pos;
      this.pos = this.treeSegPos + offset;
      if (isObject) {
        container[name] = this._decodeNode();
      } else {
        container[i] = this._decodeNode();
      }
    }

    return container;
  }

  //---------------------------------------------------------------------------
  // _decodeNode()
  //
  // Decodes a node from the tree segment and returns the JavaScript
  // equivalent.
  //---------------------------------------------------------------------------
  _decodeNode() {

    // if the most significant bit is set the node refers to a container
    let nodeType = this.readUInt8();
    if (nodeType & 0x80) {
      return this._decodeContainerNode(nodeType);
    }

    // handle simple scalars
    if (nodeType === constants.TNS_JSON_TYPE_NULL) {
      return null;
    } else if (nodeType === constants.TNS_JSON_TYPE_TRUE) {
      return true;
    } else if (nodeType === constants.TNS_JSON_TYPE_FALSE) {
      return false;

    // handle fixed length scalars
    } else if (nodeType === constants.TNS_JSON_TYPE_DATE ||
        nodeType === constants.TNS_JSON_TYPE_TIMESTAMP7) {
      return this.parseOracleDate(this.readBytes(7));
    } else if (nodeType === constants.TNS_JSON_TYPE_TIMESTAMP) {
      return this.parseOracleDate(this.readBytes(11));
    } else if (nodeType === constants.TNS_JSON_TYPE_TIMESTAMP_TZ) {
      return this.parseOracleDate(this.readBytes(13));
    } else if (nodeType === constants.TNS_JSON_TYPE_BINARY_FLOAT) {
      return this.parseBinaryFloat(this.readBytes(4));
    } else if (nodeType === constants.TNS_JSON_TYPE_BINARY_DOUBLE) {
      return this.parseBinaryDouble(this.readBytes(8));

    // handle scalars with lengths stored outside the node itself
    } else if (nodeType === constants.TNS_JSON_TYPE_STRING_LENGTH_UINT8) {
      return this.readBytes(this.readUInt8()).toString();
    } else if (nodeType === constants.TNS_JSON_TYPE_STRING_LENGTH_UINT16) {
      return this.readBytes(this.readUInt16BE()).toString();
    } else if (nodeType === constants.TNS_JSON_TYPE_STRING_LENGTH_UINT32) {
      return this.readBytes(this.readUInt32BE()).toString();
    } else if (nodeType === constants.TNS_JSON_TYPE_NUMBER_LENGTH_UINT8) {
      return parseFloat(this.readOracleNumber());
    } else if (nodeType === constants.TNS_JSON_TYPE_ID) {
      const buf = this.readBytes(this.readUInt8());
      const jsonId = new types.JsonId(buf.length);
      buf.copy(jsonId);
      return jsonId;
    } else if (nodeType === constants.TNS_JSON_TYPE_BINARY_LENGTH_UINT16) {
      return Buffer.from(this.readBytes(this.readUInt16BE()));
    } else if (nodeType === constants.TNS_JSON_TYPE_BINARY_LENGTH_UINT32) {
      return Buffer.from(this.readBytes(this.readUInt32BE()));
    } else if (nodeType === constants.TNS_JSON_TYPE_EXTENDED) {
      nodeType = this.readUInt8();
      if (nodeType === constants.TNS_JSON_TYPE_VECTOR) {
        const vecImage = this.readBytes(this.readUInt32BE());
        const decoder = new vector.VectorDecoder(vecImage);
        return decoder.decode();
      }
    }

    // handle number/decimal with length stored inside the node itself
    const typeBits = nodeType & 0xf0;
    if (typeBits === 0x20 || typeBits === 0x60) {
      const len = nodeType & 0x0f;
      return parseFloat(this.parseOracleNumber(this.readBytes(len + 1)));

    // handle integer with length stored inside the node itself
    } else if (typeBits === 0x40 || typeBits === 0x50) {
      const len = nodeType & 0x0f;
      return parseFloat(this.parseOracleNumber(this.readBytes(len)));

    // handle string with length stored inside the node itself
    } else if ((nodeType & 0xe0) == 0) {
      if (nodeType === 0)
        return '';
      return this.readBytes(nodeType).toString();
    }

    errors.throwErr(errors.ERR_UNSUPPORTED_DATA_TYPE_IN_JSON, nodeType);
  }

  //---------------------------------------------------------------------------
  // _getNumChildren()
  //
  // Returns the number of children a container has. This is determined by
  // looking at the 4th and 5th most significant bits of the node type.
  //
  //   00 - number of children is uint8_t
  //   01 - number of children is uint16_t
  //   10 - number of children is uint32_t
  //   11 - field ids are shared with another object whose offset follows
  //
  // In the latter case the value undefined is returned and the number of
  // children must be read from the shared object at the specified offset.
  //---------------------------------------------------------------------------
  _getNumChildren(nodeType) {
    const childrenBits = (nodeType & 0x18);
    if (childrenBits === 0) {
      return this.readUInt8();
    } else if (childrenBits === 0x08) {
      return this.readUInt16BE();
    } else if (childrenBits === 0x10) {
      return this.readUInt32BE();
    }
  }

  //---------------------------------------------------------------------------
  // _getOffset()
  //
  // Returns an offset. The offset will be either a 16-bit or 32-bit value
  // depending on the value of the 3rd significant bit of the node type.
  //---------------------------------------------------------------------------
  _getOffset(nodeType) {
    if (nodeType & 0x20) {
      return this.readUInt32BE();
    } else {
      return this.readUInt16BE();
    }
  }

  //---------------------------------------------------------------------------
  // _getFieldNames
  //
  // Reads the field names from the buffer.
  //---------------------------------------------------------------------------
  _getFieldNames(arrStartPos, numFields, offsetsSize, fieldNamesSegSize, fieldNamesSize) {

    // skip the hash id array (1 byte * fieldNamesSize for each field)
    this.skipBytes(numFields * fieldNamesSize);

    // skip the field name offsets array for now
    const offsetsPos = this.pos;
    this.skipBytes(numFields * offsetsSize);
    const ptr = this.readBytes(fieldNamesSegSize);
    const finalPos = this.pos;

    // determine the names of the fields
    this.pos = offsetsPos;
    let offset;
    for (let i = arrStartPos; i < arrStartPos + numFields; i++) {
      if (offsetsSize === 2) {
        offset = this.readUInt16BE();
      } else {
        offset = this.readUInt32BE();
      }

      // get the field name object
      let temp;
      if (fieldNamesSize === 1) {
        // Short Field Name
        temp = ptr.readUInt8(offset);
      } else {
        // Long Field Name
        temp = ptr.readUInt16BE(offset);
      }
      this.fieldNames[i] = ptr.subarray(offset + fieldNamesSize, offset + temp + fieldNamesSize).toString();
    }
    this.pos = finalPos;
  }

  //---------------------------------------------------------------------------
  // decode()
  //
  // Decodes the OSON and returns a JavaScript object corresponding to its
  // contents.
  //---------------------------------------------------------------------------
  decode() {

    // parse root header
    const magic = this.readBytes(3);
    if (magic[0] !== constants.TNS_JSON_MAGIC_BYTE_1 ||
        magic[1] !== constants.TNS_JSON_MAGIC_BYTE_2 ||
        magic[2] !== constants.TNS_JSON_MAGIC_BYTE_3) {
      errors.throwErr(errors.ERR_UNEXPECTED_DATA, magic.toString('hex'));
    }
    const version = this.readUInt8();
    if (version !== constants.TNS_JSON_VERSION_MAX_FNAME_255 &&
        version !== constants.TNS_JSON_VERSION_MAX_FNAME_65535) {
      errors.throwErr(errors.ERR_OSON_VERSION_NOT_SUPPORTED, version);
    }
    const primaryFlags = this.readUInt16BE();
    this.relativeOffsets = primaryFlags & constants.TNS_JSON_FLAG_REL_OFFSET_MODE;

    // scalar values are much simpler
    if (primaryFlags & constants.TNS_JSON_FLAG_IS_SCALAR) {
      if (primaryFlags & constants.TNS_JSON_FLAG_TREE_SEG_UINT32) {
        this.skipBytes(4);
      } else {
        this.skipBytes(2);
      }
      return this._decodeNode();
    }

    // determine the number of short field names
    let numShortFieldNames;
    if (primaryFlags & constants.TNS_JSON_FLAG_NUM_FNAMES_UINT32) {
      numShortFieldNames = this.readUInt32BE();
      this.fieldIdLength = 4;
    } else if (primaryFlags & constants.TNS_JSON_FLAG_NUM_FNAMES_UINT16) {
      numShortFieldNames = this.readUInt16BE();
      this.fieldIdLength = 2;
    } else {
      numShortFieldNames = this.readUInt8();
      this.fieldIdLength = 1;
    }

    // determine the size of the short field names segment
    let shortFieldNameOffsetsSize, shortFieldNamesSegSize;
    if (primaryFlags & constants.TNS_JSON_FLAG_FNAMES_SEG_UINT32) {
      shortFieldNameOffsetsSize = 4;
      shortFieldNamesSegSize = this.readUInt32BE();
    } else {
      shortFieldNameOffsetsSize = 2;
      shortFieldNamesSegSize = this.readUInt16BE();
    }

    // if the version indicates that field names > 255 bytes exist, parse
    // the information about that segment
    let longFieldNameOffsetsSize, longFieldNamesSegSize;
    let numLongFieldNames = 0;
    if (version === constants.TNS_JSON_VERSION_MAX_FNAME_65535) {
      const secondaryFlags = this.readUInt16BE();
      if (secondaryFlags & constants.TNS_JSON_FLAG_SEC_FNAMES_SEG_UINT16) {
        longFieldNameOffsetsSize = 2;
      } else {
        longFieldNameOffsetsSize = 4;
      }
      numLongFieldNames = this.readUInt32BE();
      longFieldNamesSegSize = this.readUInt32BE();
    }

    // skip the size of the tree segment
    if (primaryFlags & constants.TNS_JSON_FLAG_TREE_SEG_UINT32) {
      this.skipBytes(4);
    } else {
      this.skipBytes(2);
    }

    // skip the number of "tiny" nodes
    this.skipBytes(2);

    this.fieldNames = new Array(numShortFieldNames + numLongFieldNames);

    // if there are any short names, read them now
    if (numShortFieldNames > 0) {
      this._getFieldNames(0, numShortFieldNames,
        shortFieldNameOffsetsSize, shortFieldNamesSegSize, 1);
    }

    // if there are any long names, read them now
    if (numLongFieldNames > 0) {
      this._getFieldNames(numShortFieldNames, numLongFieldNames,
        longFieldNameOffsetsSize, longFieldNamesSegSize, 2);
    }

    // determine tree segment position in the buffer
    this.treeSegPos = this.pos;

    // decode the root node
    return this._decodeNode();
  }

}

class OsonFieldName {

  constructor(name, maxFieldNameSize) {
    this.name = name;
    this.nameBytes = Buffer.from(name);
    if (this.nameBytes.length > maxFieldNameSize) {
      errors.throwErr(errors.ERR_OSON_FIELD_NAME_LIMITATION, maxFieldNameSize);
    }

    // BigInt constants for calculating Hash ID for the OSON Field Name
    const INITIAL_HASHID = 0x811C9DC5n;
    const HASH_MULTIPLIER = 16777619n;
    const HASH_MASK = 0xffffffffn;

    this.hashId = INITIAL_HASHID;
    for (let i = 0; i < this.nameBytes.length; i++) {
      const c = BigInt(this.nameBytes[i]);
      this.hashId = ((this.hashId ^ c) * HASH_MULTIPLIER) & HASH_MASK;
    }
    this.hashId = Number(this.hashId) & 0xff;
  }

}

class OsonFieldNamesSegment extends GrowableBuffer {

  constructor() {
    super();
    this.fieldNames = [];
  }

  //---------------------------------------------------------------------------
  // addName()
  //
  // Adds a name to the field names segment.
  //---------------------------------------------------------------------------
  addName(fieldName) {
    fieldName.offset = this.pos;
    if (fieldName.nameBytes.length <= 255) {
      this.writeUInt8(fieldName.nameBytes.length);
    } else {
      this.writeUInt16BE(fieldName.nameBytes.length);
    }
    this.writeBytes(fieldName.nameBytes);
    this.fieldNames.push(fieldName);
  }

  //---------------------------------------------------------------------------
  // _processFieldNames()
  //
  // Processes the field names in preparation for encoding within OSON.
  //---------------------------------------------------------------------------
  _processFieldNames(fieldIdOffset) {
    this.fieldNames.sort((a, b) => {
      if (a.hashId < b.hashId)
        return -1;
      if (a.hashId > b.hashId)
        return 1;
      if (a.nameBytes.length < b.nameBytes.length)
        return -1;
      if (a.nameBytes.length > b.nameBytes.length)
        return 1;
      if (a.name < b.name)
        return -1;
      if (a.name > b.name)
        return 1;
      return 0;
    });
    for (let i = 0; i < this.fieldNames.length; i++) {
      this.fieldNames[i].fieldId = fieldIdOffset + i + 1;
    }
    if (this.fieldNames.length < 256) {
      this.fieldIdSize = 1;
    } else if (this.fieldNames.length < 65536) {
      this.fieldIdSize = 2;
    } else {
      this.fieldIdSize = 4;
    }
  }

}

class OsonTreeSegment extends GrowableBuffer {

  //---------------------------------------------------------------------------
  // _encodeArray()
  //
  // Encodes an array in the OSON tree segment.
  //---------------------------------------------------------------------------
  _encodeArray(value, encoder) {
    this._encodeContainer(constants.TNS_JSON_TYPE_ARRAY, value.length);
    const len = value.length * 4;
    const pos = this.reserveBytes(len);
    let offsetsBufPos = pos;
    for (const element of value) {
      this.buf.writeUInt32BE(this.pos, offsetsBufPos);
      offsetsBufPos += 4;
      this.encodeNode(element, encoder);
    }
  }

  //---------------------------------------------------------------------------
  // _encodeContainer()
  //
  // Encodes the first part of a container (array or object) in the OSON tree
  // segment.
  //---------------------------------------------------------------------------
  _encodeContainer(nodeType, numChildren) {
    nodeType |= 0x20;                   // use uint32_t for offsets
    if (numChildren > 65535) {
      nodeType |= 0x10;                 // num children is uint32_t
    } else if (numChildren > 255) {
      nodeType |= 0x08;                 // num children is uint16_t
    }
    this.writeUInt8(nodeType);
    if (numChildren < 256) {
      this.writeUInt8(numChildren);
    } else if (numChildren < 65536) {
      this.writeUInt16BE(numChildren);
    } else {
      this.writeUInt32BE(numChildren);
    }
  }

  //---------------------------------------------------------------------------
  // _encodeObject()
  //
  // Encodes an object in the OSON tree segment.
  //---------------------------------------------------------------------------
  _encodeObject(value, encoder) {
    const numChildren = value.values.length;
    this._encodeContainer(constants.TNS_JSON_TYPE_OBJECT, numChildren);
    let fieldIdOffset = this.pos;
    let valueOffset = this.pos + (numChildren * encoder.fieldIdSize);
    const finalOffset = valueOffset + numChildren * 4;
    this.reserveBytes(finalOffset - this.pos);

    for (let i = 0; i < value.fields.length; i++) {
      const fieldName = encoder.fieldNamesMap.get(value.fields[i]);
      if (encoder.fieldIdSize == 1) {
        this.buf[fieldIdOffset] = fieldName.fieldId;
      } else if (encoder.fieldIdSize == 2) {
        this.buf.writeUInt16BE(fieldName.fieldId, fieldIdOffset);
      } else {
        this.buf.writeUInt32BE(fieldName.fieldId, fieldIdOffset);
      }
      this.buf.writeUInt32BE(this.pos, valueOffset);
      fieldIdOffset += encoder.fieldIdSize;
      valueOffset += 4;
      this.encodeNode(value.values[i], encoder);
    }
  }

  //---------------------------------------------------------------------------
  // encodeNode()
  //
  // Encodes a value (node) in the OSON tree segment.
  //---------------------------------------------------------------------------
  encodeNode(value, encoder) {

    // handle null
    if (value === undefined || value === null) {
      this.writeUInt8(constants.TNS_JSON_TYPE_NULL);

    // handle booleans
    } else if (typeof value === 'boolean') {
      if (value) {
        this.writeUInt8(constants.TNS_JSON_TYPE_TRUE);
      } else {
        this.writeUInt8(constants.TNS_JSON_TYPE_FALSE);
      }

    // handle numbers
    } else if (typeof value === 'number') {
      this.writeUInt8(constants.TNS_JSON_TYPE_NUMBER_LENGTH_UINT8);
      this.writeOracleNumber(value.toString());

    // handle strings
    } else if (typeof value === 'string') {
      const buf = Buffer.from(value);
      if (buf.length < 256) {
        this.writeUInt8(constants.TNS_JSON_TYPE_STRING_LENGTH_UINT8);
        this.writeUInt8(buf.length);
      } else if (buf.length < 65536) {
        this.writeUInt8(constants.TNS_JSON_TYPE_STRING_LENGTH_UINT16);
        this.writeUInt16BE(buf.length);
      } else {
        this.writeUInt8(constants.TNS_JSON_TYPE_STRING_LENGTH_UINT32);
        this.writeUInt32BE(buf.length);
      }
      if (buf.length > 0) {
        this.writeBytes(buf);
      }

    // handle dates
    } else if (util.types.isDate(value)) {
      if (value.getUTCMilliseconds() === 0) {
        this.writeUInt8(constants.TNS_JSON_TYPE_TIMESTAMP7);
        this.writeOracleDate(value, types.DB_TYPE_DATE, false);
      } else {
        this.writeUInt8(constants.TNS_JSON_TYPE_TIMESTAMP);
        this.writeOracleDate(value, types.DB_TYPE_TIMESTAMP, false);
      }

    // handle buffers
    } else if (Buffer.isBuffer(value)) {
      if (value.length < 65536) {
        this.writeUInt8(constants.TNS_JSON_TYPE_BINARY_LENGTH_UINT16);
        this.writeUInt16BE(value.length);
      } else {
        this.writeUInt8(constants.TNS_JSON_TYPE_BINARY_LENGTH_UINT32);
        this.writeUInt32BE(value.length);
      }
      this.writeBytes(value);

    // handle arrays
    } else if (Array.isArray(value)) {
      this._encodeArray(value, encoder);

    // handle vectors
    } else if (nodbUtil.isVectorValue(value)) {
      this.writeUInt8(constants.TNS_JSON_TYPE_EXTENDED);
      this.writeUInt8(constants.TNS_JSON_TYPE_VECTOR);
      const encoder = new vector.VectorEncoder();
      const buf = encoder.encode(value);
      this.writeUInt32BE(buf.length);
      this.writeBytes(buf);

    } else if (value instanceof types.JsonId) {
      this.writeUInt8(constants.TNS_JSON_TYPE_ID);
      this.writeUInt8(value.length);
      this.writeBytes(Buffer.from(value.buffer));

    // handle objects
    } else {
      this._encodeObject(value, encoder);
    }

  }

}

/**
 * Class used for encoding
 */

class OsonEncoder extends GrowableBuffer {

  //---------------------------------------------------------------------------
  // _addFieldName()
  //
  // Add a field with the given name.
  //---------------------------------------------------------------------------
  _addFieldName(name) {
    const fieldName = new OsonFieldName(name, this.maxFieldNameSize);
    this.fieldNamesMap.set(name, fieldName);
    if (fieldName.nameBytes.length <= 255) {
      this.shortFieldNamesSeg.addName(fieldName);
    } else {
      if (!this.longFieldNamesSeg) {
        this.longFieldNamesSeg = new OsonFieldNamesSegment();
      }
      this.longFieldNamesSeg.addName(fieldName);
    }
  }

  //---------------------------------------------------------------------------
  // _examineNode()
  //
  // Examines the value. If it contains fields, unique names are retained. The
  // values are then examined to see if they also contain fields. Arrays are
  // examined to determine they contain elements that contain fields.
  //---------------------------------------------------------------------------
  _examineNode(value) {
    if (Array.isArray(value)) {
      for (const element of value) {
        this._examineNode(element);
      }
    } else if (value && Array.isArray(value.fields)) {
      for (let i = 0; i < value.fields.length; i++) {
        const name = value.fields[i];
        const element = value.values[i];
        if (!this.fieldNamesMap.has(name)) {
          this._addFieldName(name);
        }
        this._examineNode(element);
      }
    }
  }

  //---------------------------------------------------------------------------
  // _writeExtendedHeader()
  //
  // Write the extended header containing information about the short and long
  // field name segments.
  //---------------------------------------------------------------------------
  _writeExtendedHeader() {
    // write number of short field names
    if (this.fieldIdSize === 1) {
      this.writeUInt8(this.shortFieldNamesSeg.fieldNames.length);
    } else if (this.fieldIdSize === 2) {
      this.writeUInt16BE(this.shortFieldNamesSeg.fieldNames.length);
    } else {
      this.writeUInt32BE(this.shortFieldNamesSeg.fieldNames.length);
    }

    // write size of short field names segment
    if (this.shortFieldNamesSeg.pos < 65536) {
      this.writeUInt16BE(this.shortFieldNamesSeg.pos);
    } else {
      this.writeUInt32BE(this.shortFieldNamesSeg.pos);
    }

    // write fields for long field names segment, if applicable
    if (this.longFieldNamesSeg) {
      let secondaryFlags = 0;
      if (this.longFieldNamesSeg.pos < 65536) {
        secondaryFlags = constants.TNS_JSON_FLAG_SEC_FNAMES_SEG_UINT16;
      }
      this.writeUInt16BE(secondaryFlags);
      this.writeUInt32BE(this.longFieldNamesSeg.fieldNames.length);
      this.writeUInt32BE(this.longFieldNamesSeg.pos);
    }
  }

  //---------------------------------------------------------------------------
  // _writeFieldNamesSeg()
  //
  // Write the contents of the field names segment to the buffer.
  //---------------------------------------------------------------------------
  _writeFieldNamesSeg(fieldNamesSeg) {
    // write array of hash ids
    for (const fieldName of fieldNamesSeg.fieldNames) {
      if (fieldName.nameBytes.length <= 255) {
        this.writeUInt8(fieldName.hashId);
      } else {
        this.writeUInt16BE(fieldName.hashId);
      }
    }

    // write array of field name offsets for the short field names
    for (const fieldName of fieldNamesSeg.fieldNames) {
      if (fieldNamesSeg.pos < 65536) {
        this.writeUInt16BE(fieldName.offset);
      } else {
        this.writeUInt32BE(fieldName.offset);
      }
    }

    // write field names
    if (fieldNamesSeg.pos > 0) {
      this.writeBytes(fieldNamesSeg.buf.subarray(0, fieldNamesSeg.pos));
    }

  }

  //---------------------------------------------------------------------------
  // encode()
  //
  // Encodes the value as OSON and returns a buffer containing the OSON bytes.
  //---------------------------------------------------------------------------
  encode(value, maxFieldNameSize) {

    this.maxFieldNameSize = maxFieldNameSize;

    // determine the flags to use
    let flags = constants.TNS_JSON_FLAG_INLINE_LEAF;
    if (Array.isArray(value) || (value && Array.isArray(value.fields))) {
      // examine all values recursively to determine the unique set of field
      // names and whether they need to be added to the long field names
      // segment (> 255 bytes) or short field names segment (<= 255 bytes)
      this.fieldNamesMap = new Map();
      this.shortFieldNamesSeg = new OsonFieldNamesSegment();
      this._examineNode(value);

      // perform processing of field names segments and determine the total
      // number of unique field names in the value
      let totalNumFieldNames = 0;
      if (this.shortFieldNamesSeg) {
        this.shortFieldNamesSeg._processFieldNames(0);
        totalNumFieldNames += this.shortFieldNamesSeg.fieldNames.length;
      }
      if (this.longFieldNamesSeg) {
        this.longFieldNamesSeg._processFieldNames(totalNumFieldNames);
        totalNumFieldNames += this.longFieldNamesSeg.fieldNames.length;
      }

      // determine remaining flags and field id size
      flags |= constants.TNS_JSON_FLAG_HASH_ID_UINT8 |
        constants.TNS_JSON_FLAG_TINY_NODES_STAT;
      if (totalNumFieldNames > 65535) {
        flags |= constants.TNS_JSON_FLAG_NUM_FNAMES_UINT32;
        this.fieldIdSize = 4;
      } else if (totalNumFieldNames > 255) {
        flags |= constants.TNS_JSON_FLAG_NUM_FNAMES_UINT16;
        this.fieldIdSize = 2;
      } else {
        this.fieldIdSize = 1;
      }
      if (this.shortFieldNamesSeg.pos > 65535) {
        flags |= constants.TNS_JSON_FLAG_FNAMES_SEG_UINT32;
      }
    } else {
      // if the value is a simple scalar
      flags |= constants.TNS_JSON_FLAG_IS_SCALAR;
    }

    // encode values into the OSON tree segment
    const treeSeg = new OsonTreeSegment();
    treeSeg.encodeNode(value, this);
    if (treeSeg.pos > 65535) {
      flags |= constants.TNS_JSON_FLAG_TREE_SEG_UINT32;
    }

    // write initial header
    this.writeUInt8(constants.TNS_JSON_MAGIC_BYTE_1);
    this.writeUInt8(constants.TNS_JSON_MAGIC_BYTE_2);
    this.writeUInt8(constants.TNS_JSON_MAGIC_BYTE_3);
    if (this.longFieldNamesSeg) {
      this.writeUInt8(constants.TNS_JSON_VERSION_MAX_FNAME_65535);
    } else {
      this.writeUInt8(constants.TNS_JSON_VERSION_MAX_FNAME_255);
    }
    this.writeUInt16BE(flags);

    // write extended header (when value is not scalar)
    if (this.shortFieldNamesSeg) {
      this._writeExtendedHeader();
    }

    // write size of tree segment
    if (treeSeg.pos < 65536) {
      this.writeUInt16BE(treeSeg.pos);
    } else {
      this.writeUInt32BE(treeSeg.pos);
    }

    // write remainder of header and any data (when value is not scalar)
    if (this.shortFieldNamesSeg) {

      // write number of "tiny" nodes (always zero)
      this.writeUInt16BE(0);

      // write the field names segments
      this._writeFieldNamesSeg(this.shortFieldNamesSeg);
      if (this.longFieldNamesSeg) {
        this._writeFieldNamesSeg(this.longFieldNamesSeg);
      }
    }

    // write tree segment data
    this.writeBytes(treeSeg.buf.subarray(0, treeSeg.pos));

    return this.buf.subarray(0, this.pos);
  }

}

module.exports = {
  OsonDecoder,
  OsonEncoder
};
