// 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 { Buffer } = require('buffer');
const constants = require('./protocol/constants.js');
const errors = require('../errors.js');
const types = require('../types.js');
const DbObjectImpl = require('../impl/dbObject.js');
const { GrowableBuffer } = require('../impl/datahandlers/buffer.js');
const ThinLobImpl = require('./lob.js');

class DbObjectPickleBuffer extends GrowableBuffer {

  //---------------------------------------------------------------------------
  // _readBytesWithLength()
  //
  // Helper function that processes the number of bytes (if needed) and then
  // acquires the specified number of bytes from the buffer.
  //---------------------------------------------------------------------------
  _readBytesWithLength(numBytes) {
    if (numBytes === constants.TNS_LONG_LENGTH_INDICATOR) {
      numBytes = this.readUInt32BE();
    }
    return this.readBytes(numBytes);
  }

  //---------------------------------------------------------------------------
  // getIsAtomicNull()
  //
  // Reads the next byte and checks to see if the value is atomically null. If
  // not, the byte is returned to the buffer for further processing.
  //---------------------------------------------------------------------------
  getIsAtomicNull() {
    const value = this.readUInt8();
    if (value === constants.TNS_OBJ_ATOMIC_NULL ||
        value === constants.TNS_NULL_LENGTH_INDICATOR) {
      return true;
    }
    this.pos -= 1;
    return false;
  }

  //---------------------------------------------------------------------------
  // readHeader()
  //
  // Reads the header of the pickled data.
  //---------------------------------------------------------------------------
  readHeader(obj) {
    obj.imageFlags = this.readUInt8();
    obj.imageVersion = this.readUInt8();
    this.readLength();
    if ((obj.imageFlags & constants.TNS_OBJ_NO_PREFIX_SEG) === 0) {
      const prefixSegLength = this.readLength();
      this.skipBytes(prefixSegLength);
    }
  }

  //---------------------------------------------------------------------------
  // readLength()
  //
  // Read the length from the buffer. This will be a single byte, unless the
  // value meets or exeeds TNS_LONG_LENGTH_INDICATOR. In that case, the value
  // is stored as a 4-byte integer.
  //---------------------------------------------------------------------------
  readLength() {
    const shortLength = this.readUInt8();
    if (shortLength !== constants.TNS_LONG_LENGTH_INDICATOR) {
      return shortLength;
    }
    return this.readUInt32BE();
  }

  //---------------------------------------------------------------------------
  // writeHeader()
  //
  // Writes the header of the pickled data. Since the size is unknown at this
  // point, zero is written initially and the actual size is written later.
  //---------------------------------------------------------------------------
  writeHeader(obj) {
    this.writeUInt8(obj.imageFlags);
    this.writeUInt8(obj.imageVersion);
    this.writeUInt8(constants.TNS_LONG_LENGTH_INDICATOR);
    this.writeUInt32BE(0);
    if (obj._objType.isCollection) {
      this.writeUInt8(1);               // length of prefix segment
      this.writeUInt8(1);               // prefix segment contents
    }
  }

  //---------------------------------------------------------------------------
  // writeLength()
  //
  // Writes the length to the buffer.
  //---------------------------------------------------------------------------
  writeLength(length) {
    if (length <= constants.TNS_OBJ_MAX_SHORT_LENGTH) {
      this.writeUInt8(length);
    } else {
      this.writeUInt8(constants.TNS_LONG_LENGTH_INDICATOR);
      this.writeUInt32BE(length);
    }
  }

  //---------------------------------------------------------------------------
  // _writeRawBytesAndLength()
  //
  // Writes the length in the format required before
  // writing the bytes.
  //---------------------------------------------------------------------------
  _writeRawBytesAndLength(value, numBytes) {
    this.writeLength(numBytes);
    this.writeBytes(value);
  }
}

class ThinDbObjectImpl extends DbObjectImpl {

