// Copyright (c) 2022, 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 { Buffer } = require('buffer');
const constants = require("./constants.js");
const errors = require("../../errors.js");
const types = require("../../types.js");
const nodbUtil = require("../../util.js");

/**
 * Base buffer class used for managing buffered data without unnecessary
 * copying.
 */
class BaseBuffer {

  //---------------------------------------------------------------------------
  // constructor()
  //
  // The initializer is either an integer specifying the size of the buffer, or
  // an existing Buffer, which is used directly.
  //---------------------------------------------------------------------------
  constructor(initializer) {
    if (typeof initializer === 'number') {
      this.buf = Buffer.alloc(initializer);
      this.size = 0;
      this.maxSize = initializer;
    } else if (initializer) {
      this.buf = initializer;
      this.size = this.maxSize = initializer.length;
    }
    this.pos = 0;
  }

  //---------------------------------------------------------------------------
  // _grow()
  //
  // Called when the buffer needs to grow. The base function simply raises an
  // error.
  //---------------------------------------------------------------------------
  _grow(numBytes) {
    errors.throwErr(errors.ERR_BUFFER_LENGTH_INSUFFICIENT, this.numBytesLeft(),
      numBytes);
  }

  //---------------------------------------------------------------------------
  // _readBytesWithLength()
  //
  // Helper function that processes the number of bytes (if needed) and then
  // acquires the specified number of bytes from the buffer. The base function
  // simply uses the length as given.
  //---------------------------------------------------------------------------
  _readBytesWithLength(numBytes) {
    return this.readBytes(numBytes);
  }

  //---------------------------------------------------------------------------
  // _readInteger()
  //
  // Read an integer from the buffer of the specified maximum size and returns
  // it. The signed flag indicates whether the value is allowed to be signed or
  // not and the skip flag indicates whether the data should simply be skipped.
  //---------------------------------------------------------------------------
  _readInteger(maxSize, signed, skip) {
    let isNegative = false;
    let size = this.readUInt8();
    if (size === 0) {
      return 0;
    } else if (size & 0x80) {
      if (!signed) {
        errors.throwErr(errors.ERR_UNEXPECTED_NEGATIVE_INTEGER, this.pos, this.packetNum);
      }
      isNegative = true;
      size = size & 0x7f;
    }
    if (size > maxSize) {
      errors.throwErr(errors.ERR_INTEGER_TOO_LARGE, size, maxSize, this.pos, this.packetNum);
    }
    if (skip) {
      this.skipBytes(size);
    } else {
      const buf = this.readBytes(size);
      const value = buf.readUIntBE(0, size);
      return (isNegative) ? -value : value;
    }
  }

  //---------------------------------------------------------------------------
  // numBytesLeft()
  //
  // Returns the number of bytes that are remaining in the buffer.
  //---------------------------------------------------------------------------
  numBytesLeft() {
    return this.size - this.pos;
  }

  //---------------------------------------------------------------------------
  // parseBinaryDouble()
  //
  // Parses a binary double from the supplied buffer and returns a Number.
  // It is assumed at this point that the size of the buffer is 8 bytes. A copy
  // is made of the buffer in order to ensure that the original buffer is not
  // modified. If it is and data spans multiple packets, incorrect data may be
  // returned!
  //---------------------------------------------------------------------------
  parseBinaryDouble(buf) {
    buf = Buffer.from(buf);
    if (buf[0] & 0x80) {
      buf[0] &= 0x7f;
    } else {
      // complement the bits for a negative number
      buf[0] ^= 0xff;
      buf[1] ^= 0xff;
      buf[2] ^= 0xff;
      buf[3] ^= 0xff;
      buf[4] ^= 0xff;
      buf[5] ^= 0xff;
      buf[6] ^= 0xff;
      buf[7] ^= 0xff;
    }
    return buf.readDoubleBE();
  }

