import * as cp from "child_process"
import Bundler from "parcel-bundler"
import * as path from "path"

async function main(): Promise<void> {
  try {
    const watcher = new Watcher()
    await watcher.watch()
  } catch (error) {
    console.error(error.message)
    process.exit(1)
  }
}

class Watcher {
  private readonly rootPath = path.resolve(__dirname, "../..")
  private readonly vscodeSourcePath = path.join(this.rootPath, "lib/vscode")

  private static log(message: string, skipNewline = false): void {
    process.stdout.write(message)
    if (!skipNewline) {
      process.stdout.write("\n")
    }
  }

  public async watch(): Promise<void> {
    let server: cp.ChildProcess | undefined
    const restartServer = (): void => {
      if (server) {
        server.kill()
      }
      const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(2))
      console.log(`[server] spawned process ${s.pid}`)
      s.on("exit", () => console.log(`[server] process ${s.pid} exited`))
      server = s
    }

    const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
    const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
    const plugin = process.env.PLUGIN_DIR
      ? cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR })
      : undefined
    const bundler = this.createBundler()

    const cleanup = (code?: number | null): void => {
      Watcher.log("killing vs code watcher")
      vscode.removeAllListeners()
      vscode.kill()

      Watcher.log("killing tsc")
      tsc.removeAllListeners()
      tsc.kill()

      if (plugin) {
        Watcher.log("killing plugin")
        plugin.removeAllListeners()
        plugin.kill()
      }

      if (server) {
        Watcher.log("killing server")
        server.removeAllListeners()
        server.kill()
      }

      Watcher.log("killing bundler")
      process.exit(code || 0)
    }

    process.on("SIGINT", () => cleanup())
    process.on("SIGTERM", () => cleanup())

    vscode.on("exit", (code) => {
      Watcher.log("vs code watcher terminated unexpectedly")
      cleanup(code)
    })
    tsc.on("exit", (code) => {
      Watcher.log("tsc terminated unexpectedly")
      cleanup(code)
    })
    if (plugin) {
      plugin.on("exit", (code) => {
        Watcher.log("plugin terminated unexpectedly")
        cleanup(code)
      })
    }
    const bundle = bundler.bundle().catch(() => {
      Watcher.log("parcel watcher terminated unexpectedly")
      cleanup(1)
    })
    bundler.on("buildEnd", () => {
      console.log("[parcel] bundled")
    })
    bundler.on("buildError", (error) => {
      console.error("[parcel]", error)
    })

    vscode.stderr.on("data", (d) => process.stderr.write(d))
    tsc.stderr.on("data", (d) => process.stderr.write(d))
    if (plugin) {
      plugin.stderr.on("data", (d) => process.stderr.write(d))
    }

    // From https://github.com/chalk/ansi-regex
    const pattern = [
      "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
      "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))",
    ].join("|")
    const re = new RegExp(pattern, "g")

    /**
     * Split stdout on newlines and strip ANSI codes.
     */
    const onLine = (proc: cp.ChildProcess, callback: (strippedLine: string, originalLine: string) => void): void => {
      let buffer = ""
      if (!proc.stdout) {
        throw new Error("no stdout")
      }
      proc.stdout.setEncoding("utf8")
      proc.stdout.on("data", (d) => {
        const data = buffer + d
        const split = data.split("\n")
        const last = split.length - 1

        for (let i = 0; i < last; ++i) {
          callback(split[i].replace(re, ""), split[i])
        }

        // The last item will either be an empty string (the data ended with a
        // newline) or a partial line (did not end with a newline) and we must
        // wait to parse it until we get a full line.
        buffer = split[last]
      })
    }

    let startingVscode = false
    let startedVscode = false
    onLine(vscode, (line, original) => {
      console.log("[vscode]", original)
      // Wait for watch-client since "Finished compilation" will appear multiple
      // times before the client starts building.
      if (!startingVscode && line.includes("Starting watch-client")) {
        startingVscode = true
      } else if (startingVscode && line.includes("Finished compilation")) {
        if (startedVscode) {
          bundle.then(restartServer)
        }
        startedVscode = true
      }
    })

    onLine(tsc, (line, original) => {
      // tsc outputs blank lines; skip them.
      if (line !== "") {
        console.log("[tsc]", original)
      }
      if (line.includes("Watching for file changes")) {
        bundle.then(restartServer)
      }
    })

    if (plugin) {
      onLine(plugin, (line, original) => {
        // tsc outputs blank lines; skip them.
        if (line !== "") {
          console.log("[plugin]", original)
        }
        if (line.includes("Watching for file changes")) {
          bundle.then(restartServer)
        }
      })
    }
  }

  private createBundler(out = "dist"): Bundler {
    return new Bundler(
      [
        path.join(this.rootPath, "src/browser/register.ts"),
        path.join(this.rootPath, "src/browser/serviceWorker.ts"),
        path.join(this.rootPath, "src/browser/pages/login.ts"),
        path.join(this.rootPath, "src/browser/pages/vscode.ts"),
      ],
      {
        outDir: path.join(this.rootPath, out),
        cacheDir: path.join(this.rootPath, ".cache"),
        minify: !!process.env.MINIFY,
        logLevel: 1,
        publicUrl: ".",
      },
    )
  }
}

main()