  constructor(objType, packedData) {
    if (typeof objType === 'function') {
      objType = objType.prototype._objType;
    }
    super(objType);
    this.packedData = packedData;
    this.unpackedAttrs = new Map();
    if (packedData) {
      this.unpackedAssocArray = new Map();
      this.unpackedAssocKeys = undefined;
    } else if (objType) {
      const prefix = Buffer.from([0, 0x22, constants.TNS_OBJ_NON_NULL_OID,
        constants.TNS_OBJ_HAS_EXTENT_OID]);
      this.toid = Buffer.concat([prefix, objType.oid, constants.TNS_EXTENT_OID]);
      this.flags = constants.TNS_OBJ_TOP_LEVEL;
      this.imageFlags = constants.TNS_OBJ_IS_VERSION_81;
      this.imageVersion = constants.TNS_OBJ_IMAGE_VERSION;
      if (objType.isCollection) {
        this.imageFlags |= constants.TNS_OBJ_IS_COLLECTION;
        if (objType.collectionType === constants.TNS_OBJ_PLSQL_INDEX_TABLE) {
          this.unpackedAssocArray = new Map();
        } else {
          this.unpackedArray = [];
        }
      } else {
        this.imageFlags |= constants.TNS_OBJ_NO_PREFIX_SEG;
      }
    }
  }

  //---------------------------------------------------------------------------
  // _ensureAssocKeys()
  //
  // Ensure that the keys for the associative array have been calculated.
  // PL/SQL associative arrays keep their keys in sorted order so this must be
  // calculated when indices are required.
  //---------------------------------------------------------------------------
  _ensureAssocKeys() {
    if (!this.unpackedAssocKeys) {
      // Associative arrays indexed by integer are only supported now
      this.unpackedAssocKeys = [...this.unpackedAssocArray.keys()].sort((x, y) => x - y);
    }
  }

  //---------------------------------------------------------------------------
  // _ensureUnpacked()
  //
  // Ensure that the data has been unpacked.
  //---------------------------------------------------------------------------
  _ensureUnpacked() {
    if (this.packedData) {
      this._unpackData();
    }
  }

  //---------------------------------------------------------------------------
  // _getPackedData()
  //
  // Returns the packed data for the object. This will either be the value
  // retrieved from the database or generated packed data (for new objects and
  // those that have had their data unpacked already).
  //---------------------------------------------------------------------------
  _getPackedData() {
    if (this.packedData)
      return this.packedData;
    const buf = new DbObjectPickleBuffer();
    buf.writeHeader(this);
    this._packData(buf);
    const size = buf.pos;
    buf.pos = 3;
    buf.writeUInt32BE(size);
    return buf.buf.subarray(0, size);
  }

  //---------------------------------------------------------------------------
  // _packData()
  //
  // Packs the data from the object into the buffer.
  //---------------------------------------------------------------------------
  _packData(buf) {
    const objType = this._objType;
    if (objType.isCollection) {
      buf.writeUInt8(objType.collectionFlags);
      if (objType.collectionType === constants.TNS_OBJ_PLSQL_INDEX_TABLE) {
        this._ensureAssocKeys();
        buf.writeLength(this.unpackedAssocKeys.length);
        for (const index of this.unpackedAssocKeys) {
          buf.writeInt32BE(index);
          this._packValue(buf, objType.elementType, objType.elementTypeClass,
            this.unpackedAssocArray.get(index));
        }
      } else {
        buf.writeLength(this.unpackedArray.length);
        for (const value of this.unpackedArray) {
          this._packValue(buf, objType.elementType, objType.elementTypeClass,
            value);
        }
      }
    } else {
      for (const attr of objType.attributes) {
        this._packValue(buf, attr.type, attr.typeClass,
          this.unpackedAttrs.get(attr.name));
      }
    }
  }

  //---------------------------------------------------------------------------
  // _packValue()
  //
  // Packs a value into the buffer. At this point it is assumed that the value
  // matches the correct type.
  //---------------------------------------------------------------------------
  _packValue(buf, type, typeClass, value) {
    if (value === null || value === undefined) {
      if (typeClass && !typeClass.prototype.isCollection) {
        buf.writeUInt8(constants.TNS_OBJ_ATOMIC_NULL);
      } else {
        buf.writeUInt8(constants.TNS_NULL_LENGTH_INDICATOR);
      }
    } else {
      switch (type) {
        case types.DB_TYPE_CHAR:
        case types.DB_TYPE_VARCHAR:
          buf.writeBytesWithLength(Buffer.from(value));
          break;
        case types.DB_TYPE_NCHAR:
        case types.DB_TYPE_NVARCHAR:
          buf.writeBytesWithLength(Buffer.from(value, 'utf16le').swap16());
          break;
        case types.DB_TYPE_NUMBER:
          buf.writeOracleNumber(value.toString());
          break;
        case types.DB_TYPE_BINARY_INTEGER:
        case types.DB_TYPE_BOOLEAN:
          buf.writeUInt8(4);
          buf.writeUInt32BE(value);
          break;
        case types.DB_TYPE_RAW:
          buf.writeBytesWithLength(value);
          break;
        case types.DB_TYPE_BINARY_DOUBLE:
          buf.writeUInt8(8);
          buf.writeBinaryDouble(value);
          break;
        case types.DB_TYPE_BINARY_FLOAT:
          buf.writeUInt8(4);
          buf.writeBinaryFloat(value);
          break;
        case types.DB_TYPE_DATE:
        case types.DB_TYPE_TIMESTAMP:
        case types.DB_TYPE_TIMESTAMP_LTZ:
        case types.DB_TYPE_TIMESTAMP_TZ:
          buf.writeOracleDate(value, type);
          break;
        case types.DB_TYPE_OBJECT:
          if (this._objType.isCollection || value._objType.isCollection) {
            buf.writeBytesWithLength(value._getPackedData());
          } else {
            value._packData(buf);
          }
          break;
        default:
          errors.throwErr(errors.ERR_NOT_IMPLEMENTED, type);
      }
    }
  }

