/* global Worker, window, setTimeout */

const WORKER_TIMEOUT = 60 * 1000; // one minute

class TimedOutError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name;
    if (typeof Error.captureStackTrace === 'function') {
      Error.captureStackTrace(this, this.constructor);
    } else {
      this.stack = new Error(message).stack;
    }
  }
}

class WorkerInterface {
  constructor(path, timeout = WORKER_TIMEOUT) {
    this._utilWorker = new Worker(path);
    this.timeout = timeout;
    this._jobs = Object.create(null);
    this._DEBUG = false;
    this._jobCounter = 0;

    this._utilWorker.onmessage = e => {
      const [jobId, errorForDisplay, result] = e.data;

      const job = this._getJob(jobId);
      if (!job) {
        throw new Error(
          `Received worker reply to job ${jobId}, but did not have it in our registry!`
        );
      }

      const { resolve, reject, fnName } = job;

      if (errorForDisplay) {
        return reject(
          new Error(
            `Error received from worker job ${jobId} (${fnName}): ${errorForDisplay}`
          )
        );
      }

      return resolve(result);
    };
  }

  _makeJob(fnName) {
    this._jobCounter += 1;
    const id = this._jobCounter;

    if (this._DEBUG) {
      window.log.info(`Worker job ${id} (${fnName}) started`);
    }
    this._jobs[id] = {
      fnName,
      start: Date.now(),
    };

    return id;
  }

  _updateJob(id, data) {
    const { resolve, reject } = data;
    const { fnName, start } = this._jobs[id];

    this._jobs[id] = {
      ...this._jobs[id],
      ...data,
      resolve: value => {
        this._removeJob(id);
        const end = Date.now();
        window.log.info(
          `Worker job ${id} (${fnName}) succeeded in ${end - start}ms`
        );
        return resolve(value);
      },
      reject: error => {
        this._removeJob(id);
        const end = Date.now();
        window.log.info(
          `Worker job ${id} (${fnName}) failed in ${end - start}ms`
        );
        return reject(error);
      },
    };
  }

  _removeJob(id) {
    if (this._DEBUG) {
      this._jobs[id].complete = true;
    } else {
      delete this._jobs[id];
    }
  }

  _getJob(id) {
    return this._jobs[id];
  }

  callWorker(fnName, ...args) {
    const jobId = this._makeJob(fnName);

    return new Promise((resolve, reject) => {
      this._utilWorker.postMessage([jobId, fnName, ...args]);

      this._updateJob(jobId, {
        resolve,
        reject,
        args: this._DEBUG ? args : null,
      });

      setTimeout(
        () =>
          reject(
            new TimedOutError(`Worker job ${jobId} (${fnName}) timed out`)
          ),
        this.timeout
      );
    });
  }
}

module.exports = WorkerInterface;