// Copyright (c) 2019, 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 Lob = require('./lob.js');
const impl = require('./impl');
const errors = require('./errors.js');
const types = require('./types.js');
const util = require('util');

//---------------------------------------------------------------------------
// validatePropertyValue
//
// Validate the value based on metadata.
// For object type, metaData corresponds to the attribute which is set.
// For collection type, metaData corresponds to element in the collection.
//---------------------------------------------------------------------------
function validatePropertyValue(objType, metaData, value, index) {
  let valueLen, lengthErr = false;

  if (value) {
    switch (metaData.type) {
      case types.DB_TYPE_VARCHAR:
      case types.DB_TYPE_NVARCHAR:
      case types.DB_TYPE_NCHAR:
      case types.DB_TYPE_CHAR:
        valueLen = Buffer.byteLength(value);
        if (valueLen > metaData.maxSize) {
          lengthErr = true;
        }
        break;
      case types.DB_TYPE_RAW:
        valueLen = value.length;
        if (valueLen > metaData.maxSize) {
          lengthErr = true;
        }
        break;
      default:
        break;
    }
    if (lengthErr) {
      if (index !== undefined) {
        errors.throwErr(errors.ERR_WRONG_LENGTH_FOR_DBOBJECT_ELEM,
          index, objType.fqn, valueLen, metaData.maxSize);
      } else {
        errors.throwErr(errors.ERR_WRONG_LENGTH_FOR_DBOBJECT_ATTR,
          metaData.name, objType.fqn, valueLen, metaData.maxSize);
      }
    }
  }
}

// define base database object class; instances of this class are never
// instantiated; instead, classes subclassed from this one will be
// instantiated; a cache of these classes are maintained on each connection
class BaseDbObject {

  //---------------------------------------------------------------------------
  // _getAttrValue()
  //
  // Returns the value of the given attribute on the object.
  //---------------------------------------------------------------------------
  _getAttrValue(attr) {
    const value = this._impl.getAttrValue(attr);
    return this._transformValueOut(value, attr.typeClass);
  }

  //---------------------------------------------------------------------------
  // _setAttrValue()
  //
  // Sets the value of the attribute on the object to the given value.
  //---------------------------------------------------------------------------
  _setAttrValue(attr, value) {
    const info = {
      fqn: this._objType.fqn,
      attrName: attr.name,
      type: attr.type,
      typeClass: attr.typeClass
    };
    const options = {allowArray: false};
    value = transformer.transformValueIn(info, value, options);
    validatePropertyValue(this._objType, attr, value);
    this._impl.setAttrValue(attr, value);
  }

  //---------------------------------------------------------------------------
  // _toPojo()
  //
  // Returns the database object as a plain Javascript object.
  //---------------------------------------------------------------------------
  _toPojo() {
    if (this.isCollection) {
      const result = this.getValues();
      if (this.elementType === types.DB_TYPE_OBJECT) {
        for (let i = 0; i < result.length; i++) {
          result[i] = result[i]._toPojo();
        }
      }
      return (result);
    }
    const result = {};
    for (const name in this.attributes) {
      let value = this[name];
      if (value instanceof BaseDbObject) {
        value = value._toPojo();
      }
      result[name] = value;
    }
    return (result);
  }

  //---------------------------------------------------------------------------
  // _transformValueOut()
  //
  // Transforms a value going out to the caller from the implementation.
  //---------------------------------------------------------------------------
  _transformValueOut(value, cls) {
    let outValue = value;
    if (value instanceof impl.LobImpl) {
      outValue = new Lob();
      outValue._setup(value, true);
    } else if (value instanceof impl.DbObjectImpl) {
      outValue = Object.create(cls.prototype);
      outValue._impl = value;
      if (outValue.isCollection) {
        outValue = new Proxy(outValue, BaseDbObject._collectionProxyHandler);
      }
    }
    return outValue;
  }

  //---------------------------------------------------------------------------
  // append()
  //
  // Appends an element to the collection.
  //---------------------------------------------------------------------------
  append(value) {
    errors.assertArgCount(arguments, 1, 1);
    const info = {
      fqn: this._objType.fqn,
      type: this._objType.elementType,
      typeClass: this._objType.elementTypeClass
    };
    const options = {allowArray: false};
    value = transformer.transformValueIn(info, value, options);
    let index = this._impl.getLastIndex();
    if (index) {
      index = index + 1; // element will be appended at index + 1.
    } else {
      index = 0; // undefined for initial append, so set it to 0
    }
    validatePropertyValue(this._objType, this._objType.elementTypeInfo, value, index);
    this._impl.append(value);
  }

