/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 * @flow
 */

'use strict';

const invariant = require('invariant');

/**
 * Static poolers. Several custom versions for each potential number of
 * arguments. A completely generic pooler is easy to implement, but would
 * require accessing the `arguments` object. In each of these, `this` refers to
 * the Class itself, not an instance. If any others are needed, simply add them
 * here, or in their own files.
 */
const oneArgumentPooler = function(copyFieldsFrom) {
  const Klass = this;
  if (Klass.instancePool.length) {
    const instance = Klass.instancePool.pop();
    Klass.call(instance, copyFieldsFrom);
    return instance;
  } else {
    return new Klass(copyFieldsFrom);
  }
};

const twoArgumentPooler = function(a1, a2) {
  const Klass = this;
  if (Klass.instancePool.length) {
    const instance = Klass.instancePool.pop();
    Klass.call(instance, a1, a2);
    return instance;
  } else {
    return new Klass(a1, a2);
  }
};

const threeArgumentPooler = function(a1, a2, a3) {
  const Klass = this;
  if (Klass.instancePool.length) {
    const instance = Klass.instancePool.pop();
    Klass.call(instance, a1, a2, a3);
    return instance;
  } else {
    return new Klass(a1, a2, a3);
  }
};

const fourArgumentPooler = function(a1, a2, a3, a4) {
  const Klass = this;
  if (Klass.instancePool.length) {
    const instance = Klass.instancePool.pop();
    Klass.call(instance, a1, a2, a3, a4);
    return instance;
  } else {
    return new Klass(a1, a2, a3, a4);
  }
};

const standardReleaser = function(instance) {
  const Klass = this;
  invariant(
    instance instanceof Klass,
    'Trying to release an instance into a pool of a different type.',
  );
  instance.destructor();
  if (Klass.instancePool.length < Klass.poolSize) {
    Klass.instancePool.push(instance);
  }
};

const DEFAULT_POOL_SIZE = 10;
const DEFAULT_POOLER = oneArgumentPooler;

type Pooler = any;

/**
 * Augments `CopyConstructor` to be a poolable class, augmenting only the class
 * itself (statically) not adding any prototypical fields. Any CopyConstructor
 * you give this may have a `poolSize` property, and will look for a
 * prototypical `destructor` on instances.
 *
 * @param {Function} CopyConstructor Constructor that can be used to reset.
 * @param {Function} pooler Customizable pooler.
 */
const addPoolingTo = function<T>(
  CopyConstructor: Class<T>,
  pooler: Pooler,
): Class<T> & {
  getPooled(
    ...args: $ReadOnlyArray<mixed>
  ): /* arguments of the constructor */ T,
  release(instance: mixed): void,
  ...
} {
  // Casting as any so that flow ignores the actual implementation and trusts
  // it to match the type we declared
  const NewKlass = (CopyConstructor: any);
  NewKlass.instancePool = [];
  NewKlass.getPooled = pooler || DEFAULT_POOLER;
  if (!NewKlass.poolSize) {
    NewKlass.poolSize = DEFAULT_POOL_SIZE;
  }
  NewKlass.release = standardReleaser;
  return NewKlass;
};

const PooledClass = {
  addPoolingTo: addPoolingTo,
  oneArgumentPooler: (oneArgumentPooler: Pooler),
  twoArgumentPooler: (twoArgumentPooler: Pooler),
  threeArgumentPooler: (threeArgumentPooler: Pooler),
  fourArgumentPooler: (fourArgumentPooler: Pooler),
};

module.exports = PooledClass;
