/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *     http://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';

/**
 * Creates a non-clearable timer that resolves the promise once elapses.
 * @param {number} ms
 * @returns {Promise<void>}
 */
function delay(ms) {
  return new Promise(r => setTimeout(r, ms || 0));
}

/**
 * Creates a Promise that gets resolved or rejected based on an event.
 * @param {object} emitter
 * @param {string} eventName
 * @returns {Promise}
 */
function fromEvent(emitter, eventName) {
  return new Promise((resolve, reject) =>
    emitter.once(eventName, (err, result) => {
      if (err) {
        reject(err);
      } else {
        resolve(result);
      }
    }));
}

/**
 * Creates a Promise from a callback based function.
 * @param {Function} fn
 * @returns {Promise}
 */
function fromCallback(fn) {
  return new Promise((resolve, reject) =>
    fn((err, result) => {
      if (err) {
        reject(err);
      } else {
        resolve(result);
      }
    }));
}

/**
 * Gets a function that has the signature of a callback that invokes the appropriate promise handler parameters.
 * @param {Function} resolve
 * @param {Function} reject
 * @returns {Function}
 */
function getCallback(resolve, reject) {
  return function (err, result) {
    if (err) {
      reject(err);
    } else {
      resolve(result);
    }
  };
}

async function invokeSequentially(info, length, fn) {
  let index;
  while ((index = info.counter++) < length) {
    await fn(index);
  }
}

/**
 * Invokes the new query plan of the load balancing policy and returns a Promise.
 * @param {LoadBalancingPolicy} lbp The load balancing policy.
 * @param {String} keyspace Name of currently logged keyspace at <code>Client</code> level.
 * @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
 * @returns {Promise<Iterator>}
 */
function newQueryPlan(lbp, keyspace, executionOptions) {
  return new Promise((resolve, reject) => {
    lbp.newQueryPlan(keyspace, executionOptions, (err, iterator) => {
      if (err) {
        reject(err);
      } else {
        resolve(iterator);
      }
    });
  });
}

/**
 * Method that handles optional callbacks (dual promise and callback support).
 * When callback is undefined it returns the promise.
 * When using a callback, it will use it as handlers of the continuation of the promise.
 * @param {Promise} promise
 * @param {Function?} callback
 * @returns {Promise|undefined}
 */
function optionalCallback(promise, callback) {
  if (!callback) {
    return promise;
  }

  toCallback(promise, callback);
}

/**
 * Invokes the provided function multiple times, considering the concurrency level limit.
 * @param {Number} count
 * @param {Number} limit
 * @param {Function} fn
 * @returns {Promise}
 */
function times(count, limit, fn) {
  if (limit > count) {
    limit = count;
  }

  const promises = new Array(limit);

  const info = {
    counter: 0
  };

  for (let i = 0; i < limit; i++) {
    promises[i] = invokeSequentially(info, count, fn);
  }

  return Promise.all(promises);
}

/**
 * Deals with unexpected rejections in order to avoid the unhandled promise rejection warning or failure.
 * @param {Promise} promise
 * @returns {undefined}
 */
function toBackground(promise) {
  promise.catch(() => {});
}

/**
 * Invokes the callback once outside the promise chain the promise is resolved or rejected.
 * @param {Promise} promise
 * @param {Function?} callback
 * @returns {undefined}
 */
function toCallback(promise, callback) {
  promise
    .then(
      result => process.nextTick(() => callback(null, result)),
      // Avoid marking the promise as rejected
      err => process.nextTick(() => callback(err)));
}

module.exports = {
  delay,
  fromCallback,
  fromEvent,
  getCallback,
  newQueryPlan,
  optionalCallback,
  times,
  toBackground,
  toCallback
};