exports.connect =
exports.isIP =
exports.isIPv4 =
exports.isIPv6 =
  function () { return true };

const { NSocket, ConnData, SocketAddress } = require("../lib/socket")

let M = new NSocket();

const normalizedArgsSymbol = Symbol("normalizedArgsSymbol");

const { Duplex } = require("readable-stream");
const { EventEmitter } = require("events");


function toSocketAddress(opt) {
  let req = new SocketAddress()
  if (((typeof opt.path) === 'string') && opt.path != "") {
    req.setProtocol("unix")
    req.setAddress(opt.path)
  } else if (opt.port && opt.host) {
    req.setProtocol("tcp")
    req.setAddress(`${opt.host}:${opt.port}`)
  } else {
    throw `Only support the unix and tcp socket! Please specify options.path or options.host and options.port field.`
  }
  return req
}

class Server extends EventEmitter {
  constructor(options, connectionListener) {
    super();

    if (!connectionListener) {
      connectionListener = options;
      options = {};
    }

    this._connectionListener = connectionListener
    this._control = {}
  }

  address() {
    return this._address || "connecting"
  }

  close(cb) {
    this._control.cancel()
    if (cb) {
      cb()
    }
  }

  async listen(...args) {
    args = normalizeArgs(args);
    const options = args[0];
    const callback = args[1];

    if (callback) {
      this.on("listening", callback)
    }

    await M.Listen(toSocketAddress(options), (c) => {
      const cid = c.getId()
      if (cid === 0) {
        this._address = c.getPeer().getAddress();
        this.emit("listening", true)
        return;
      }

      let socket = new Socket()
      socket._init(c, M.ReadWriteCloseConn.bind(M))
      this._connectionListener(socket);
    }, this._control)
  }
}

class Socket extends Duplex {
  constructor(option) {
    super();
    this._dial_control = {};

    this._control = new Promise((resolve)=>{
      this.__control_resolve__ = resolve;
    })
    this.connecting = true
  }

  destroy(err) {
    this._dial_control && this._dial_control.end()
  }

  address() {
    return this._address;
  }

  _init(cid, rwFn) {
    let ctrl = {}

    this._address = cid.getPeer().getAddress()

    const req = new ConnData()
    req.setId(cid.getId())

    rwFn(req, (conn) => { this.push(conn.getData()) }, ctrl)
      .then((msg) => {
        if (msg.code != 0) {
          this.emit("error", msg)
        } else {
          this.emit("end", msg)
          this.emit("close", msg)
        }
      })
      .catch((e)=>{this.emit("error", e)})

    this.__control_resolve__(ctrl)
  }

  _dial({path, port, host, localAddress, localPort, family, hints, lookup}, callback) {
    const req = toSocketAddress({path, port, host})
    return M.Dial(req, (cid) => { callback(cid) }, this._dial_control)
  }
  connect(options, cb) {
    this._dial(options, (cid) => {
      this._init(cid, M.Connect.bind(M));
      this.connecting = false
      this.emit("connect");
    }).then((c) => { this._on_close(c) })

    return this;
  }

  _on_close(c) {
    this.emit("close", c.code != 0)
  }

  _read(size) {
    // data is automatically pushed to buffer when the socket received chunk
  }
  async _write(chunk, encoding, callback) {
    const ctrl = await this._control;

    const req = new ConnData();
    req.setData(chunk)
    try {
      ctrl.write(req)
      callback(null)
    } catch (e) {
      callback(e)
    }
  }
  async _final(callback) {
    const ctrl = await this._control;
    try {
      ctrl.end()
      callback(null)
    } catch (e) {
      callback(e)
    }
  }

  setNoDelay() {
    // FIXME: remove this
    return true
  }
}

exports.connect = function(...args) {
  const normalized = normalizeArgs(args);
  const options = normalized[0];
  const socket = new Socket(options);

  if (options.timeout) {
    socket.setTimeout(options.timeout);
  }

  return socket.connect(options, normalized[1]);
}

exports.createConnection = exports.connect;

exports.createServer =  function(options, connectionListener) {
  return new Server(options, connectionListener);
}


function isPipeName(s) {
  return typeof s === 'string' && toNumber(s) === false;
}
function toNumber(x) { return (x = Number(x)) >= 0 ? x : false; }


// Returns an array [options, cb], where options is an object,
// cb is either a function or null.
// Used to normalize arguments of Socket.prototype.connect() and
// Server.prototype.listen(). Possible combinations of parameters:
//   (options[...][, cb])
//   (path[...][, cb])
//   ([port][, host][...][, cb])
// For Socket.prototype.connect(), the [...] part is ignored
// For Server.prototype.listen(), the [...] part is [, backlog]
// but will not be handled here (handled in listen())
function normalizeArgs(args) {
  let arr;

  if (args.length === 0) {
    arr = [{}, null];
    arr[normalizedArgsSymbol] = true;
    return arr;
  }

  const arg0 = args[0];
  let options = {};
  if (typeof arg0 === 'object' && arg0 !== null) {
    // (options[...][, cb])
    options = arg0;
  } else if (isPipeName(arg0)) {
    // (path[...][, cb])
    options.path = arg0;
  } else {
    // ([port][, host][...][, cb])
    options.port = arg0;
    if (args.length > 1 && typeof args[1] === 'string') {
      options.host = args[1];
    }
  }

  const cb = args[args.length - 1];
  if (typeof cb !== 'function')
    arr = [options, null];
  else
    arr = [options, cb];

  arr[normalizedArgsSymbol] = true;
  return arr;
}
