import { ChildProcess } from "child_process";
import { EventEmitter } from "events";

/**
 * Provides a client for tsserver. Tsserver does not use standard JSON-RPC
 * protocol thus the need for this custom client.
 */
export class Client {
  private data: Buffer|undefined;
  private id = 0;
  private responseEmitter = new EventEmitter();

  constructor(private readonly server: ChildProcess) {}

  listen() {
    this.server.stdout!.on('data', (data: Buffer) => {
      this.data = this.data ? Buffer.concat([this.data, data]) : data;
      // tsserver could batch multiple responses together so we have to go
      // through the entire buffer to keep looking for messages.
      const CONTENT_LENGTH = 'Content-Length: '
      do {
        const index = this.data.indexOf(CONTENT_LENGTH);
        if (index < 0) {
          return;
        }
        let start = index + CONTENT_LENGTH.length;
        let end = this.data.indexOf('\r\n', start);
        if (end < start) {
          return;
        }
        const contentLengthStr = this.data.slice(start, end).toString();
        const contentLength = Number(contentLengthStr);
        if (isNaN(contentLength) || contentLength < 0) {
          return;
        }
        start = end + 4;
        end = start + contentLength;
        if (end > this.data.length) {
          return;
        }
        const content = this.data.slice(start, end).toString();
        this.data = this.data.slice(end);
        try {
          const payload = JSON.parse(content);
          if (payload.type === "response") {
            const seq = `${payload.request_seq}`;
            this.responseEmitter.emit(seq, payload);
          }
        }
        catch (error) {
          this.responseEmitter.emit('error', error);
        }
      } while (this.data.length > 0)
    });
  }

  async send(type: string, command: string, params: {}) {
    const seq = this.id++;
    const request = {
      seq,
      type,
      command,
      arguments: params
    };
    this.server.stdin!.write(JSON.stringify(request) + '\r\n');
    return new Promise((resolve, reject) => {
      this.responseEmitter.once(`${seq}`, resolve);
      this.responseEmitter.once('error', reject);
    });
  }

  async sendRequest(command: string, params: {}) {
    return this.send('request', command, params);
  }
}