  //---------------------------------------------------------------------------
  // parseBinaryFloat()
  //
  // Parses a binary float from the supplied buffer and returns a Number.  It
  // is assumed at this point that the size of the buffer is 4 bytes. A copy is
  // made of the buffer in order to ensure that the original buffer is not
  // modified. If it is and data spans multiple packets, incorrect data may be
  // returned!
  //---------------------------------------------------------------------------
  parseBinaryFloat(buf) {
    buf = Buffer.from(buf);
    if (buf[0] & 0x80) {
      buf[0] &= 0x7f;
    } else {
      // complement the bits for a negative number
      buf[0] ^= 0xff;
      buf[1] ^= 0xff;
      buf[2] ^= 0xff;
      buf[3] ^= 0xff;
    }
    return buf.readFloatBE();
  }

  //---------------------------------------------------------------------------
  // parseOracleDate()
  //
  // Parses an Oracle date from the supplied buffer and returns a Date. It is
  // assumed at this point that the size of the buffer is either 7 bytes (date
  // or compressed timestamp), 11 bytes (timestamp) or 13 bytes (timestamp with
  // time zone). Time zone information is discarded because Node.js uses UTC
  // timestamps and the server returns the data in that format, too. The Date
  // type in Node.js doesn't support time zone information.
  //---------------------------------------------------------------------------
  parseOracleDate(buf, useLocalTime = true) {
    let fseconds = 0;
    if (buf.length >= 11) {
      fseconds = Math.floor(buf.readUInt32BE(7) / (1000 * 1000));
    }
    const year = (buf[0] - 100) * 100 + buf[1] - 100;
    return nodbUtil.makeDate(useLocalTime, year, buf[2], buf[3], buf[4] - 1,
      buf[5] - 1, buf[6] - 1, fseconds, 0);
  }

  //---------------------------------------------------------------------------
  // parseOracleNumber()
  //
  // Parses an Oracle number from the supplied buffer and returns a Number. It
  // is assumed at this point that the buffer only contains the encoded numeric
  // data.
  //---------------------------------------------------------------------------
  parseOracleNumber(buf) {

    // the first byte is the exponent; positive numbers have the highest
    // order bit set, whereas negative numbers have the highest order bit
    // cleared and the bits inverted
    let exponent = buf[0];
    const isPositive = Boolean(exponent & 0x80);
    if (!isPositive) {
      exponent = (exponent ^ 0xFF);
    }
    exponent -= 193;
    let decimalPointIndex = exponent * 2 + 2;

    // a mantissa length of 0 implies a value of 0 (if positive) or a value
    // of -1e126 (if negative)
    if (buf.length === 1) {
      if (isPositive) {
        return "0";
      }
      return "-1e126";
    }

    // check for the trailing 102 byte for negative numbers and, if present,
    // reduce the number of mantissa digits
    let numBytes = buf.length;
    if (!isPositive && buf[buf.length - 1] === 102) {
      numBytes -= 1;
    }

    // process the mantissa bytes which are the remaining bytes; each
    // mantissa byte is a base-100 digit
    let base100Digit;
    const digits = [];
    for (let i = 1; i < numBytes; i++) {

      // positive numbers have 1 added to them; negative numbers are
      // subtracted from the value 101
      if (isPositive) {
        base100Digit = buf[i] - 1;
      } else {
        base100Digit = 101 - buf[i];
      }

      // process the first digit; leading zeroes are ignored
      let digit = Math.floor(base100Digit / 10);
      if (digit === 0 && i === 1) {
        decimalPointIndex -= 1;
      } else if (digit === 10) {
        digits.push("1");
        digits.push("0");
        decimalPointIndex += 1;
      } else if (digit !== 0 || i > 1) {
        digits.push(digit.toString());
      }

      // process the second digit; trailing zeroes are ignored
      digit = base100Digit % 10;
      if (digit !== 0 || i < numBytes - 1) {
        digits.push(digit.toString());
      }
    }

    // create string of digits for transformation to JS value
    const chars = [];

    // if negative, include the sign
    if (!isPositive) {
      chars.push("-");
    }

    // if the decimal point index is 0 or less, add the decimal point and
    // any leading zeroes that are needed
    if (decimalPointIndex <= 0) {
      chars.push(".");
      if (decimalPointIndex < 0)
        chars.push("0".repeat(-decimalPointIndex));
    }

    // add each of the digits
    for (let i = 0; i < digits.length; i++) {
      if (i > 0 && i === decimalPointIndex) {
        chars.push(".");
      }
      chars.push(digits[i]);
    }

    // if the decimal point index exceeds the number of digits, add any
    // trailing zeroes that are needed
    if (decimalPointIndex > digits.length) {
      for (let i = digits.length; i < decimalPointIndex; i++) {
        chars.push("0");
      }
    }

    // convert result to a Number
    return chars.join("");
  }

