import path from "path";
import chalk from "chalk";
import esbuild from "esbuild";
import { Filesystem } from "./fs.js";
export { nodeFs, RecordingFs } from "./fs.js";
export type { Filesystem } from "./fs.js";

export const actionsDir = "actions";

// Returns a generator of { isDir, path } for all paths
// within dirPath in some topological order (not including
// dirPath itself).
export function* walkDir(
  fs: Filesystem,
  dirPath: string
): Generator<{ isDir: boolean; path: string }, void, void> {
  for (const dirEntry of fs.listDir(dirPath)) {
    const childPath = path.join(dirPath, dirEntry.name);
    if (dirEntry.isDirectory()) {
      yield { isDir: true, path: childPath };
      yield* walkDir(fs, childPath);
    } else if (dirEntry.isFile()) {
      yield { isDir: false, path: childPath };
    }
  }
}

export interface Bundle {
  path: string;
  source: string;
  sourceMap?: string;
}

export class BundleError extends Error {}

type EsBuildResult = esbuild.BuildResult & {
  outputFiles: esbuild.OutputFile[];
};

async function doEsbuild(
  fs: Filesystem,
  dir: string,
  entryPoints: string[],
  generateSourceMaps: boolean,
  platform: esbuild.Platform,
  chunksFolder: string
): Promise<EsBuildResult> {
  try {
    const result = await esbuild.build({
      entryPoints,
      bundle: true,
      platform: platform,
      format: "esm",
      target: "esnext",
      outdir: "out",
      outbase: dir,
      write: false,
      sourcemap: generateSourceMaps,
      splitting: true,
      chunkNames: path.join(chunksFolder, "[hash]"),
      treeShaking: true,
      minify: false,
      metafile: true,
    });

    for (const [relPath, input] of Object.entries(result.metafile!.inputs)) {
      // TODO: esbuild outputs paths prefixed with "(disabled)"" when bundling our internal
      // udf-system package. The files do actually exist locally, though.
      if (relPath.indexOf("(disabled):") !== -1) {
        continue;
      }
      const absPath = path.resolve(relPath);
      const st = fs.stat(absPath);
      if (st.size !== input.bytes) {
        throw new Error(
          `Bundled file ${absPath} changed right after esbuild invocation`
        );
      }
      fs.registerPath(absPath, st);
    }
    return result;
  } catch (err) {
    throw new BundleError(`esbuild failed: ${(err as any).toString()}`);
  }
}

export async function bundle(
  fs: Filesystem,
  dir: string,
  entryPoints: string[],
  generateSourceMaps: boolean,
  platform: esbuild.Platform,
  chunksFolder = "_deps"
): Promise<Bundle[]> {
  const result = await doEsbuild(
    fs,
    dir,
    entryPoints,
    generateSourceMaps,
    platform,
    chunksFolder
  );
  if (result.errors.length) {
    for (const error of result.errors) {
      console.log(chalk.red(`esbuild error: ${error.text}`));
    }
    throw new BundleError("esbuild failed");
  }
  for (const warning of result.warnings) {
    console.log(chalk.yellow(`esbuild warning: ${warning.text}`));
  }
  const sourceMaps = new Map();
  const modules: Bundle[] = [];
  for (const outputFile of result.outputFiles) {
    const relPath = path.relative(path.normalize("out"), outputFile.path);
    if (path.extname(relPath) === ".map") {
      sourceMaps.set(relPath, outputFile.text);
      continue;
    }
    const posixRelPath = relPath.split(path.sep).join(path.posix.sep);
    modules.push({ path: posixRelPath, source: outputFile.text });
  }
  for (const module of modules) {
    const sourceMapPath = module.path + ".map";
    const sourceMap = sourceMaps.get(sourceMapPath);
    if (sourceMap) {
      module.sourceMap = sourceMap;
    }
  }
  return modules;
}

export async function bundleSchema(fs: Filesystem, dir: string) {
  return bundle(fs, dir, [path.resolve(dir, "schema.ts")], true, "neutral");
}

// If you wanted to build regex patterns programatically (questionable but useful)
// you need to escape special characters in it.
function escapeRegex(s: string): string {
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}

async function entryPoints(
  fs: Filesystem,
  dir: string,
  includePattern: RegExp,
  verbose: boolean
): Promise<string[]> {
  const entryPoints = [];
  for (const { isDir, path: fpath } of walkDir(fs, dir)) {
    if (isDir) {
      continue;
    }
    const relPath = path.relative(dir, fpath);
    const base = path.parse(fpath).base;

    const log = (line: string) => {
      if (verbose) {
        console.log(line);
      }
    };
    if (!relPath.match(includePattern)) {
      // Skip without logging.
      continue;
    } else if (relPath.startsWith("_deps" + path.sep)) {
      throw new Error(
        `The path "${fpath}" is within the "_deps" directory, which is reserved for dependencies. Please move your code to another directory.`
      );
    } else if (relPath.startsWith("_generated" + path.sep)) {
      log(chalk.yellow(`Skipping ${fpath}`));
    } else if (base.startsWith(".")) {
      log(chalk.yellow(`Skipping dotfile ${fpath}`));
    } else if (base === "README.md") {
      log(chalk.yellow(`Skipping ${fpath}`));
    } else if (base === "_generated.ts") {
      log(chalk.yellow(`Skipping ${fpath}`));
    } else if (base === "schema.ts") {
      log(chalk.yellow(`Skipping ${fpath}`));
    } else if (base.includes(".test.")) {
      log(chalk.yellow(`Skipping ${fpath}`));
    } else if (base === "tsconfig.json") {
      log(chalk.yellow(`Skipping ${fpath}`));
    } else if (relPath.includes(" ")) {
      log(chalk.yellow(`Skipping ${relPath} because it contains a space`));
    } else {
      log(chalk.green(`Preparing ${fpath}`));
      entryPoints.push(fpath);
    }
  }
  return entryPoints;
}

export async function allEntryPoints(
  fs: Filesystem,
  dir: string,
  verbose: boolean
) {
  return entryPoints(fs, dir, new RegExp(".*"), verbose);
}

export async function databaseEntryPoints(
  fs: Filesystem,
  dir: string,
  verbose: boolean
): Promise<string[]> {
  const excludePrefix = actionsDir + path.sep;
  return entryPoints(
    fs,
    dir,
    // Exclude functions/ subdirectory.
    RegExp(`^(?!${escapeRegex(excludePrefix)})`),
    verbose
  );
}

export async function actionsEntryPoints(
  fs: Filesystem,
  dir: string,
  verbose: boolean
): Promise<string[]> {
  // Only look in functions subdirectory.
  const prefix = actionsDir + path.sep;
  return entryPoints(fs, dir, RegExp(`^${escapeRegex(prefix)}`), verbose);
}
