// Adapted from: https://github.com/Cryptkeeper/mcping-js/
import net from "net";
import { SocksProxyAgent } from "socks-proxy-agent";

class MinecraftProtocol {
  static writeVarInt(val) {
    // "VarInts are never longer than 5 bytes"
    // https://wiki.vg/Data_types#VarInt_and_VarLong
    const buf = Buffer.alloc(5);
    let written = 0;

    while (true) {
      if ((val & 0xffffff80) === 0) {
        buf.writeUInt8(val, written++);
        break;
      } else {
        buf.writeUInt8((val & 0x7f) | 0x80, written++);
        val >>>= 7;
      }
    }

    return buf.slice(0, written);
  }

  static writeString(val) {
    return Buffer.from(val, "UTF-8");
  }

  static writeUShort(val) {
    return Buffer.from([val >> 8, val & 0xff]);
  }

  static concat(chunks) {
    let length = 0;

    for (const chunk of chunks) {
      length += chunk.length;
    }

    const buf = [MinecraftProtocol.writeVarInt(length), ...chunks];

    return Buffer.concat(buf);
  }
}

class MinecraftBufferReader {
  constructor(buffer) {
    this._buffer = buffer;
    this._offset = 0;
  }

  readVarInt() {
    let val = 0;
    let count = 0;

    while (true) {
      const b = this._buffer.readUInt8(this._offset++);

      val |= (b & 0x7f) << (count++ * 7);

      if ((b & 0x80) != 128) {
        break;
      }
    }

    return val;
  }

  readString() {
    const length = this.readVarInt();
    const val = this._buffer.toString(
      "UTF-8",
      this._offset,
      this._offset + length
    );

    // Advance the reader index forward by the string length
    this._offset += length;

    return val;
  }

  offset() {
    return this._offset;
  }
}

export function pingMinecraftServer(host, port, proxy) {
  return new Promise((resolve, reject) => {
    const { protocol, host: proxyHost, port: proxyPort, username: proxyUsername, password: proxyPassword } = proxy;

    const agent = new SocksProxyAgent(
      `${protocol}://${proxyUsername && proxyPassword ? `${proxyUsername}:${proxyPassword}@` : ""}${proxyHost}:${proxyPort}`
    );

    const socket = net.createConnection({
      host: host,
      port: port,
      agent: agent,
    });

    const timeoutTask = setTimeout(() => {
      socket.emit("error", new Error("Socket timeout"));
    }, 5000);

    const closeSocket = () => {
      socket.destroy();
      clearTimeout(timeoutTask);
    };

    let didFireError = false;

    const handleErr = (err) => {
      closeSocket();
      if (!didFireError) {
        didFireError = true;
        reject(err);
      }
    };

    socket.setNoDelay(true);

    socket.on("connect", () => {
      const handshake = MinecraftProtocol.concat([
        MinecraftProtocol.writeVarInt(0),
        MinecraftProtocol.writeVarInt(340),
        MinecraftProtocol.writeVarInt(host.length),
        MinecraftProtocol.writeString(host),
        MinecraftProtocol.writeUShort(port),
        MinecraftProtocol.writeVarInt(1),
      ]);

      socket.write(handshake);

      const request = MinecraftProtocol.concat([
        MinecraftProtocol.writeVarInt(0),
      ]);

      socket.write(request);
    });

    let incomingBuffer = Buffer.alloc(0);

    socket.on("data", (data) => {
      incomingBuffer = Buffer.concat([incomingBuffer, data]);

      if (incomingBuffer.length < 5) {
        return;
      }

      const bufferReader = new MinecraftBufferReader(incomingBuffer);
      const length = bufferReader.readVarInt();

      if (incomingBuffer.length - bufferReader.offset() < length) {
        return;
      }

      const id = bufferReader.readVarInt();

      if (id === 0) {
        const reply = bufferReader.readString();

        try {
          const message = JSON.parse(reply);
          resolve(message);
          closeSocket();
        } catch (err) {
          handleErr(err);
        }
      } else {
        handleErr(new Error("Received unexpected packet"));
      }
    });

    socket.on("error", handleErr);
  });
}