  //---------------------------------------------------------------------------
  // readBinaryDouble()
  //
  // Reads a binary double value from the buffer and returns a Number or a
  // String, depending on the desired type.
  //---------------------------------------------------------------------------
  readBinaryDouble() {
    const buf = this.readBytesWithLength();
    if (!buf) {
      return null;
    }
    return this.parseBinaryDouble(buf);
  }

  //---------------------------------------------------------------------------
  // readBinaryFloat()
  //
  // Reads a binary float value from the buffer and returns a Number or a
  // String, depending on the desired type.
  //---------------------------------------------------------------------------
  readBinaryFloat() {
    const buf = this.readBytesWithLength();
    if (!buf) {
      return null;
    }
    return this.parseBinaryFloat(buf);
  }

  //---------------------------------------------------------------------------
  // readBinaryInteger()
  //
  // Reads a binary integer value from the buffer and returns a Number
  //---------------------------------------------------------------------------
  readBinaryInteger() {
    const buf = this.readBytesWithLength();
    if (!buf) {
      return 0;
    }
    if (buf.length > 4) {
      errors.throwErr(errors.ERR_INTEGER_TOO_LARGE, buf.length, 4, this.pos, this.packetNum);
    }
    return (buf.readIntBE(0, buf.length));
  }

  //---------------------------------------------------------------------------
  // readBool()
  //
  // Reads a boolean value from the buffer and returns a Boolean.
  //---------------------------------------------------------------------------
  readBool() {
    const buf = this.readBytesWithLength();
    if (!buf) {
      return null;
    }
    return (buf[buf.length - 1] === 1);
  }

  //---------------------------------------------------------------------------
  // readBytes()
  //
  // Returns a Buffer containing the specified number of bytes. If an
  // insufficient number of bytes are available an error is thrown.
  //---------------------------------------------------------------------------
  readBytes(numBytes) {
    const numBytesLeft = this.numBytesLeft();
    if (numBytes > numBytesLeft) {
      errors.throwErr(errors.ERR_UNEXPECTED_END_OF_DATA, numBytes,
        numBytesLeft);
    }
    const buf = this.buf.subarray(this.pos, this.pos + numBytes);
    this.pos += numBytes;
    return buf;
  }

  //---------------------------------------------------------------------------
  // readBytesWithLength()
  //
  // Reads the length from the buffer and then returns a Buffer containing the
  // specified number of bytes. If the length is 0 or the special null length
  // indicator value, null is returned instead.
  //---------------------------------------------------------------------------
  readBytesWithLength() {
    const numBytes = this.readUInt8();
    if (numBytes === 0 || numBytes === constants.TNS_NULL_LENGTH_INDICATOR)
      return null;
    return this._readBytesWithLength(numBytes);
  }

  //---------------------------------------------------------------------------
  // readDbObject()
  //
  // Reads a database object from the buffer and returns the implementation
  // object (or null, if the object is atomically null).
  //---------------------------------------------------------------------------
  readDbObject() {
    const obj = {};
    let numBytes = this.readUB4();
    if (numBytes > 0)
      obj.toid = Buffer.from(this.readBytesWithLength());
    numBytes = this.readUB4();
    if (numBytes > 0)
      obj.oid = Buffer.from(this.readBytesWithLength());
    numBytes = this.readUB4();
    if (numBytes > 0)
      obj.snapshot = Buffer.from(this.readBytesWithLength());
    this.skipUB2();                     // version
    numBytes = this.readUB4();
    this.skipUB2();                     // flags
    if (numBytes > 0)
      obj.packedData = Buffer.from(this.readBytesWithLength());
    return obj;
  }

  //---------------------------------------------------------------------------
  // readInt8()
  //
  // Reads a signed 8-bit integer from the buffer.
  //---------------------------------------------------------------------------
  readInt8() {
    const buf = this.readBytes(1);
    return buf.readInt8();
  }

