'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});
exports.default = void 0;

var _types = require('./types');

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}

class Farm {
  constructor(numOfWorkers, callback, computeWorkerKey) {
    _defineProperty(this, '_computeWorkerKey', void 0);

    _defineProperty(this, '_cacheKeys', void 0);

    _defineProperty(this, '_callback', void 0);

    _defineProperty(this, '_last', void 0);

    _defineProperty(this, '_locks', void 0);

    _defineProperty(this, '_numOfWorkers', void 0);

    _defineProperty(this, '_offset', void 0);

    _defineProperty(this, '_queue', void 0);

    this._cacheKeys = Object.create(null);
    this._callback = callback;
    this._last = [];
    this._locks = [];
    this._numOfWorkers = numOfWorkers;
    this._offset = 0;
    this._queue = [];

    if (computeWorkerKey) {
      this._computeWorkerKey = computeWorkerKey;
    }
  }

  doWork(method, ...args) {
    return new Promise((resolve, reject) => {
      const computeWorkerKey = this._computeWorkerKey;
      const request = [_types.CHILD_MESSAGE_CALL, false, method, args];
      let worker = null;
      let hash = null;

      if (computeWorkerKey) {
        hash = computeWorkerKey.call(this, method, ...args);
        worker = hash == null ? null : this._cacheKeys[hash];
      }

      const onStart = worker => {
        if (hash != null) {
          this._cacheKeys[hash] = worker;
        }
      };

      const onEnd = (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      };

      const task = {
        onEnd,
        onStart,
        request
      };

      if (worker) {
        this._enqueue(task, worker.getWorkerId());
      } else {
        this._push(task);
      }
    });
  }

  _getNextTask(workerId) {
    let queueHead = this._queue[workerId];

    while (queueHead && queueHead.task.request[1]) {
      queueHead = queueHead.next || null;
    }

    this._queue[workerId] = queueHead;
    return queueHead && queueHead.task;
  }

  _process(workerId) {
    if (this._isLocked(workerId)) {
      return this;
    }

    const task = this._getNextTask(workerId);

    if (!task) {
      return this;
    }

    const onEnd = (error, result) => {
      task.onEnd(error, result);

      this._unlock(workerId);

      this._process(workerId);
    };

    task.request[1] = true;

    this._lock(workerId);

    this._callback(workerId, task.request, task.onStart, onEnd);

    return this;
  }

  _enqueue(task, workerId) {
    const item = {
      next: null,
      task
    };

    if (task.request[1]) {
      return this;
    }

    if (this._queue[workerId]) {
      this._last[workerId].next = item;
    } else {
      this._queue[workerId] = item;
    }

    this._last[workerId] = item;

    this._process(workerId);

    return this;
  }

  _push(task) {
    for (let i = 0; i < this._numOfWorkers; i++) {
      this._enqueue(task, (this._offset + i) % this._numOfWorkers);
    }

    this._offset++;
    return this;
  }

  _lock(workerId) {
    this._locks[workerId] = true;
  }

  _unlock(workerId) {
    this._locks[workerId] = false;
  }

  _isLocked(workerId) {
    return this._locks[workerId];
  }
}

exports.default = Farm;
