// Copyright (c) 2022, 2023, 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 constants = require('../constants.js');
const errors = require('../errors.js');
const nodbUtil = require('../util.js');
const settings = require('../settings.js');
const future = require('../future.js');
const types = require('../types.js');
const Lob = require('../lob.js');
const oson = require('./datahandlers/oson.js');

// define implementation class
class ResultSetImpl {

  //---------------------------------------------------------------------------
  // _determineFetchType()
  //
  // Determine the fetch type to use for the specified metadata.
  //---------------------------------------------------------------------------
  _determineFetchType(metadata, options) {

    // clear any previous fetch type and converter functions that may have been
    // retained
    delete metadata.fetchType;
    delete metadata.converter;

    // if a fetch type handler is specified, call it; if no value (undefined)
    // is returned, the normal processing takes place
    if (options.fetchTypeHandler) {
      const result = options.fetchTypeHandler(metadata);
      if (result !== undefined) {
        errors.assert(typeof result === 'object',
          errors.ERR_FETCH_TYPE_HANDLER_RETURN_VALUE);
        if (result.type !== undefined) {
          errors.assert(result.type instanceof types.DbType,
            errors.ERR_FETCH_TYPE_HANDLER_TYPE);
        }
        if (result.converter !== undefined) {
          errors.assert(typeof result.converter === 'function',
            errors.ERR_FETCH_TYPE_HANDLER_CONVERTER);
        }
        metadata.fetchType = result.type;
        metadata.converter = result.converter;
      }
    }

    // continue processing if no fetch type was specified or no fetch type
    // handler was supplied
    if (!metadata.fetchType) {

      // if the fetchTypeMap exists (calculated from the fetchInfo argument),
      // use it to determine the fetch type
      if (options.fetchTypeMap && options.fetchTypeMap.has(metadata.name)) {
        metadata.fetchType = options.fetchTypeMap.get(metadata.name);
        if (metadata.fetchType === constants.DEFAULT) {
          metadata.fetchType =
            types.DB_TYPE_FETCH_TYPE_MAP.get(metadata.dbType);
        }

      // otherwise, use the default fetch type map (calculated from the
      // fetchAsString and fetchAsBuffer settings
      } else {
        metadata.fetchType = settings.fetchTypeMap.get(metadata.dbType);
      }

    }

    // if the types do not match, verify that the conversion is supported and
    // adjust the final fetch type to match what the database expects
    if (metadata.fetchType !== metadata.dbType) {
      const map = types.DB_TYPE_CONVERSION_MAP.get(metadata.dbType);
      const actualFetchType = map && map.get(metadata.fetchType);
      if (!actualFetchType) {
        errors.throwErr(errors.ERR_UNSUPPORTED_CONVERSION,
          metadata.dbType.name, metadata.fetchType.name);
      }
      metadata.fetchType = actualFetchType;
    }

    let converter;
    const userConverter = metadata.converter;

    // If IsJson or IsOson is set, convert to JSON objects unless
    // user defined output type handler overwrites it.
    if (metadata.dbType !== types.DB_TYPE_JSON && future.oldJsonColumnAsObj
      && userConverter === undefined) {
      let outConverter;
      if (metadata.isOson) {
        outConverter = async function(val) {
          if (!val) {
            return val;
          }
          let result = val;
          if (val instanceof Lob) {
            result = await val.getData();
          }
          const decoder = new oson.OsonDecoder(result);
          return decoder.decode();
        };
        converter = outConverter;
      } else if (metadata.isJson) {
        outConverter = async function(val) {
          if (!val) {
            return val;
          }

          let result = val;
          if (val instanceof Lob) {
            result = await val.getData();
          }
          if (result instanceof Buffer) {
            result = result.toString();
          }
          result = JSON.parse(result);
          return result;
        };
        converter = outConverter;
      }
    }

    // in thin mode, Oracle NUMBER values are internally fetched as string in
    // order to preserve precision so must be converted to JavaScript Number
    // when needed; other numeric and date types are fetched natively as
    // JavaScript Number and Date values and are converted to string using
    // toString() when desired
    if (settings.thin) {
      if (metadata.dbType === types.DB_TYPE_NUMBER &&
        metadata.fetchType === types.DB_TYPE_NUMBER) {
        converter = (v) => (v === null) ? null : parseFloat(v);
      } else if (metadata.fetchType === types.DB_TYPE_VARCHAR) {
        if (metadata.dbType === types.DB_TYPE_BINARY_DOUBLE ||
          metadata.dbType === types.DB_TYPE_BINARY_FLOAT ||
          metadata.dbType === types.DB_TYPE_DATE ||
          metadata.dbType === types.DB_TYPE_TIMESTAMP ||
          metadata.dbType === types.DB_TYPE_TIMESTAMP_LTZ ||
          metadata.dbType === types.DB_TYPE_TIMESTAMP_TZ) {
          converter = (v) => (v === null) ? null : v.toString();
        } else if (metadata.dbType === types.DB_TYPE_RAW) {
          converter = (v) => (v === null) ? null : v.toString('hex').toUpperCase();
        }
      } else if (metadata.dbType === types.DB_TYPE_XMLTYPE) {
        const xmlConverter = async function(val) {
          if (!val) {
            return val;
          }
          if (typeof val === 'string') {
            return val;
          }
          return await val.getData();
        };
        converter = xmlConverter;
      }
      if (userConverter && converter) {
        const internalConverter = converter;
        converter = (v) => userConverter(internalConverter(v));
      }
    }

    if (converter) {
      metadata.converter = converter;
    }

  }