  //---------------------------------------------------------------------------
  // readOracleDate()
  //
  // Reads an Oracle date from the buffer and returns a Date or a String,
  // depending on the desired type.
  //---------------------------------------------------------------------------
  readOracleDate(useLocalTime) {
    const buf = this.readBytesWithLength();
    if (!buf) {
      return null;
    }
    return this.parseOracleDate(buf, useLocalTime);
  }

  //---------------------------------------------------------------------------
  // readOracleNumber()
  //
  // Reads an Oracle number from the buffer and returns a Number or a String,
  // depending on the desired type.
  //---------------------------------------------------------------------------
  readOracleNumber() {
    const buf = this.readBytesWithLength();
    if (!buf) {
      return null;
    }
    return this.parseOracleNumber(buf);
  }

  //---------------------------------------------------------------------------
  // readSB2()
  //
  // Reads a signed, variable length integer of up to 2 bytes in length.
  //---------------------------------------------------------------------------
  readSB2() {
    return this._readInteger(2, true, false);
  }

  //---------------------------------------------------------------------------
  // readSB4()
  //
  // Reads a signed, variable length integer of up to 4 bytes in length.
  //---------------------------------------------------------------------------
  readSB4() {
    return this._readInteger(4, true, false);
  }

  //---------------------------------------------------------------------------
  // readSB8()
  //
  // Reads a signed, variable length integer of up to 8 bytes in length.
  //---------------------------------------------------------------------------
  readSB8() {
    return this._readInteger(8, true, false);
  }

  //---------------------------------------------------------------------------
  // readStr()
  //
  // Reads a string from the buffer in the specified character set form.
  //---------------------------------------------------------------------------
  readStr(csfrm) {
    const buf = this.readBytesWithLength();
    if (!buf) {
      return null;
    }
    if (csfrm === constants.CSFRM_IMPLICIT)
      return buf.toString();

    // need a copy of the buffer since swap16() changes the buffer in place and
    // it is possible that the buffer may need to be rescanned (for the case
    // where insufficient packets are available during the initial scan)
    return Buffer.from(buf).swap16().toString('utf16le');
  }

  //---------------------------------------------------------------------------
  // readUB2()
  //
  // Reads an unsigned, variable length integer of up to 2 bytes in length.
  //---------------------------------------------------------------------------
  readUB2() {
    return this._readInteger(2, false, false);
  }

  //---------------------------------------------------------------------------
  // readUB4()
  //
  // Reads an unsigned, variable length integer of up to 4 bytes in length.
  //---------------------------------------------------------------------------
  readUB4() {
    return this._readInteger(4, false, false);
  }

  //---------------------------------------------------------------------------
  // readUB8()
  //
  // Reads an unsigned, variable length integer of up to 8 bytes in length.
  //---------------------------------------------------------------------------
  readUB8() {
    return this._readInteger(8, false, false);
  }

  //---------------------------------------------------------------------------
  // readUInt8()
  //
  // Reads an unsigned 8-bit integer from the buffer.
  //---------------------------------------------------------------------------
  readUInt8() {
    const buf = this.readBytes(1);
    return buf[0];
  }

  //---------------------------------------------------------------------------
  // readUInt16BE()
  //
  // Reads an unsigned 16-bit integer from the buffer in big endian order.
  //---------------------------------------------------------------------------
  readUInt16BE() {
    const buf = this.readBytes(2);
    return buf.readUInt16BE();
  }

  //---------------------------------------------------------------------------
  // readUInt16LE()
  //
  // Reads an unsigned 16-bit integer from the buffer in little endian order.
  //---------------------------------------------------------------------------
  readUInt16LE() {
    const buf = this.readBytes(2);
    return buf.readUInt16LE();
  }

  //---------------------------------------------------------------------------
  // readUInt32BE()
  //
  // Reads an unsigned 32-bit integer from the buffer in big endian order.
  //---------------------------------------------------------------------------
  readUInt32BE() {
    const buf = this.readBytes(4);
    return buf.readUInt32BE();
  }

