'use strict';
const util = require('util');
const Duplex = require('stream').Duplex;
const uuid = require('./uuid');

module.exports = IOStream;

util.inherits(IOStream, Duplex);

/**
 * Duplex
 *
 * @param {Object} options
 * @api private
 */
function IOStream(options) {
  if (!(this instanceof IOStream)) {
    return new IOStream(options);
  }

  IOStream.super_.call(this, options);

  this.options = options;
  this.id = uuid();
  this.socket = null;

  // Buffers
  this.pushBuffer = [];
  this.writeBuffer = [];

  // Op states
  this._readable = false;
  this._writable = false;
  this.destroyed = false;

  // default to *not* allowing half open sockets
  this.allowHalfOpen = (options && options.allowHalfOpen) || false;

  this.on('finish', this._onfinish);
  this.on('end', this._onend);
  this.on('error', this._onerror);
}

/**
 * Ensures that no more I/O activity happens on this stream.
 * Not necessary in the usual case.
 *
 * @api public
 */
IOStream.prototype.destroy = function () {
  if (this.destroyed) {
    return;
  }

  this.readable = this.writable = false;

  if (this.socket) {
    this.socket.cleanup(this.id);
    this.socket = null;
  }

  this.destroyed = true;
};

/**
 * Local read
 *
 * @api private
 */
IOStream.prototype._read = function (size) {
  let push;

  // We can not read from the socket if it's destroyed obviously ...
  if (this.destroyed) return;

  if (this.pushBuffer.length) {
    // flush buffer and end if it exists.
    while ((push = this.pushBuffer.shift())) {
      if (!push()) break;
    }
    return;
  }

  this._readable = true;

  // Go get data from remote stream
  // Calls
  // ._onread remotely
  // then
  // ._onwrite locally
  this.socket._read(this.id, size);
};

/* eslint-disable consistent-return */
/**
 * Read from remote stream
 *
 * @api private
 */
IOStream.prototype._onread = function () {
  const write = this.writeBuffer.shift();
  if (write) return write();

  this._writable = true;
};

/**
 * Write local data to remote stream
 * Calls
 * remtote ._onwrite
 *
 * @api private
 */
IOStream.prototype._write = function (chunk, encoding, callback) {
  const self = this;

  function write() {
    // We can not write to the socket if it's destroyed obviously ...
    if (self.destroyed) return;

    self._writable = false;
    self.socket._write(self.id, chunk, encoding, callback);
  }

  if (this._writable) {
    write();
  } else {
    this.writeBuffer.push(write);
  }
};

/**
 * Write the data fetched remotely
 * so that we can now read locally
 *
 * @api private
 */
IOStream.prototype._onwrite = function (chunk, encoding, callback) {
  const self = this;

  function push() {
    self._readable = false;
    const ret = self.push(chunk || '', encoding);
    callback();
    return ret;
  }

  if (this._readable) {
    push();
  } else {
    this.pushBuffer.push(push);
  }
};

/**
 * When ending send 'end' event to remote stream
 *
 * @api private
 */
IOStream.prototype._end = function () {
  if (this.pushBuffer.length) {
    // end after flushing buffer.
    this.pushBuffer.push(this._done.bind(this));
  } else {
    this._done();
  }
};

/**
 * Remote stream just ended
 *
 * @api private
 */
IOStream.prototype._done = function () {
  this._readable = false;

  // signal the end of the data.
  return this.push(null);
};

/**
 * the user has called .end(), and all the bytes have been
 * sent out to the other side.
 * If allowHalfOpen is false, or if the readable side has
 * ended already, then destroy.
 * If allowHalfOpen is true, then we need to set writable false,
 * so that only the writable side will be cleaned up.
 *
 * @api private
 */
IOStream.prototype._onfinish = function () {
  // Local socket just finished
  // send 'end' event to remote
  if (this.socket) {
    this.socket._end(this.id);
  }

  this.writable = false;
  this._writableState.ended = true;

  if (!this.readable || this._readableState.ended) {
    return this.destroy();
  }

  if (!this.allowHalfOpen) {
    this.push(null);

    // just in case we're waiting for an EOF.
    if (this.readable && !this._readableState.endEmitted) {
      this.read(0);
    }
  }
};

/**
 * the EOF has been received, and no more bytes are coming.
 * if the writable side has ended already, then clean everything
 * up.
 *
 * @api private
 */
IOStream.prototype._onend = function () {
  this.readable = false;
  this._readableState.ended = true;

  if (!this.writable || this._writableState.finished) {
    return this.destroy();
  }

  if (!this.allowHalfOpen) {
    this.end();
  }
};

/**
 * When error in local stream
 * notyify remote
 * if err.remote = true
 * then error happened on remote stream
 *
 * @api private
 */
IOStream.prototype._onerror = function (err) {
  // check if the error came from remote stream.
  if (!err.remote && this.socket) {
    // notify the error to the corresponding remote stream.
    this.socket._error(this.id, err);
  }

  this.destroy();
};