  //---------------------------------------------------------------------------
  // attributes
  //
  // Property for the attributes stored on the object type.
  //---------------------------------------------------------------------------
  get attributes() {
    if (!this._attributes) {
      const implAttrs = this._objType.attributes || [];
      const attrs = {};
      for (let i = 0; i < implAttrs.length; i++) {
        const implAttr = implAttrs[i];
        const attr = {
          type: implAttr.type,
          typeName: implAttr.typeName
        };
        if (implAttr.typeClass) {
          attr.typeClass = implAttr.typeClass;
        }
        attrs[implAttr.name] = attr;
      }
      this._attributes = attrs;
    }
    return this._attributes;
  }

  //---------------------------------------------------------------------------
  // deleteElement()
  //
  // Deletes the element in a collection at the specified index.
  //---------------------------------------------------------------------------
  deleteElement(index) {
    errors.assertArgCount(arguments, 1, 1);
    errors.assertParamValue(Number.isInteger(index), 1);
    return this._impl.deleteElement(index);
  }

  //---------------------------------------------------------------------------
  // elementType
  //
  // Property for the element type, if the database object type is a
  // collection. It will be one of the DB_TYPE_ constants.
  //---------------------------------------------------------------------------
  get elementType() {
    return this._objType.elementType;
  }

  //---------------------------------------------------------------------------
  // elementTypeClass
  //
  // Property for the element type class, if the database object type is a
  // collection and the elements in the collection refer to database objects.
  //---------------------------------------------------------------------------
  get elementTypeClass() {
    return this._objType.elementTypeClass;
  }

  //---------------------------------------------------------------------------
  // elementTypeName
  //
  // Property for the element type name, if the database object type is a
  // collection.
  //---------------------------------------------------------------------------
  get elementTypeName() {
    return this._objType.elementTypeName;
  }

  //---------------------------------------------------------------------------
  // fqn
  //
  // Property for the fully qualified name of the database object type in the
  // form: <schema>.<name>.
  //---------------------------------------------------------------------------
  get fqn() {
    return this._objType.fqn;
  }

  //---------------------------------------------------------------------------
  // getElement()
  //
  // Returns the element in a collection at the specified index.
  //---------------------------------------------------------------------------
  getElement(index) {
    errors.assertArgCount(arguments, 1, 1);
    errors.assertParamValue(Number.isInteger(index), 1);
    const value = this._impl.getElement(index);
    return this._transformValueOut(value, this.elementTypeClass);
  }

  //---------------------------------------------------------------------------
  // getKeys()
  //
  // Returns an array of the keys of the collection.
  //---------------------------------------------------------------------------
  getKeys() {
    errors.assertArgCount(arguments, 0, 0);
    return this._impl.getKeys();
  }

  //---------------------------------------------------------------------------
  // getFirstIndex()
  //
  // Returns the first index in the collection.
  //---------------------------------------------------------------------------
  getFirstIndex() {
    errors.assertArgCount(arguments, 0, 0);
    return this._impl.getFirstIndex();
  }

  //---------------------------------------------------------------------------
  // getLastIndex()
  //
  // Returns the last index in the collection.
  //---------------------------------------------------------------------------
  getLastIndex() {
    errors.assertArgCount(arguments, 0, 0);
    return this._impl.getLastIndex();
  }

  //---------------------------------------------------------------------------
  // getNextIndex()
  //
  // Returns the next index in the collection.
  //---------------------------------------------------------------------------
  getNextIndex(index) {
    errors.assertArgCount(arguments, 1, 1);
    errors.assertParamValue(Number.isInteger(index), 1);
    return this._impl.getNextIndex(index);
  }

  //---------------------------------------------------------------------------
  // getPrevIndex()
  //
  // Returns the previous index in the collection.
  //---------------------------------------------------------------------------
  getPrevIndex(index) {
    errors.assertArgCount(arguments, 1, 1);
    errors.assertParamValue(Number.isInteger(index), 1);
    return this._impl.getPrevIndex(index);
  }

  //---------------------------------------------------------------------------
  // getValues()
  //
  // Returns the elements in a collection.
  //---------------------------------------------------------------------------
  getValues() {
    errors.assertArgCount(arguments, 0, 0);
    const values = this._impl.getValues();
    for (let i = 0; i < values.length; i++) {
      values[i] = this._transformValueOut(values[i], this.elementTypeClass);
    }
    return values;
  }

  //---------------------------------------------------------------------------
  // hasElement()
  //
  // Returns a boolean indicating if an element exists at the specified index.
  //---------------------------------------------------------------------------
  hasElement(index) {
    errors.assertArgCount(arguments, 1, 1);
    errors.assertParamValue(Number.isInteger(index), 1);
    return this._impl.hasElement(index);
  }