  //---------------------------------------------------------------------------
  // _unpackData()
  //
  // Unpacks the packed data into a map of JavaScript values.
  //---------------------------------------------------------------------------
  _unpackData() {
    const buf = new DbObjectPickleBuffer(this.packedData);
    buf.readHeader(this);
    this._unpackDataFromBuf(buf);
    this.packedData = undefined;
  }

  //---------------------------------------------------------------------------
  // _unpackDataFromBuf()
  //
  // Unpacks the data in the buffer into a map of JavaScript values.
  //---------------------------------------------------------------------------
  _unpackDataFromBuf(buf) {
    let unpackedArray, unpackedAssocArray, assocIndex, unpackedAttrs;
    const objType = this._objType;
    if (objType.isCollection) {
      if (objType.collectionType === constants.TNS_OBJ_PLSQL_INDEX_TABLE) {
        unpackedAssocArray = new Map();
      } else {
        unpackedArray = [];
      }
      this.collectionFlags = buf.readUInt8();
      const numElements = buf.readLength();
      for (let i = 0; i < numElements; i++) {
        if (objType.collectionType === constants.TNS_OBJ_PLSQL_INDEX_TABLE) {
          assocIndex = buf.readUInt32BE();
        }
        const value = this._unpackValue(buf, objType.elementType,
          objType.elementTypeClass);
        if (objType.collectionType === constants.TNS_OBJ_PLSQL_INDEX_TABLE) {
          unpackedAssocArray.set(assocIndex, value);
        } else {
          unpackedArray.push(value);
        }
      }
    } else {
      unpackedAttrs = new Map();
      for (const attr of objType.attributes) {
        const value = this._unpackValue(buf, attr.type, attr.typeClass);
        unpackedAttrs.set(attr.name, value);
      }
    }
    this.unpackedAttrs = unpackedAttrs;
    this.unpackedArray = unpackedArray;
    this.unpackedAssocArray = unpackedAssocArray;
  }

  //---------------------------------------------------------------------------
  // _unpackValue()
  //
  // Unpacks a single value and returns it.
  //---------------------------------------------------------------------------
  _unpackValue(buf, type, typeClass) {
    let isNull, obj, value;
    switch (type) {
      case types.DB_TYPE_NUMBER:
        value = buf.readOracleNumber();
        if (value !== null)
          value = parseFloat(value);
        return value;
      case types.DB_TYPE_BINARY_INTEGER:
        return buf.readBinaryInteger();
      case types.DB_TYPE_VARCHAR:
      case types.DB_TYPE_CHAR:
        return buf.readStr(constants.CSFRM_IMPLICIT);
      case types.DB_TYPE_NVARCHAR:
      case types.DB_TYPE_NCHAR:
        return buf.readStr(constants.CSFRM_NCHAR);
      case types.DB_TYPE_RAW:
        value = buf.readBytesWithLength();
        if (value !== null)
          value = Buffer.from(value);
        return value;
      case types.DB_TYPE_BINARY_DOUBLE:
        return buf.readBinaryDouble();
      case types.DB_TYPE_BINARY_FLOAT:
        return buf.readBinaryFloat();
      case types.DB_TYPE_DATE:
      case types.DB_TYPE_TIMESTAMP:
        return buf.readOracleDate(true);
      case types.DB_TYPE_TIMESTAMP_LTZ:
      case types.DB_TYPE_TIMESTAMP_TZ:
        return buf.readOracleDate(false);
      case types.DB_TYPE_BOOLEAN:
        return buf.readBool();
      case types.DB_TYPE_OBJECT:
      case types.DB_TYPE_XMLTYPE:
        isNull = buf.getIsAtomicNull();
        if (isNull)
          return null;
        obj = new ThinDbObjectImpl(typeClass);
        if (obj._objType.isXmlType) {
          return readXML(obj._objType._connection, buf.readBytesWithLength());
        }
        if (obj._objType.isCollection || this._objType.isCollection) {
          obj.packedData = Buffer.from(buf.readBytesWithLength());
        } else {
          obj._unpackDataFromBuf(buf);
        }
        return obj;
      default:
        errors.throwErr(errors.ERR_NOT_IMPLEMENTED, type);
    }
  }

