// 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 { BaseBuffer } = require('../../impl/datahandlers/buffer.js');
const { Buffer } = require('buffer');
const constants = require('./constants.js');
const oson = require('../../impl/datahandlers/oson.js');
const utils = require('./utils.js');
const vector = require('../../impl/datahandlers/vector.js');
const errors = require("../../errors.js");

const TNS_BASE64_ALPHABET_ARRAY = Buffer.from("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", 'utf8');
const FAST_AUTH_END_OF_RPC_VALUE = 0x800;
const FAST_AUTH_END_OF_RPC_OFFSET = 0x8;
const MSG_TYPE_OFFSET = 11;

/**
 * Class used for byte chunks used in the ChunkedBytesBuffer.
 */
class BytesChunk {

  /**
   * Constructor.
   * @param {Number} number of bytes to add to the chunk (rounded to the
   * nearest chunk size to avoid unnecessary allocations and copies)
   */
  constructor(numBytes) {
    this.allocLen = numBytes;
    const remainder = numBytes % constants.CHUNKED_BYTES_CHUNK_SIZE;
    if (remainder > 0) {
      this.allocLen += (constants.CHUNKED_BYTES_CHUNK_SIZE - remainder);
    }
    this.buf = Buffer.allocUnsafe(this.allocLen);
    this.actualLen = 0;
  }

}


/**
 * Class used for handling chunked reads.
 */
class ChunkedBytesBuffer {

  /**
   * Constructor.
   */
  constructor() {
    this.chunks = [];
  }

  /**
   * End the chunked read and return a consolidated buffer.
   */
  endChunkedRead() {
    if (this.chunks.length > 1) {
      let totalNumBytes = 0;
      for (const chunk of this.chunks) {
        totalNumBytes += chunk.actualLen;
      }
      let pos = 0;
      const consolidatedChunk = new BytesChunk(totalNumBytes);
      for (const chunk of this.chunks) {
        chunk.buf.copy(consolidatedChunk.buf, pos, 0, chunk.actualLen);
        pos += chunk.actualLen;
      }
      consolidatedChunk.actualLen = totalNumBytes;
      this.chunks = [consolidatedChunk];
    }
    const chunk = this.chunks[0];
    return chunk.buf.subarray(0, chunk.actualLen);
  }

  /**
   * Constructor.
   */
  getBuf(numBytes) {
    let chunk;
    if (this.chunks.length > 0) {
      chunk = this.chunks[this.chunks.length - 1];
      if (chunk.allocLen - chunk.actualLen < numBytes) {
        chunk = undefined;
      }
    }
    if (!chunk) {
      chunk = new BytesChunk(numBytes);
      this.chunks.push(chunk);
    }
    const buf = chunk.buf.subarray(chunk.actualLen,
      chunk.actualLen + numBytes);
    chunk.actualLen += numBytes;
    return buf;
  }

  /**
   * Start a chunked read. This ensures that only one chunk is available and
   * its actual length is set to zero.
   */
  startChunkedRead() {
    if (this.chunks.length > 0) {
      this.chunks = this.chunks.splice(0, 1);
      this.chunks[0].actualLen = 0;
    }
  }

}


/**
 * Encapsulates the Network Read Buffer
 *
 * @class ReadPacket
 */

class ReadPacket extends BaseBuffer {

  /**
   * Constructor.
   * @param {Object} adapter used for sending/receiving data
   * @param {Object} capabilities
   */

  constructor(nsi, caps) {
    super();
    this.nsi = nsi;
    this.caps = caps;
    this.chunkedBytesBuf = new ChunkedBytesBuffer();
  }

  /**
   * Helper function that processes the length. If the length is defined as
   * TNS_LONG_LENGTH_INDICATOR, a chunked read is performed.
   */
  _readBytesWithLength(numBytes) {
    if (numBytes !== constants.TNS_LONG_LENGTH_INDICATOR) {
      return this.readBytes(numBytes);
    }
    this.chunkedBytesBuf.startChunkedRead();
    while (true) { // eslint-disable-line
      const numBytesInChunk = this.readUB4();
      if (numBytesInChunk === 0) {
        break;
      }
      this.readBytes(numBytesInChunk, true);
    }
    return this.chunkedBytesBuf.endChunkedRead();
  }

  skipBytes(numBytes) {

    // if no bytes are left in the buffer, a new packet needs to be fetched
    // before anything else can take place
    if (this.pos === this.size) {
      this.receivePacket();
    }

    // if there is enough room in the buffer to satisfy the number of bytes
    // requested, return the buffer directly
    const numBytesLeft = this.numBytesLeft();
    if (numBytes <= numBytesLeft) {
      this.pos += numBytes;
      return;
    }
    numBytes -= numBytesLeft;

    // acquire packets until the requested number of bytes is satisfied
    while (numBytes > 0) {
      this.receivePacket();
      const numSplitBytes = Math.min(numBytes, this.size - this.pos);
      this.pos += numSplitBytes;
      numBytes -= numSplitBytes;
    }
  }

