////
// Manages the commands on disk, in NOSE_ROOT_BIN and NOSE_BIN

import { Glob } from "bun"
import { watch, readFileSync } from "fs"
import { join, basename } from "path"
import { isFile } from "./utils"
import { sendAll } from "./websocket"
import { expectDir, mtimeEpoch } from "./utils"
import { importUrl } from "./shared/utils"
import type { Command, Commands } from "./shared/types"
import { projectBin, projectName } from "./project"
import { DEFAULT_PROJECT, NOSE_DIR, NOSE_ROOT_BIN, NOSE_BIN } from "./config"

export function initCommands() {
  startWatchers()
}

export async function commands(project = DEFAULT_PROJECT): Promise<Commands> {
  let binCmds = await findCommands(NOSE_BIN)
  let rootCmds = await findCommands(NOSE_ROOT_BIN)
  let projCmds = project === DEFAULT_PROJECT ? new Map : await findCommands(projectBin())

  return Object.fromEntries(
    [
      ...Object.entries(binCmds),
      ...Object.entries(rootCmds),
      ...Object.entries(projCmds)
    ]
      .sort((a, b) => a[0].localeCompare(b[0]))
  )
}

export async function findCommands(path: string): Promise<Commands> {
  const glob = new Glob("**/*.{ts,tsx}")
  let obj: Commands = {}

  for await (const file of glob.scan(path))
    obj[file.replace(/\.tsx?$/, "")] = describeCommand(join(path, file))

  return obj
}

function describeCommand(path: string): Command {
  const code = readFileSync(path, "utf8")
  let game = /^export const game = true$/mg.test(code)
  let browser = /^\/\/\/ ?<reference lib=['"]dom['"]\s*\/>$/mg.test(code)

  return {
    name: basename(path).replace(/\.tsx?$/, ""),
    type: game ? "game" : browser ? "browser" : "server"
  }
}

export function commandPath(cmd: string): string | undefined {
  let paths = [
    join(NOSE_BIN, cmd + ".ts"),
    join(NOSE_BIN, cmd + ".tsx"),
    join(NOSE_ROOT_BIN, cmd + ".ts"),
    join(NOSE_ROOT_BIN, cmd + ".tsx"),
  ]

  if (projectName() !== DEFAULT_PROJECT)
    paths = paths.concat(
      join(projectBin(), cmd + ".ts"),
      join(projectBin(), cmd + ".tsx"),
    )

  return paths.find((path: string) => isFile(path))
}

export function commandExists(cmd: string): boolean {
  return commandPath(cmd) !== undefined
}

export async function commandSource(name: string): Promise<string> {
  const path = commandPath(name)
  if (!path) return ""
  return Bun.file(path).text()
}

export async function loadCommandModule(cmd: string) {
  const path = commandPath(cmd)
  if (!path) return

  if (path.startsWith(NOSE_DIR))
    return await importUrl(path, await mtimeEpoch(path))
  else
    return await importUrl(path)
}

let noseDirWatcher
let binCmdWatcher
function startWatchers() {
  if (!expectDir(NOSE_BIN)) return
  if (!expectDir(NOSE_ROOT_BIN)) return

  binCmdWatcher = watch(NOSE_BIN, async (event, filename) => {
    sendAll({ type: "commands", data: await commands() })
  })

  noseDirWatcher = watch(NOSE_DIR, async (event, filename) =>
    sendAll({ type: "commands", data: await commands() })
  )
}