  //---------------------------------------------------------------------------
  // append()
  //
  // Appends an element to the collection.
  //---------------------------------------------------------------------------
  append(value) {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      const objType = this._objType;
      if (objType.maxNumElements > 0 &&
          this.unpackedArray.length >= objType.maxNumElements) {
        errors.throwErr(errors.ERR_INVALID_COLL_INDEX_SET,
          this.unpackedArray.length, 0, objType.maxNumElements);
      }
      this.unpackedArray.push(value);
    } else {
      this._ensureAssocKeys();
      let newIndex;
      if (this.unpackedAssocKeys.length === 0) {
        newIndex = 0;
      } else {
        const keyIndex = this.unpackedAssocKeys.length - 1;
        newIndex = this.unpackedAssocKeys[keyIndex] + 1;
      }
      this.unpackedAssocArray.set(newIndex, value);
      this.unpackedAssocKeys.push(newIndex);
    }
  }

  //---------------------------------------------------------------------------
  // deleteElement()
  //
  // Deletes an element from a collection.
  //---------------------------------------------------------------------------
  deleteElement(index) {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      if (this._objType.collectionType == constants.TNS_OBJ_VARRAY) {
        errors.throwErr(errors.ERR_DELETE_ELEMENTS_OF_VARRAY);
      }
      this.unpackedArray.splice(index, 1);
    } else {
      this._unpackedAssocKeys = undefined;
      this.unpackedAssocArray.delete(index);
    }
  }

  //---------------------------------------------------------------------------
  // getAttrValue()
  //
  // Returns the value of the given attribute on the object.
  //---------------------------------------------------------------------------
  getAttrValue(attr) {
    this._ensureUnpacked();
    const value = this.unpackedAttrs.get(attr.name);
    if (value === undefined)
      return null;
    return value;
  }

  //---------------------------------------------------------------------------
  // getElement()
  //
  // Returns an element from the collection.
  //---------------------------------------------------------------------------
  getElement(index) {
    let value;
    this._ensureUnpacked();
    if (this.unpackedArray) {
      value = this.unpackedArray[index];
    } else {
      value = this.unpackedAssocArray.get(index);
    }
    if (value === undefined) {
      errors.throwErr(errors.ERR_INVALID_COLL_INDEX_GET, index);
    }
    return value;
  }

  //---------------------------------------------------------------------------
  // getFirstIndex()
  //
  // Returns the first index in a collection.
  //---------------------------------------------------------------------------
  getFirstIndex() {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      return 0;
    } else if (this.unpackedAssocArray) {
      this._ensureAssocKeys();
      return this.unpackedAssocKeys[0];
    }
  }

  //---------------------------------------------------------------------------
  // getKeys()
  //
  // Returns the keys of the collection in a JavaScript array.
  //---------------------------------------------------------------------------
  getKeys() {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      return Array.from(this.unpackedArray.keys());
    } else if (this.unpackedAssocArray) {
      this._ensureAssocKeys();
      return Array.from(this.unpackedAssocKeys);
    }
    return [];
  }

  //---------------------------------------------------------------------------
  // getLastIndex()
  //
  // Returns the last index in a collection.
  //---------------------------------------------------------------------------
  getLastIndex() {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      if (this.unpackedArray.length > 0)
        return this.unpackedArray.length - 1;
    } else if (this.unpackedAssocArray) {
      this._ensureAssocKeys();
      return this.unpackedAssocKeys[this.unpackedAssocKeys.length - 1];
    }
  }

  //---------------------------------------------------------------------------
  // getNextIndex()
  //
  // Returns the next index in a collection.
  // For associative arrays indexed by integers, if the passed-in index
  // parameter is not present, it will return the next higher index found
  // in the associative array.
  //---------------------------------------------------------------------------
  getNextIndex(index) {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      if (index + 1 < this.unpackedArray.length) {
        return index + 1;
      }
    } else if (this.unpackedAssocArray) {
      this._ensureAssocKeys();
      for (const key of this.unpackedAssocKeys) {
        if (key > index)
          return key;
      }
    }
  }

  //---------------------------------------------------------------------------
  // getPrevIndex()
  //
  // Returns the previous index in a collection.
  // For associative arrays indexed by integers, if the passed-in index
  // parameter is not present, it will return the next lower index found
  // in the associative array.
  //---------------------------------------------------------------------------
  getPrevIndex(index) {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      if (index > 0) {
        return index - 1;
      }
    } else if (this.unpackedAssocArray) {
      this._ensureAssocKeys();
      let prev;
      for (const key of this.unpackedAssocKeys) {
        if (key >= index)
          return prev;
        prev = key;
      }
    }
  }

  //---------------------------------------------------------------------------
  // getValues()
  //
  // Returns the values of the collection in a JavaScript array.
  //---------------------------------------------------------------------------
  getValues() {
    const result = [];
    this._ensureUnpacked();
    if (this.unpackedArray) {
      return Array.from(this.unpackedArray);
    } else if (this.unpackedAssocArray) {
      this._ensureAssocKeys();
      for (const key of this.unpackedAssocKeys) {
        result.push(this.unpackedAssocArray.get(key));
      }
    }
    return result;
  }

  //---------------------------------------------------------------------------
  // hasElement()
  //
  // Returns whether an element exists at the given index.
  //---------------------------------------------------------------------------
  hasElement(index) {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      return (index >= 0 && index < this.unpackedArray.length);
    }
    return this.unpackedAssocArray.has(index);
  }

  //---------------------------------------------------------------------------
  // setAttrValue()
  //
  // Sets the value of the attribute on the object to the given value.
  //---------------------------------------------------------------------------
  setAttrValue(attr, value) {
    this._ensureUnpacked();
    this.unpackedAttrs.set(attr.name, value);
  }

  //---------------------------------------------------------------------------
  // setElement()
  //
  // Sets an entry in a collection that is indexed by integers.
  //---------------------------------------------------------------------------
  setElement(index, value) {
    this._ensureUnpacked();
    if (this.unpackedArray) {
      const maxIndex = Math.max(this.unpackedArray.length - 1, 0);
      if (index > maxIndex) {
        errors.throwErr(errors.ERR_INVALID_COLL_INDEX_SET, index, 0, maxIndex);
      }
      this.unpackedArray[index] = value;
    } else {
      if (!this.unpackedAssocArray.has(index))
        this.unpackedAssocKeys = undefined;
      this.unpackedAssocArray.set(index, value);
    }
  }

  //---------------------------------------------------------------------------
  // trim()
  //
  // Trim the specified number of elements from the end of the collection.
  //---------------------------------------------------------------------------
  trim(numToTrim) {
    this._ensureUnpacked();
    if (numToTrim > 0) {
      this.unpackedArray = this.unpackedArray.slice(0,
        this.unpackedArray.length - numToTrim);
    }
  }

}

