// 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 BaseDbObject = require('./dbObject.js');
const { Buffer } = require('buffer');
const Lob = require('./lob.js');
const ResultSet = require('./resultset.js');
const constants = require('./constants.js');
const errors = require('./errors.js');
const util = require('util');
const types = require('./types.js');
const nodbUtil = require('./util.js');

//-----------------------------------------------------------------------------
// checkType()
//
// Checks that the type of the data matches one of the given types. If the type
// has not been specified yet, the first type is assumed to be the correct one.
//
// A failure to match results in an exception being thrown. The data in the
// info parameter is used to determine which error should be thrown.
//-----------------------------------------------------------------------------
function checkType(info, options) {
  if (info.type === undefined && arguments.length > 2) {
    info.type = arguments[2];
  } else {
    let matches = false;
    for (let i = 2; i < arguments.length; i++) {
      if (info.type === arguments[i]) {
        matches = true;
        break;
      }
    }
    if (!matches) {
      if (info.attrName) {
        errors.throwErr(errors.ERR_WRONG_VALUE_FOR_DBOBJECT_ATTR,
          info.attrName, info.fqn);
      } else if (info.fqn) {
        errors.throwErr(errors.ERR_WRONG_VALUE_FOR_DBOBJECT_ELEM, info.fqn);
      } else if (info.isArray && info.name) {
        errors.throwErr(errors.ERR_INCOMPATIBLE_TYPE_ARRAY_BIND, options.pos,
          info.name);
      } else if (info.isArray) {
        errors.throwErr(errors.ERR_INCOMPATIBLE_TYPE_ARRAY_INDEX_BIND,
          options.pos, info.pos);
      } else {
        errors.throwErr(errors.ERR_BIND_VALUE_AND_TYPE_MISMATCH);
      }
    }
  }
}

//-----------------------------------------------------------------------------
// transformJsonValue()
//
// Returns a normalized JSON value. Scalar values are returned unchanged.
// Arrays are returned as a new array with transformed JSON values. Objects are
// returned as new objects with keys "fields" and "values", both of which
// are arrays (with the value transformed to JSON values).
//-----------------------------------------------------------------------------
function transformJsonValue(value) {

  // handle simple scalars
  if (value === undefined || value === null ||
    typeof value === 'number' || typeof value === 'string' ||
    typeof value === 'boolean' || Buffer.isBuffer(value) ||
    util.types.isDate(value) || nodbUtil.isVectorValue(value))
    return value;

  // arrays are transformed to a new array with processed values
  if (Array.isArray(value)) {
    const outValue = new Array(value.length);
    for (let i = 0; i < value.length; i++) {
      outValue[i] = transformJsonValue(value[i]);
    }
    return outValue;
  }

  // database objects are treated as empty objects
  if (value instanceof BaseDbObject)
    return {fields: [], values: []};

  // JsonId is a special type to represent autogenerated id
  // for SODA documents.
  if (value instanceof types.JsonId) {
    return value;
  }

  // all other objects are transformed to an object with two arrays (fields
  // and values)
  const outValue = {};
  outValue.fields = Object.getOwnPropertyNames(value);
  outValue.values = new Array(outValue.fields.length);
  for (let i = 0; i < outValue.fields.length; i++) {
    outValue.values[i] = transformJsonValue(value[outValue.fields[i]]);
  }
  return outValue;

}