  //---------------------------------------------------------------------------
  // reserveBytes()
  //
  // Reserves the specified number of bytes in the buffer. If not enough bytes
  // remain in the buffer, the buffer is grown.
  //---------------------------------------------------------------------------
  reserveBytes(numBytes) {
    if (numBytes > this.numBytesLeft()) {
      this._grow(this.pos + numBytes);
    }
    const pos = this.pos;
    this.pos += numBytes;
    return pos;
  }

  //---------------------------------------------------------------------------
  // skipBytes()
  //
  // Skips the specified number of bytes in the buffer.
  //---------------------------------------------------------------------------
  skipBytes(numBytes) {
    if (numBytes > this.numBytesLeft())
      errors.throwErr(errors.ERR_UNEXPECTED_END_OF_DATA);
    this.pos += numBytes;
  }

  //---------------------------------------------------------------------------
  // skipSB4()
  //
  // Skips a signed, variable length integer of up to 4 bytes in length.
  //---------------------------------------------------------------------------
  skipSB4() {
    return this._readInteger(4, true, true);
  }

  //---------------------------------------------------------------------------
  // skipUB1()
  //
  // Skips a single byte integer in the buffer.
  //---------------------------------------------------------------------------
  skipUB1() {
    this.skipBytes(1);
  }

  //---------------------------------------------------------------------------
  // skipUB2()
  //
  // Skips an unsigned, variable length integer of up to 2 bytes in length.
  //---------------------------------------------------------------------------
  skipUB2() {
    return this._readInteger(2, false, true);
  }

  //---------------------------------------------------------------------------
  // skipUB4()
  //
  // Skips an unsigned, variable length integer of up to 4 bytes in length.
  //---------------------------------------------------------------------------
  skipUB4() {
    return this._readInteger(4, false, true);
  }

  //---------------------------------------------------------------------------
  // skipUB8()
  //
  // Skips an unsigned, variable length integer of up to 8 bytes in length.
  //---------------------------------------------------------------------------
  skipUB8() {
    return this._readInteger(8, false, true);
  }

  //---------------------------------------------------------------------------
  // writeBinaryDouble()
  //
  // Writes the number in binary double format to the buffer.
  //---------------------------------------------------------------------------
  writeBinaryDouble(n, pos) {
    if (!pos) {
      pos = this.reserveBytes(8);
    }
    this.buf.writeDoubleBE(n, pos);
    if ((this.buf[pos] & 0x80) === 0) {
      this.buf[pos] |= 0x80;
    } else {
      // We complement the bits for a negative number
      this.buf[pos] ^= 0xff;
      this.buf[pos + 1] ^= 0xff;
      this.buf[pos + 2] ^= 0xff;
      this.buf[pos + 3] ^= 0xff;
      this.buf[pos + 4] ^= 0xff;
      this.buf[pos + 5] ^= 0xff;
      this.buf[pos + 6] ^= 0xff;
      this.buf[pos + 7] ^= 0xff;
    }
  }

  //---------------------------------------------------------------------------
  // writeBinaryFloat()
  //
  // Writes the number in binary float format to the buffer.
  //---------------------------------------------------------------------------
  writeBinaryFloat(n, pos) {
    if (!pos) {
      pos = this.reserveBytes(4);
    }
    this.buf.writeFloatBE(n, pos);
    if ((this.buf[pos] & 0x80) === 0) {
      this.buf[pos] |= 0x80;
    } else {
      // We complement the bits for a negative number
      this.buf[pos] ^= 0xff;
      this.buf[pos + 1] ^= 0xff;
      this.buf[pos + 2] ^= 0xff;
      this.buf[pos + 3] ^= 0xff;
    }
  }

  //---------------------------------------------------------------------------
  // writeBytes()
  //
  // Writes the bytes in the supplied buffer to the buffer.
  //---------------------------------------------------------------------------
  writeBytes(value) {
    let start = 0;
    let valueLen = value.length;
    while (valueLen > 0) {
      const bytesLeft = this.numBytesLeft();
      if (bytesLeft === 0) {
        this._grow(this.pos + valueLen);
      }
      const bytesToWrite = Math.min(bytesLeft, valueLen);
      value.copy(this.buf, this.pos, start, start + bytesToWrite);
      this.pos += bytesToWrite;
      start += bytesToWrite;
      valueLen -= bytesToWrite;
    }
  }