  /**
   * Returns a buffer containing the specified number of bytes. If an
   * insufficient number of bytes are available, a new packet is read.
   * @param {Number} specifies the number of bytes to read from the buffer
   */
  readBytes(numBytes, inChunkedRead = false) {

    // if no bytes are left in the buffer, a new packet needs to be fetched
    // before anything else can take place
    if (this.pos === this.size) {
      this.receivePacket();
    }

    // if there is enough room in the buffer to satisfy the number of bytes
    // requested, return the buffer directly
    const numBytesLeft = this.numBytesLeft();
    if (numBytes <= numBytesLeft) {
      let buf;
      if (inChunkedRead) {
        buf = this.chunkedBytesBuf.getBuf(numBytes);
        this.buf.copy(buf, 0, this.pos, this.pos + numBytes);
      } else {
        buf = this.buf.subarray(this.pos, this.pos + numBytes);
      }
      this.pos += numBytes;
      return buf;
    }

    // the requested bytes are split across multiple packets; if a chunked read
    // is in progress, a chunk is acquired that will accommodate the requested
    // bytes; otherwise, a separate buffer will be allocated to accommodate the
    // requested bytes
    let buf;
    if (inChunkedRead) {
      buf = this.chunkedBytesBuf.getBuf(numBytes);
    } else {
      buf = Buffer.allocUnsafe(numBytes);
    }

    // copy the bytes to the buffer from the remainder of this packet
    let offset = 0;
    this.buf.copy(buf, offset, this.pos, this.pos + numBytesLeft);
    offset += numBytesLeft;
    numBytes -= numBytesLeft;

    // acquire packets until the requested number of bytes is satisfied
    while (numBytes > 0) {
      this.receivePacket();
      const numSplitBytes = Math.min(numBytes, this.size - this.pos);
      this.buf.copy(buf, offset, this.pos, this.pos + numSplitBytes);
      this.pos += numSplitBytes;
      offset += numSplitBytes;
      numBytes -= numSplitBytes;
    }

    return buf;

  }

  /**
   * Receives a packet from the adapter.
   */
  receivePacket() {
    if (this.savedPacketPos === this.savedPackets.length) {
      const packet = this.nsi.syncRecvPacket();
      if (!packet || this.nsi.isBreak)
        throw new utils.OutOfPacketsError();
      this.savedPackets.push(packet);
    }
    this.startPacket(this.savedPackets[this.savedPacketPos++]);
  }

  restorePoint() {
    this.savedPacketPos = 0;
    this.startPacket(this.savedPackets[this.savedPacketPos++]);
    this.pos = this.savedPos;
  }

  savePoint() {
    if (this.savedPackets) {
      this.savedPackets = this.savedPackets.splice(this.savedPacketPos - 1);
    } else {
      this.savedPackets = [this.packet];
    }
    this.savedPacketPos = 1;
    this.savedPos = this.pos;
  }

  startPacket(packet) {
    this.packet = packet;
    this.buf = packet.buf;
    this.pos = 10;                      // skip packet heaader and data flags
    this.size = packet.buf.length;
    this.packetNum = packet.num;
  }

  /**
   * Read packets from network.
   * If checkRequestBoundary is passed as true, it
   * would read all packets until end of request
   * boundary is seen in nwk header.
   */
  async waitForPackets(checkRequestBoundary = false) {
    let packet = await this.nsi.recvPacket();
    if (!this.savedPackets) {
      this.savedPackets = [packet];
      this.savedPacketPos = 0;
    } else {
      this.savedPackets.push(packet);
    }
    if (checkRequestBoundary && this.nsi.endOfRequestSupport) {
      while (packet.type === constants.TNS_PACKET_TYPE_DATA) {
        // End Marker
        if ((packet.buf.readUInt16BE(8) &
            constants.TNS_DATA_FLAGS_END_OF_REQUEST)) {
          break;
        }

        // Single Byte 1D packet
        if (packet.buf.length === MSG_TYPE_OFFSET &&
            packet.buf[MSG_TYPE_OFFSET - 1] ===
            constants.TNS_MSG_TYPE_END_OF_REQUEST) {
          break;
        }
        packet = await this.nsi.recvPacket();
        this.savedPackets.push(packet);
      }
    }
    this.startPacket(this.savedPackets[this.savedPacketPos++]);
  }