  //---------------------------------------------------------------------------
  // _getConnImpl()
  //
  // Common method on all classes that make use of a connection -- used to
  // ensure serialization of all use of the connection.
  //---------------------------------------------------------------------------
  _getConnImpl() {
    return this._parentObj;
  }

  //---------------------------------------------------------------------------
  // _setup()
  //
  // Setup a result set. The metadata is examined to to determine if any
  // columns need to be manipulated before being returned to the caller. If the
  // rows fetched from the result set are expected to be objects, a unique set
  // of attribute names are also determined.
  //---------------------------------------------------------------------------
  _setup(options, metaData) {
    this._parentObj = options.connection._impl;
    this.metaData = metaData;
    this.lobIndices = [];
    this.dbObjectIndices = [];
    this.nestedCursorIndices = [];
    this.converterIndices = [];
    this.outFormat = options.outFormat;
    this.fetchArraySize = options.fetchArraySize;
    this.dbObjectAsPojo = options.dbObjectAsPojo;
    this.maxRows = options.maxRows;
    const names = new Map();
    for (let i = 0; i < metaData.length; i++) {
      const name = metaData[i].name;
      if (!names.has(name)) {
        names.set(name, i);
      }
    }
    for (let i = 0; i < metaData.length; i++) {
      const info = metaData[i];
      if (info.dbTypeClass) {
        const cls = options.connection._getDbObjectClass(info.dbTypeClass);
        info.dbTypeClass = cls;
      }
      nodbUtil.addTypeProperties(info, "dbType");
      this._determineFetchType(info, options);
      if (info.fetchType === types.DB_TYPE_CURSOR) {
        this.nestedCursorIndices.push(i);
      } else if (info.fetchType === types.DB_TYPE_CLOB ||
          info.fetchType === types.DB_TYPE_NCLOB ||
          info.fetchType === types.DB_TYPE_BLOB ||
          info.fetchType === types.DB_TYPE_BFILE) {
        this.lobIndices.push(i);
      } else if (info.fetchType === types.DB_TYPE_OBJECT) {
        this.dbObjectIndices.push(i);
      }
      if (info.converter) {
        this.converterIndices.push(i);
      }
      let name = info.name;
      if (names.get(name) !== i) {
        let seqNum = 0;
        while (names.has(name)) {
          seqNum = seqNum + 1;
          name = `${info.name}_${seqNum}`;
        }
        names.set(name, i);
        info.name = name;
      }
    }
  }

  //---------------------------------------------------------------------------
  // close()
  //
  // Closes the result set.
  //---------------------------------------------------------------------------
  close() {
    errors.throwNotImplemented("closing a result set");
  }

  //---------------------------------------------------------------------------
  // getRows()
  //
  // Returns rows from a result set.
  //---------------------------------------------------------------------------
  getRows() {
    errors.throwNotImplemented("getting rows");
  }

  //---------------------------------------------------------------------------
  // _getAllRows() [INTERNAL]
  //
  // Fetches all the rows from the database to use internally.
  //---------------------------------------------------------------------------
  async _getAllRows() {
    const fetchArraySize = 100;
    // fetch all rows
    let rowsFetched = [];
    while (true) {    // eslint-disable-line
      // constant default value for fetchArraySize
      const rows = await this.getRows(fetchArraySize, {});
      if (rows) {
        await this._processRows(rows, false);
        rowsFetched = rowsFetched.concat(rows);
      }
      if (rows.length < fetchArraySize) {
        break;
      }
    }

    return rowsFetched;
  }

}

module.exports = ResultSetImpl;