  // _writeRawBytesAndLength()
  //
  // Writes the length in the format required before
  // writing the bytes.
  //---------------------------------------------------------------------------
  _writeRawBytesAndLength(value, numBytes) {
    if (numBytes <= constants.TNS_MAX_SHORT_LENGTH) {
      this.writeUInt8(numBytes);
      if (numBytes > 0) {
        this.writeBytes(value);
      }
    } else {
      let start = 0;
      this.writeUInt8(constants.TNS_LONG_LENGTH_INDICATOR);
      while (numBytes > 0) {
        const chunkLen = Math.min(numBytes, constants.BUFFER_CHUNK_SIZE);
        this.writeUB4(chunkLen);
        this.writeBytes(value.subarray(start, start + chunkLen));
        numBytes -= chunkLen;
        start += chunkLen;
      }
      this.writeUB4(0);
    }
  }
  //---------------------------------------------------------------------------
  // writeBytesWithLength()
  //
  // Writes the bytes in the supplied buffer to the buffer, but first writes
  // the length. If the length exceeds a fixed value, the value is written in
  // chunks instead.
  //---------------------------------------------------------------------------
  writeBytesWithLength(value) {
    const numBytes = value.length;
    this._writeRawBytesAndLength(value, numBytes);
  }

  //---------------------------------------------------------------------------
  // writeDbObject()
  //
  // Writes a database object to the buffer.
  //---------------------------------------------------------------------------
  writeDbObject(obj) {
    this.writeUB4(obj.toid.length);
    this.writeBytesWithLength(obj.toid);
    if (obj.oid) {
      this.writeUB4(obj.oid.length);
      this.writeBytesWithLength(obj.oid);
    } else {
      this.writeUB4(0);
    }
    this.writeUB4(0);                   // snapshot
    this.writeUB4(0);                   // version
    const packedData = obj._getPackedData();
    this.writeUB4(packedData.length);
    this.writeUB4(obj.flags);
    this.writeBytesWithLength(packedData);
  }

  //---------------------------------------------------------------------------
  // writeOracleDate()
  //
  // Writes the date to the buffer using the given Oracle type. Note that if a
  // timestamp with zero milliseconds is written, the type is automatically
  // changed to DB_TYPE_DATE (except for DB_TYPE_TIMESTAMP_TZ which requires
  // the full amount to be written).
  //---------------------------------------------------------------------------
  writeOracleDate(date, type, writeLength = true) {
    let fsec;
    let length = type._bufferSizeFactor;
    if (length > 7) {
      fsec = date.getUTCMilliseconds() * 1000 * 1000;
      if (fsec === 0 && length <= 11)
        length = 7;
    }
    if (writeLength) {
      this.writeUInt8(length);
    }
    const pos = this.reserveBytes(length);
    if (type === types.DB_TYPE_DATE || type == types.DB_TYPE_TIMESTAMP) {
      const year = date.getFullYear();
      this.buf[pos] = Math.trunc(year / 100) + 100;
      this.buf[pos + 1] = year % 100 + 100;
      this.buf[pos + 2] = date.getMonth() + 1;
      this.buf[pos + 3] = date.getDate();
      this.buf[pos + 4] = date.getHours() + 1;
      this.buf[pos + 5] = date.getMinutes() + 1;
      this.buf[pos + 6] = date.getSeconds() + 1;
    } else {
      const year = date.getUTCFullYear();
      this.buf[pos] = Math.trunc(year / 100) + 100;
      this.buf[pos + 1] = year % 100 + 100;
      this.buf[pos + 2] = date.getUTCMonth() + 1;
      this.buf[pos + 3] = date.getUTCDate();
      this.buf[pos + 4] = date.getUTCHours() + 1;
      this.buf[pos + 5] = date.getUTCMinutes() + 1;
      this.buf[pos + 6] = date.getUTCSeconds() + 1;
    }
    if (length > 7) {
      this.buf.writeInt32BE(fsec, pos + 7);
      if (length > 11) {
        this.buf[pos + 11] = constants.TZ_HOUR_OFFSET;
        this.buf[pos + 12] = constants.TZ_MINUTE_OFFSET;
      }
    }
  }

