'use strict';

const Dispatcher = require('./dispatcher');
const UnwrapHandler = require('../handler/unwrap-handler');
const {
  ClientDestroyedError,
  ClientClosedError,
  InvalidArgumentError,
} = require('../core/errors');
const {
  kDestroy,
  kClose,
  kClosed,
  kDestroyed,
  kDispatch,
} = require('../core/symbols');

const kOnDestroyed = Symbol('onDestroyed');
const kOnClosed = Symbol('onClosed');

class DispatcherBase extends Dispatcher {
  constructor() {
    super();

    this[kDestroyed] = false;
    this[kOnDestroyed] = null;
    this[kClosed] = false;
    this[kOnClosed] = [];
  }

  get destroyed() {
    return this[kDestroyed];
  }

  get closed() {
    return this[kClosed];
  }

  close(callback) {
    if (callback === undefined) {
      return new Promise((resolve, reject) => {
        this.close((err, data) => {
          return err ? reject(err) : resolve(data);
        });
      });
    }

    if (typeof callback !== 'function') {
      throw new InvalidArgumentError('invalid callback');
    }

    if (this[kDestroyed]) {
      queueMicrotask(() => callback(new ClientDestroyedError(), null));
      return;
    }

    if (this[kClosed]) {
      if (this[kOnClosed]) {
        this[kOnClosed].push(callback);
      } else {
        queueMicrotask(() => callback(null, null));
      }
      return;
    }

    this[kClosed] = true;
    this[kOnClosed].push(callback);

    const onClosed = () => {
      const callbacks = this[kOnClosed];
      this[kOnClosed] = null;
      for (let i = 0; i < callbacks.length; i++) {
        callbacks[i](null, null);
      }
    };

    // Should not error.
    this[kClose]()
      .then(() => this.destroy())
      .then(() => {
        queueMicrotask(onClosed);
      });
  }

  destroy(err, callback) {
    if (typeof err === 'function') {
      callback = err;
      err = null;
    }

    if (callback === undefined) {
      return new Promise((resolve, reject) => {
        this.destroy(err, (err, data) => {
          return err ?
              /* istanbul ignore next: should never error */ reject(err)
            : resolve(data);
        });
      });
    }

    if (typeof callback !== 'function') {
      throw new InvalidArgumentError('invalid callback');
    }

    if (this[kDestroyed]) {
      if (this[kOnDestroyed]) {
        this[kOnDestroyed].push(callback);
      } else {
        queueMicrotask(() => callback(null, null));
      }
      return;
    }

    if (!err) {
      err = new ClientDestroyedError();
    }

    this[kDestroyed] = true;
    this[kOnDestroyed] = this[kOnDestroyed] || [];
    this[kOnDestroyed].push(callback);

    const onDestroyed = () => {
      const callbacks = this[kOnDestroyed];
      this[kOnDestroyed] = null;
      for (let i = 0; i < callbacks.length; i++) {
        callbacks[i](null, null);
      }
    };

    // Should not error.
    this[kDestroy](err).then(() => {
      queueMicrotask(onDestroyed);
    });
  }

  dispatch(opts, handler) {
    if (!handler || typeof handler !== 'object') {
      throw new InvalidArgumentError('handler must be an object');
    }

    handler = UnwrapHandler.unwrap(handler);

    try {
      if (!opts || typeof opts !== 'object') {
        throw new InvalidArgumentError('opts must be an object.');
      }

      if (this[kDestroyed] || this[kOnDestroyed]) {
        throw new ClientDestroyedError();
      }

      if (this[kClosed]) {
        throw new ClientClosedError();
      }

      return this[kDispatch](opts, handler);
    } catch (err) {
      if (typeof handler.onError !== 'function') {
        throw err;
      }

      handler.onError(err);

      return false;
    }
  }
}

module.exports = DispatcherBase;