const P2PNode = require('./libp2p_node');
const PeerId = require('peer-id');
const PeerInfo = require('peer-info');
const PeerBook = require('peer-book');
const Multiaddr = require('multiaddr');
const pull = require('pull-stream');
const pb = require('../protobuf');
const { protoStreamDecode, protoStreamEncode, peerInfoProtoUnmarshal } = require('./util');
const { DEFAULT_LISTEN_ADDR, PROTOCOLS } = require('./constants');

class DirectoryNode {

  constructor(options) {
    let { peerId, listenAddresses } = options;
    if (listenAddresses == null) listenAddresses = [DEFAULT_LISTEN_ADDR];

    const peerInfo = new PeerInfo(peerId);
    listenAddresses.forEach(addr => {
      peerInfo.multiaddr.add(Multiaddr(addr));
    });

    this.p2p = new P2PNode({ peerInfo });
    this.registeredPeers = new PeerBook();
    this.p2p.handle(PROTOCOLS.dir.register, this.registerHandler.bind(this));
    this.p2p.handle(PROTOCOLS.dir.lookup, this.lookupHandler.bind(this));
    this.p2p.handle(PROTOCOLS.dir.list, this.listHandler.bind(this));
  }

  start() {
    return this.p2p.start();
  }

  stop() {
    return this.p2p.stop();
  }

  get peerInfo() {
    return this.p2p.peerInfo;
  }

  registerHandler(protocol, conn) {
    // for some reason, conn.peerInfo is always null here,
    let peerForConn = null;

    const sink = () => read => {
      read(null, function next(end, peerInfo) {
        if (end === true) {
          if (peerForConn) {
            this.registeredPeers.removeByB58String(peerForConn.id.toB58String());
          }
          return;
        }

        if (end) throw end;

        if (peerInfo) {
          peerForConn = peerInfo;
          this.registeredPeers.put(peerInfo);
        }
        read(null, next);
      });
    };

    const abortable = this.p2p.newAbortable();

    pull(conn, protoStreamDecode(pb.dir.RegisterPeer), pull.map(req => req.info), pull.map(peerInfoProtoUnmarshal), abortable, sink);
  }

  lookupHandler(protocol, conn) {
    const abortable = this.p2p.newAbortable();

    pull(conn, protoStreamDecode(pb.dir.LookupPeerRequest), pull.map(req => this._doLookup(req)), protoStreamEncode(pb.dir.LookupPeerResponse), abortable, conn);
  }

  _doLookup(req) {
    if (req.id == null) {
      return { peer: null };
    }

    try {
      const peerInfo = this.registeredPeers.getByB58String(req.id);
      const peer = {
        id: peerInfo.id.toB58String(),
        addr: peerInfo.multiaddrs.map(maddr => maddr.buffer)
      };
      return { peer };
    } catch (err) {
      return { peer: null };
    }
  }

  listHandler(protocol, conn) {}
}

module.exports = DirectoryNode;