  //---------------------------------------------------------------------------
  // writeOracleNumber()
  //
  // Writes the number (in string form) in Oracle Number format to the buffer.
  //---------------------------------------------------------------------------
  writeOracleNumber(value) {

    // determine if number is negative
    let isNegative = false;
    if (value[0] === '-') {
      isNegative = true;
      value = value.substring(1);
    }

    // parse the exponent, if one is present
    let exponent = 0;
    const exponentPos = value.indexOf('e');
    if (exponentPos > 0) {
      exponent = Number(value.substring(exponentPos + 1));
      value = value.substring(0, exponentPos);
    }

    // adjust the exponent and the value if there is a decimal point
    const decimalPos = value.indexOf('.');
    if (decimalPos > 0) {
      exponent -= (value.length - decimalPos - 1);
      value = value.substring(0, decimalPos) + value.substring(decimalPos + 1);
    }

    // strip any leading zeroes
    if (value[0] === '0') {
      value = value.replace(/^0+/, "");
    }

    // strip any trailing zeroes
    if (value.length > 0 && value[value.length - 1] === '0') {
      const trimmedValue = value.replace(/0+$/, "");
      exponent += (value.length - trimmedValue.length);
      value = trimmedValue;
    }

    // throw exception if number cannot be represented as an Oracle Number
    if (value.length > constants.NUMBER_MAX_DIGITS || exponent >= 126 ||
        exponent <= -131) {
      errors.throwErr(errors.ERR_ORACLE_NUMBER_NO_REPR);
    }

    // if the exponent is odd, append a zero
    if ((exponent > 0 && exponent % 2 === 1) ||
        (exponent < 0 && exponent % 2 === -1)) {
      exponent--;
      value += "0";
    }

    // add a leading zero if the number of digits is odd
    if (value.length % 2 === 1) {
      value = "0" + value;
    }

    // write the encoded data to the wire
    const appendSentinel =
      (isNegative && value.length < constants.NUMBER_MAX_DIGITS);
    const numPairs = value.length / 2;
    let exponentOnWire = ((exponent + value.length) / 2) + 192;
    if (isNegative) {
      exponentOnWire = (exponentOnWire ^ 0xFF);
    } else if (value.length === 0 && exponent === 0) {
      exponentOnWire = 128;
    }
    let pos = this.reserveBytes(numPairs + 2 + appendSentinel);
    this.buf[pos++] = numPairs + 1 + appendSentinel;
    this.buf[pos++] = exponentOnWire;
    for (let i = 0; i < value.length; i += 2) {
      const base100Digit = Number(value.substring(i, i + 2));
      if (isNegative) {
        this.buf[pos++] = 101 - base100Digit;
      } else {
        this.buf[pos++] = base100Digit + 1;
      }
    }
    if (appendSentinel) {
      this.buf[pos] = 102;
    }

  }

  //---------------------------------------------------------------------------
  // writeStr()
  //
  // Writes the string to the buffer.
  //---------------------------------------------------------------------------
  writeStr(s) {
    this.writeBytes(Buffer.from(s));
  }

  //---------------------------------------------------------------------------
  // writeInt32BE()
  //
  // Writes a signed 32-bit integer to the buffer in big endian order.
  //---------------------------------------------------------------------------
  writeInt32BE(n) {
    const pos = this.reserveBytes(4);
    this.buf.writeInt32BE(n, pos);
  }

  //---------------------------------------------------------------------------
  // writeUB4()
  //
  // Writes an unsigned integer (up to 4 bytes in length) in variable length
  // format to the buffer.
  //---------------------------------------------------------------------------
  writeUB4(value) {
    if (value === 0) {
      this.writeUInt8(0);
    } else if (value <= 0xff) {
      this.writeUInt8(1);
      this.writeUInt8(value);
    } else if (value <= 0xffff) {
      this.writeUInt8(2);
      this.writeUInt16BE(value);
    } else {
      this.writeUInt8(4);
      this.writeUInt32BE(value);
    }
  }