  /**
   * Reads OSON (QLocator followed by data) and decodes it into a JavaScript
   * object.
   */
  readOson() {
    const numBytes = this.readUB4();
    if (numBytes === 0) {
      return null;
    }
    this.skipUB8();                     // size (unused)
    this.skipUB4();                     // chunk size (unused)
    const decoder = new oson.OsonDecoder(this.readBytesWithLength());
    this.skipBytesChunked();            // locator (unused)
    return decoder.decode();
  }

  readURowID() {
    let outputOffset = 0, inputOffset = 1;
    let buf = this.readBytesWithLength();
    if (buf === null)
      return null;
    buf = this.readBytesWithLength();
    let inputLen = buf.length;

    // Handle physical rowid
    if (buf && buf[0] === 1) {
      const rba = buf.readUInt32BE(1);
      const partitionID = buf.readUInt16BE(5);
      const blockNum = buf.readUInt32BE(7);
      const slotNum = buf.readUInt16BE(11);
      return utils.encodeRowID({rba, partitionID, blockNum, slotNum});
    }

    // handle logical rowid
    let outputLen = Math.floor(inputLen / 3) * 4;
    const remainder = inputLen % 3;
    if (remainder === 1) {
      outputLen += 1;
    } else if (remainder === 2) {
      outputLen += 3;
    }

    const outputValue =  Buffer.allocUnsafe(outputLen);
    inputLen -= 1;
    outputValue[0] = 42;
    outputOffset += 1;
    while (inputLen > 0) {
      // produce first byte of quadruple
      let pos = buf[inputOffset] >> 2;
      outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos];
      outputOffset += 1;

      // produce second byte of quadruple, but if only one byte is left,
      // produce that one byte and exit
      pos = (buf[inputOffset] & 0x3) << 4;
      if (inputLen == 1) {
        outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos];
        break;
      }
      inputOffset += 1;
      pos |= ((buf[inputOffset] & 0xf0) >> 4);
      outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos];
      outputOffset += 1;

      // produce third byte of quadruple, but if only two bytes are left,
      // produce that one byte and exit
      pos = (buf[inputOffset] & 0xf) << 2;
      if (inputLen == 2) {
        outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos];
        break;
      }
      inputOffset += 1;
      pos |= ((buf[inputOffset] & 0xc0) >> 6);
      outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos];
      outputOffset += 1;

      // produce final byte of quadruple
      pos = buf[inputOffset] & 0x3f;
      outputValue[outputOffset] = TNS_BASE64_ALPHABET_ARRAY[pos];
      outputOffset += 1;
      inputOffset += 1;
      inputLen -= 3;
    }
    return outputValue.toString('utf-8');
  }

  readRowID() {
    const rba = this.readUB4();
    const partitionID = this.readUB2();
    this.skipUB1();
    const blockNum = this.readUB4();
    const slotNum = this.readUB2();
    return {rba, partitionID, blockNum, slotNum};
  }

  /**
   * Reads VECTOR data (QLocator followed by data) and decodes it into a
   * JavaScript object.
   */
  readVector() {
    const numBytes = this.readUB4();
    if (numBytes === 0) {
      return null;
    }
    this.skipUB8();                     // size (unused)
    this.skipUB4();                     // chunk size (unused)
    const decoder = new vector.VectorDecoder(this.readBytesWithLength());
    this.skipBytesChunked();            // locator (unused)
    return decoder.decode();
  }

  skipBytesChunked() {
    const numBytes = this.readUInt8();
    if (numBytes === 0 || numBytes === constants.TNS_NULL_LENGTH_INDICATOR) {
      return;
    }
    if (numBytes !== constants.TNS_LONG_LENGTH_INDICATOR) {
      this.skipBytes(numBytes);
    } else {
      while (true) { // eslint-disable-line
        const tempNumBytes = this.readUB4();
        if (tempNumBytes === 0)
          break;
        this.skipBytes(tempNumBytes);
      }
    }
  }

  readNullTerminatedBytes(maxSize = 50) {
    let offset = 0;
    const tmp = Buffer.allocUnsafe(maxSize);
    while (offset < maxSize) {
      tmp[offset] = this.readUInt8();
      if (tmp[offset] === 0) {
        break;
      }
      offset = offset + 1;
    }
    if (offset === maxSize) {
      const reason = `Byte Arrray Exceeded MaxSize ${maxSize}`;
      errors.throwErr(errors.ERR_INTERNAL, reason);
    }
    return tmp.subarray(0, offset + 1);
  }

}


/**
 * Encapsulates the Network Write Buffer
 *
 * @class WritePacket
 */
class WritePacket extends BaseBuffer {

  constructor(nsi, caps, protocol) {
    super(nsi.sAtts.sdu);
    this.size = this.maxSize;
    this.isLargeSDU = nsi.sAtts.version >= constants.TNS_VERSION_MIN_LARGE_SDU;
    this.protocol = protocol;
    this.packetType = constants.TNS_PACKET_TYPE_DATA;
    this.caps = caps;
    this.nsi = nsi;
  }