//---------------------------------------------------------------------------
// readXML()
//
// Decodes the raw Bytes to XML string or LOB object.
//
//---------------------------------------------------------------------------
function readXML(conn, buf) {
  let colValue;

  const xmlObj = new DbObjectPickleBuffer(buf);
  const tempobj = {};
  xmlObj.readHeader(tempobj);
  xmlObj.skipBytes(1);
  const xmlflag = xmlObj.readUInt32BE();
  if (xmlflag & constants.TNS_XML_TYPE_FLAG_SKIP_NEXT_4) {
    xmlObj.skipBytes(4);
  }
  const numBytesLeft = xmlObj.numBytesLeft();
  const ptr = xmlObj.readBytes(numBytesLeft);
  if (xmlflag & constants.TNS_XML_TYPE_STRING) {
    colValue = ptr.toString();
  } else if (xmlflag & constants.TNS_XML_TYPE_LOB) {
    const lobImpl = new ThinLobImpl();
    const locator = Buffer.from(ptr);
    lobImpl.init(conn, locator, types.DB_TYPE_CLOB, 0, 0);
    colValue = lobImpl;
  } else {
    // We only support String and Clob type.
    errors.throwErr(errors.ERR_UNEXPECTED_XML_TYPE, xmlflag);
  }
  return colValue;
}

module.exports = { ThinDbObjectImpl, readXML };