  //---------------------------------------------------------------------------
  // writeUB2()
  //
  // Writes an unsigned integer (up to 2 bytes in length) in variable length
  // format to the buffer.
  //---------------------------------------------------------------------------
  writeUB2(value) {
    if (value === 0) {
      this.writeUInt8(0);
    } else if (value <= 0xff) {
      this.writeUInt8(1);
      this.writeUInt8(value);
    } else {
      this.writeUInt8(2);
      this.writeUInt16BE(value);
    }
  }

  //---------------------------------------------------------------------------
  // writeUB8()
  //
  // Writes an unsigned integer (up to 8 bytes in length) in variable length
  // format to the buffer.
  //---------------------------------------------------------------------------
  writeUB8(value) {
    if (value === 0) {
      this.writeUInt8(0);
    } else if (value <= 0xff) {
      this.writeUInt8(1);
      this.writeUInt8(value);
    } else if (value <= 0xffff) {
      this.writeUInt8(2);
      this.writeUInt16BE(value);
    } else if (value <= 0xffffffff) {
      this.writeUInt8(4);
      this.writeUInt32BE(value);
    } else {
      this.writeUInt8(8);
      this.writeUInt64BE(value);
    }
  }

  //---------------------------------------------------------------------------
  // writeUInt8()
  //
  // Writes an unsigned 8-bit integer to the buffer.
  //---------------------------------------------------------------------------
  writeUInt8(n) {
    const pos = this.reserveBytes(1);
    this.buf[pos] = n;
  }

  // writeSB1()
  //
  // Writes an signed 8-bit integer to the buffer.
  //---------------------------------------------------------------------------
  writeSB1(n) {
    const pos = this.reserveBytes(1);
    this.buf[pos] = n;
  }

  //---------------------------------------------------------------------------
  // writeUInt16BE()
  //
  // Writes an unsigned 16-bit integer to the buffer in big endian order.
  //---------------------------------------------------------------------------
  writeUInt16BE(n) {
    const pos = this.reserveBytes(2);
    this.buf.writeUInt16BE(n, pos);
  }

  //---------------------------------------------------------------------------
  // writeUInt32BE()
  //
  // Writes an unsigned 32-bit integer to the buffer in big endian order.
  //---------------------------------------------------------------------------
  writeUInt32BE(n) {
    const pos = this.reserveBytes(4);
    this.buf.writeUInt32BE(n, pos);
  }

  //---------------------------------------------------------------------------
  // writeUInt64BE()
  //
  // Writes an unsigned 64-bit integer to the buffer in big endian order. Since
  // Node.js doesn't support anything above 32-bits without using BigInt, the
  // higher order bits are simply written as 0.
  //---------------------------------------------------------------------------
  writeUInt64BE(n) {
    const pos = this.reserveBytes(8);
    this.buf.writeUInt32BE(0, pos);
    this.buf.writeUInt32BE(n, pos + 4);
  }

  //---------------------------------------------------------------------------
  // writeUInt16LE()
  //
  // Writes an unsigned 16-bit integer to the buffer in little endian order.
  //---------------------------------------------------------------------------
  writeUInt16LE(n) {
    const pos = this.reserveBytes(2);
    this.buf.writeUInt16LE(n, pos);
  }

}

class GrowableBuffer extends BaseBuffer {

  //---------------------------------------------------------------------------
  // constructor()
  //
  // Initializes the buffer with an initial fixed chunk size.
  //---------------------------------------------------------------------------
  constructor(initializer) {
    if (initializer) {
      super(initializer);
    } else {
      super(constants.BUFFER_CHUNK_SIZE);
      this.size = this.maxSize;
    }
  }

  //---------------------------------------------------------------------------
  // _grow()
  //
  // Called when the buffer needs to grow. Ensures that sufficient space is
  // allocated to include the requested number of bytes, rounded to the nearest
  // chunk size.
  //---------------------------------------------------------------------------
  _grow(numBytes) {
    const remainder = numBytes % constants.BUFFER_CHUNK_SIZE;
    if (remainder > 0) {
      numBytes += (constants.BUFFER_CHUNK_SIZE - remainder);
    }
    const buf = Buffer.allocUnsafe(numBytes);
    this.buf.copy(buf);
    this.buf = buf;
    this.maxSize = this.size = numBytes;
  }
}

module.exports = {
  BaseBuffer,
  GrowableBuffer
};