  //---------------------------------------------------------------------------
  // isCollection
  //
  // Property indicating if the object is a collection or not.
  //---------------------------------------------------------------------------
  get isCollection() {
    return this._objType.isCollection;
  }

  //---------------------------------------------------------------------------
  // name
  //
  // Property for the name of the database object type.
  //---------------------------------------------------------------------------
  get name() {
    return this._objType.name;
  }

  //---------------------------------------------------------------------------
  // schema
  //
  // Property for the schema of the database object type.
  //---------------------------------------------------------------------------
  get schema() {
    return this._objType.schema;
  }

  //---------------------------------------------------------------------------
  // packageName
  //
  // Property for the packageName of the database object type.
  //---------------------------------------------------------------------------
  get packageName() {
    return this._objType.packageName;
  }

  //---------------------------------------------------------------------------
  // setElement()
  //
  // Sets the element in the collection at the specified index to the given
  // value.
  //---------------------------------------------------------------------------
  setElement(index, value) {
    errors.assertArgCount(arguments, 2, 2);
    errors.assertParamValue(Number.isInteger(index), 1);
    const info = {
      fqn: this._objType.fqn,
      type: this._objType.elementType,
      typeClass: this._objType.elementTypeClass
    };
    const options = {allowArray: false};
    value = transformer.transformValueIn(info, value, options);
    validatePropertyValue(this._objType, this._objType.elementTypeInfo, value, index);
    this._impl.setElement(index, value);
  }

  //---------------------------------------------------------------------------
  // trim()
  //
  // Trims the specified number of elements from the end of the collection.
  //---------------------------------------------------------------------------
  trim(numToTrim) {
    errors.assertArgCount(arguments, 1, 1);
    errors.assertParamValue(Number.isInteger(numToTrim) && numToTrim >= 0, 1);
    this._impl.trim(numToTrim);
  }

  // custom inspection routine
  [util.inspect.custom](depth, options) {
    return ('[' + this.fqn + '] ' + util.inspect(this._toPojo(), options));
  }

  [Symbol.iterator]() {
    if (this.isCollection) {
      const values = this.getValues();
      return (values[Symbol.iterator]());
    }
    throw TypeError("obj is not iterable");
  }

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return (NaN);
      default:
        return ('[' + this.fqn + '] ' + util.inspect(this._toPojo(), {}));
    }
  }

  get [Symbol.toStringTag]() {
    return (this.fqn);
  }

  toJSON() {
    return (this._toPojo());
  }

  //---------------------------------------------------------------------------
  // toMap()
  //
  // Returns the Map object where the collection’s indexes are the keys and
  // the elements are its values.
  //---------------------------------------------------------------------------
  toMap() {
    errors.assertArgCount(arguments, 0, 0);
    if (!this.isCollection) {
      errors.throwErr(errors.ERR_OBJECT_IS_NOT_A_COLLECTION,
        this.name);
    }
    const result = new Map();
    this.getKeys().forEach(element => {
      result.set(element, this.getElement(element));
    });
    return result;
  }

}

// method for transforming the error
function transformErr(func) {
  return function() {
    try {
      return func.apply(this, arguments);
    } catch (err) {
      throw errors.transformErr(err, errors.transformErr);
    }
  };
}

// method for wrapping the functions so that any errors thrown are transformed
function wrapFns(proto) {
  for (let i = 1; i < arguments.length; i++) {
    const name = arguments[i];
    proto[name] = transformErr(proto[name]);
  }
}

wrapFns(BaseDbObject.prototype,
  "_getAttrValue",
  "_setAttrValue",
  "append",
  "deleteElement",
  "getElement",
  "getKeys",
  "getFirstIndex",
  "getLastIndex",
  "getNextIndex",
  "getPrevIndex",
  "getValues",
  "toMap",
  "hasElement",
  "setElement",
  "trim"
);

// define proxy handler used for collections
BaseDbObject._collectionProxyHandler = {

  deleteProperty(target, prop) {
    if (typeof prop === 'string') {
      const index = +prop;
      if (!isNaN(index)) {
        return (target.deleteElement(index));
      }
    }
    return (delete target[prop]);
  },

  get(target, prop) {
    if (typeof prop === 'string') {
      const index = +prop;
      if (!isNaN(index)) {
        return (target.getElement(index));
      }
    }
    const value = target[prop];
    if (typeof value === 'function') {
      return (value.bind(target));
    }
    return (value);
  },

  set(target, prop, value) {
    if (typeof prop === 'string') {
      const index = +prop;
      if (!isNaN(index)) {
        target.setElement(index, value);
        return (true);
      }
    }
    target[prop] = value;
    return (true);
  }

};

module.exports = BaseDbObject;

// load this after the module exports are set so that it is available
const transformer = require('./transformer.js');