  /**
   * Grows the buffer by sending the existing buffer on the transport. A copy
   * is made so that the existing buffer can be used for the next batch of data
   * that needs to be sent
   */
  _grow() {
    this._sendPacket();
  }

  /**
   * Sends the data in the buffer on the transport. First, the packet header is
   * set up by writing the size and packet type.
   */
  _sendPacket(finalPacket = false) {
    const size = this.pos;
    this.pos = 0;
    if (this.isLargeSDU) {
      this.writeUInt32BE(size);
    } else {
      this.writeUInt16BE(size);
      this.writeUInt16BE(0);
    }
    this.writeUInt8(this.packetType);
    this.writeUInt8(0);
    this.writeUInt16BE(0);
    let buf = this.buf.subarray(0, size);
    if (!finalPacket) {
      buf = Buffer.from(buf);
      this.startPacket();
    } else {
      // Write End of RPC bit in last packet used only for fastAuth Message.
      this.buf.writeUInt16BE(FAST_AUTH_END_OF_RPC_VALUE,
        FAST_AUTH_END_OF_RPC_OFFSET);
    }
    if (!this.nsi.ntAdapter) {
      errors.throwErr(errors.ERR_INVALID_CONNECTION);
    }
    this.nsi.ntAdapter.send(buf);
  }

  /**
   * Starts a packet.
   */
  startPacket(dataFlags = 0) {
    this.pos = constants.PACKET_HEADER_SIZE;
    if (this.packetType === constants.TNS_PACKET_TYPE_DATA) {
      this.writeUInt16BE(dataFlags);
    }
  }

  /**
   * Starts a database request.
   */
  startRequest(packetType, dataFlags = 0) {
    this.packetType = packetType;
    this.startPacket(dataFlags);
  }

  /**
   * Ends a database request.
   */
  endRequest() {
    if (this.pos > constants.PACKET_HEADER_SIZE) {
      this._sendPacket(true);
    }
  }

  writeKeyValue(key, value, flags = 0) {
    const keyBytesLen = Buffer.byteLength(key);
    const valBytesLen = Buffer.byteLength(value);
    this.writeUB4(keyBytesLen);
    this.writeBytesWithLength(Buffer.from(key));
    this.writeUB4(valBytesLen);
    if (valBytesLen > 0) {
      this.writeBytesWithLength(Buffer.from(value));
    }
    this.writeUB4(flags);
  }

  /**
   * Encodes a JavaScript object into OSON and then writes it (QLocator
   * followed by data) to the buffer.
   */
  writeOson(value, osonMaxFieldSize) {
    const encoder = new oson.OsonEncoder();
    const buf = encoder.encode(value, osonMaxFieldSize);
    this.writeQLocator(buf.length);
    this.writeBytesWithLength(buf);
  }

  writeSeqNum() {
    this.writeUInt8(this.protocol.sequenceId);
    this.protocol.sequenceId = (this.protocol.sequenceId + 1) % 256;
  }

  /**
   * Encodes a JavaScript object into VECTOR data and then writes it (QLocator
   * followed by data) to the buffer.
   */
  writeVector(value) {
    const encoder = new vector.VectorEncoder();
    const buf = encoder.encode(value);
    this.writeQLocator(buf.length);
    this.writeBytesWithLength(buf);
  }

  //---------------------------------------------------------------------------
  // writeQLocator()
  //
  // Writes a QLocator. QLocators are always 40 bytes in length.
  //---------------------------------------------------------------------------
  writeQLocator(numBytes) {
    this.writeUB4(40);                  // QLocator length
    this.writeUInt8(40);                // repeated length
    this.writeUInt16BE(38);             // internal length
    this.writeUInt16BE(constants.TNS_LOB_QLOCATOR_VERSION);
    this.writeUInt8(constants.TNS_LOB_LOC_FLAGS_VALUE_BASED |
      constants.TNS_LOB_LOC_FLAGS_BLOB | constants.TNS_LOB_LOC_FLAGS_ABSTRACT);
    this.writeUInt8(constants.TNS_LOB_LOC_FLAGS_INIT);
    this.writeUInt16BE(0);              // additional flags
    this.writeUInt16BE(1);              // byt1
    this.writeUInt64BE(numBytes);
    this.writeUInt16BE(0);              // unused
    this.writeUInt16BE(0);              // csid
    this.writeUInt16BE(0);              // unused
    this.writeUInt64BE(0);              // unused
    this.writeUInt64BE(0);              // unused
  }

}

module.exports = {
  ReadPacket,
  WritePacket
};