//-----------------------------------------------------------------------------
// transformValueIn()
//
// Processes the value supplied by the caller and returns a normalized value,
// if necessary, for use by the implementation. All checks are performed on the
// value to ensure it is suitable for the type information supplied. If no type
// information is supplied, however, the value defines it instead!
//-----------------------------------------------------------------------------
function transformValueIn(info, value, options) {

  // null and undefined can always be set so nothing needs to be done
  if (value === undefined || value === null)
    return undefined;

  // handle setting plain JS values to database objects
  if (info.type === types.DB_TYPE_OBJECT) {
    let obj = value;
    if (!(value instanceof BaseDbObject)) {
      obj = new info.typeClass(value);
    }
    return obj._impl;

  // handle setting plain JS values to JSON
  } else if (info.type === types.DB_TYPE_JSON) {
    return transformJsonValue(value);

  // handle strings
  } else if (typeof value === 'string') {
    checkType(info, options,
      types.DB_TYPE_VARCHAR,
      types.DB_TYPE_NVARCHAR,
      types.DB_TYPE_CHAR,
      types.DB_TYPE_NCHAR,
      types.DB_TYPE_CLOB,
      types.DB_TYPE_NCLOB);
    if (info.type !== types.DB_TYPE_CLOB &&
        info.type !== types.DB_TYPE_NCLOB) {
      const valueLen = Buffer.byteLength(value);
      if (info.maxSize === undefined || valueLen > info.maxSize) {
        if (info.checkSize) {
          errors.throwErr(errors.ERR_MAX_SIZE_TOO_SMALL, info.maxSize,
            valueLen, options.pos);
        }
        info.maxSize = valueLen;
      }
    }
    return value;

  // handle numbers
  } else if (typeof value === 'number' || typeof value === 'bigint') {
    checkType(info, options,
      types.DB_TYPE_NUMBER,
      types.DB_TYPE_BINARY_INTEGER,
      types.DB_TYPE_BINARY_FLOAT,
      types.DB_TYPE_BINARY_DOUBLE);
    if (Number.isNaN(value) && info.type === types.DB_TYPE_NUMBER) {
      errors.throwErr(errors.ERR_NAN_VALUE);
    }
    return value;

  // handle booleans
  } else if (typeof value === 'boolean') {
    checkType(info, options, types.DB_TYPE_BOOLEAN);
    return value;

  // handle dates
  } else if (util.types.isDate(value)) {
    checkType(info, options,
      types.DB_TYPE_TIMESTAMP,
      types.DB_TYPE_TIMESTAMP_TZ,
      types.DB_TYPE_TIMESTAMP_LTZ,
      types.DB_TYPE_DATE);
    return value;

  // handle binding buffers
  } else if (Buffer.isBuffer(value)) {
    checkType(info, options,
      types.DB_TYPE_RAW,
      types.DB_TYPE_BLOB);
    if (info.type === types.DB_TYPE_RAW &&
        (info.maxSize === undefined || value.length > info.maxSize)) {
      if (info.checkSize) {
        errors.throwErr(errors.ERR_MAX_SIZE_TOO_SMALL, info.maxSize,
          value.length, options.pos);
      }
      info.maxSize = value.length;
    }
    return value;

  // handle result sets
  } else if (value instanceof ResultSet) {
    checkType(info, options, types.DB_TYPE_CURSOR);
    return value._impl;

  // handle binding LOBs
  } else if (value instanceof Lob) {
    checkType(info, options, value.type);
    return value._impl;

  // handle database objects
  } else if (value instanceof BaseDbObject) {
    checkType(info, options, types.DB_TYPE_OBJECT);
    return value._impl;

  // handle vectors
  } else if (nodbUtil.isVectorValue(value)) {
    checkType(info, options, types.DB_TYPE_VECTOR);
    return value;
  } else if (info.type === types.DB_TYPE_VECTOR && Array.isArray(value)) {
    return new Float64Array(value);

  // handle arrays
  } else if (options.allowArray && Array.isArray(value)) {
    info.isArray = true;
    if (info.dir === constants.BIND_IN) {
      info.maxArraySize = value.length || 1;
    } else if (info.maxArraySize === undefined) {
      errors.throwErr(errors.ERR_REQUIRED_MAX_ARRAY_SIZE);
    } else if (value.length > info.maxArraySize) {
      errors.throwErr(errors.ERR_INVALID_ARRAY_SIZE);
    }
    options.allowArray = false;
    const transformed = new Array(value.length);
    for (let i = 0; i < value.length; i++) {
      options.pos = i;
      transformed[i] = transformValueIn(info, value[i], options);
    }
    return transformed;
  }

  // no suitable bind value found
  if (info.type === undefined)
    errors.throwErr(errors.ERR_INVALID_BIND_DATA_TYPE, 2);
  checkType(info, options);

}

// define exports
module.exports = {
  transformJsonValue,
  transformValueIn

};
