import { hasShape, predicates, is } from "@sealcode/ts-predicates";
import {
  ChildProcessWithoutNullStreams,
  spawn,
  spawnSync,
} from "child_process";
import { concurrency } from "../config.json";
import { IMAGE_NAME } from "./docker-args";

export class Container {
  callbacks: Array<() => void> = [];
  ready = false;
  id: string;
  output: "";
  bg_process: ChildProcessWithoutNullStreams;
  constructor() {
    this.id = spawnSync(
      "docker",
      ["run", "-d", "-v", `${process.cwd()}/static:/opt/static`, IMAGE_NAME],
      {
        cwd: process.cwd(),
      }
    )
      .stdout.toString()
      .replace("\n", "");
    this.bg_process = spawn("docker", ["logs", "-f", this.id]);
    this.bg_process.stdout.on("data", (d: Buffer) => {
      try {
        const parsed = JSON.parse(d.toString()) as unknown;
        if (
          is(parsed, predicates.object) &&
          hasShape({ code: predicates.string }, parsed) &&
          parsed.code == "ready"
        ) {
          this.ready = true;
          this.signalReady();
        }
      } catch (e) {
        // noop
      }
      this.output += d.toString();
    });
  }

  signalReady(): void {
    this.callbacks.forEach((callback) => callback());
  }

  onReady(callback: () => void): void {
    this.ready ? callback() : this.callbacks.push(callback);
  }

  async waitReady(): Promise<void> {
    if (this.ready) {
      return;
    }
    return new Promise<void>((resolve) => {
      this.onReady(resolve);
    });
  }

  close(): void {
    spawn("docker", ["rm", "-f", this.id]);
  }

  closeSync(): void {
    spawnSync("docker", ["rm", "-f", this.id]);
    console.log("doker rm done", this.id);
  }
}

export default new (class ContainerPool {
  pool: Container[] = [];
  constructor(public concurrency: number) {
    this.concurrency = concurrency;
    for (let i = 1; i <= this.concurrency; i++) {
      this.generateContainer();
    }

    process.on("SIGINT", () => {
      console.log("SIGINT");
      this.clear();
    });
  }

  generateContainer() {
    this.pool.push(new Container());
  }

  getContainer() {
    if (!this.pool.length) {
      throw new Error("pool is empty, try again!");
    }
    const container = this.pool.shift(); // get and remove from pool the oldest container
    if (!container) {
      throw new Error("Pool was somehow empty!");
    }
    this.generateContainer();
    return container;
  }

  clear() {
    console.log("Removing all containers from the pool");
    for (const container of this.pool) {
      container.closeSync();
    }
    console.log("Removing containers done");
    process.exit(0);
  }
})(concurrency);
