import esbuild, { PluginBuild } from "esbuild";
import linaria from "@linaria/esbuild";
import fs from "fs";
import path from "path";
import postcss from "postcss";
import sass from "sass";
import postcssrc from "postcss-load-config";

// this should give us the current directory where
// the project is being built
const BASE = process.cwd();

type Assets = {
  base: string;
  files: string[];
};

export function getFilesInDirectory(startPath: string, regex?: RegExp): Assets {
  if (!fs.existsSync(startPath)) {
    return {
      base: startPath,
      files: [],
    };
  }
  const files = fs.readdirSync(startPath);
  const result = files
    .flatMap((file) => {
      const filename = path.join(startPath, file);

      const stat = fs.lstatSync(filename);
      if (stat.isDirectory()) {
        return getFilesInDirectory(filename, regex).files;
      }
      if (!regex || regex.test(filename)) {
        return [filename];
      }
      return [];
    })
    .filter((x) => !!x);

  return {
    base: startPath,
    files: result,
  };
}

let GIT_ROOT = BASE;
while (!fs.existsSync(path.join(GIT_ROOT, ".git")) && GIT_ROOT !== "/") {
  GIT_ROOT = path.join(GIT_ROOT, "../");
}
if (GIT_ROOT === "/") {
  // eslint-disable-next-line no-undef
  console.log("not found");
  // eslint-disable-next-line no-undef
  process.exit(1);
}
const GIT_HASH = git_hash();

const buf = fs.readFileSync(path.join(BASE, "package.json"));
let _package = JSON.parse(buf.toString("utf-8"));

function git_hash() {
  const rev = fs
    .readFileSync(path.join(GIT_ROOT, ".git", "HEAD"))
    .toString()
    .trim()
    .split(/.*[: ]/)
    .slice(-1)[0];
  if (rev.indexOf("/") === -1) {
    return rev;
  } else {
    return fs.readFileSync(path.join(GIT_ROOT, ".git", rev)).toString().trim();
  }
}

// FIXME: Put this into some helper library.
function copyFilesPlugin(assets: Assets | Assets[]) {
  return {
    name: "copy-files",
    setup(build: PluginBuild) {
      if (!assets || (assets instanceof Array && !assets.length)) {
        return;
      }
      const list = assets instanceof Array ? assets : [assets];

      const outDir = build.initialOptions.outdir;
      if (outDir === undefined) {
        throw Error("esbuild build options does not specify outdir");
      }
      build.onEnd(() => {
        list.forEach((as) => {
          for (const file of as.files) {
            const destination = path.join(outDir, path.relative(as.base, file));
            const dirname = path.dirname(destination);
            if (!fs.existsSync(dirname)) {
              fs.mkdirSync(dirname, { recursive: true });
            }
            fs.copyFileSync(file, destination);
          }
        });
      });
    },
  };
}

const DEFAULT_SASS_FILTER = /\.(s[ac]ss|css)$/;

const sassPlugin: esbuild.Plugin = {
  name: "custom-build-sass",
  setup(build) {
    build.onLoad({ filter: DEFAULT_SASS_FILTER }, ({ path: file }) => {
      const resolveDir = path.dirname(file);
      const { css: contents } = sass.compile(file, { loadPaths: ["./"] });

      return {
        resolveDir,
        loader: "css",
        contents,
      };
    });
  },
};


/**
 * Problem: 
 *   No loader is configured for ".node" files: ../../node_modules/.pnpm/fsevents@2.3.3/node_modules/fsevents/fsevents.node
 * 
 * Reference:
 *   https://github.com/evanw/esbuild/issues/1051#issuecomment-806325487
 */
const nativeNodeModulesPlugin: esbuild.Plugin = {
  name: 'native-node-modules',
  setup(build) {
    // If a ".node" file is imported within a module in the "file" namespace, resolve 
    // it to an absolute path and put it into the "node-file" virtual namespace.
    build.onResolve({ filter: /\.node$/, namespace: 'file' }, args => ({
      path: require.resolve(args.path, { paths: [args.resolveDir] }),
      namespace: 'node-file',
    }))

    // Files in the "node-file" virtual namespace call "require()" on the
    // path from esbuild of the ".node" file in the output directory.
    build.onLoad({ filter: /.*/, namespace: 'node-file' }, args => ({
      contents: `
        import path from ${JSON.stringify(args.path)}
        try { module.exports = require(path) }
        catch {}
      `,
    }))

    // If a ".node" file is imported within a module in the "node-file" namespace, put
    // it in the "file" namespace where esbuild's default loading behavior will handle
    // it. It is already an absolute path since we resolved it to one above.
    build.onResolve({ filter: /\.node$/, namespace: 'node-file' }, args => ({
      path: args.path,
      namespace: 'file',
    }))

    // Tell esbuild's default loading behavior to use the "file" loader for
    // these ".node" files.
    let opts = build.initialOptions
    opts.loader = opts.loader || {}
    opts.loader['.node'] = 'file'
  },
}


const postCssPlugin: esbuild.Plugin = {
  name: "custom-build-postcss",
  setup(build) {
    build.onLoad({ filter: DEFAULT_SASS_FILTER }, async ({ path: file }) => {
      const resolveDir = path.dirname(file);
      const sourceBuffer = fs.readFileSync(file);
      const source = sourceBuffer.toString("utf-8");
      const postCssConfig = await postcssrc();
      postCssConfig.options.from = file;

      const { css: contents } = await postcss(postCssConfig.plugins).process(
        source,
        postCssConfig.options,
      );

      return {
        resolveDir,
        loader: "css",
        contents,
      };
    });
  },
};

/**
 * This should be able to load the plugin but but some reason it does not work
 *
 *       text: "Cannot find module '../plugins/preeval'\n" +
 *
 */
function linariaPlugin() {
  const linariaCssPlugin: esbuild.Plugin = (linaria as any)({
    babelOptions: {
      presets: ["@babel/preset-typescript", "@babel/preset-react", "@linaria"],
    },
    sourceMap: true,
  });
  return linariaCssPlugin;
}

const defaultEsBuildConfig: esbuild.BuildOptions = {
  bundle: true,
  loader: {
    ".svg": "file",
    ".inline.svg": "text",
    ".png": "dataurl",
    ".jpeg": "dataurl",
    ".ttf": "file",
    ".woff": "file",
    ".woff2": "file",
    ".eot": "file",
  },
  target: ["es2020"],
  format: "esm",
  platform: "browser",
  jsxFactory: "h",
  jsxFragment: "Fragment",
  alias: {
    react: "preact/compat",
    "react-dom": "preact/compat",
  },
  define: {
    __VERSION__: `"${_package.version}"`,
    __GIT_HASH__: `"${GIT_HASH}"`,
  },
};

export interface BuildParams {
  type: "development" | "test" | "production";
  source: {
    assets: Assets | Assets[];
    js: string[];
  };
  public?: string;
  destination: string;
  css: "sass" | "postcss" | "linaria";
  linariaPlugin?: () => esbuild.Plugin;
}

export function computeConfig(params: BuildParams): esbuild.BuildOptions {
  const plugins: Array<esbuild.Plugin> = [
    copyFilesPlugin(params.source.assets),
  ];

  if (params.css) {
    switch (params.css) {
      case "sass": {
        plugins.push(sassPlugin);
        break;
      }
      case "postcss": {
        plugins.push(postCssPlugin);
        break;
      }

      case "linaria": {
        if (params.linariaPlugin) {
          plugins.push(params.linariaPlugin());
        }
        break;
      }

      default: {
        const cssType: never = params.css;
        throw Error(`not supported: ${cssType}`);
      }
    }
  }
  if (!params.type) {
    throw Error(
      `missing build type, it should be "test", "development" or "production"`,
    );
  }

  // if (!params.source.js) {
  //   throw Error(`no javascript entry points, nothing to compile?`);
  // }
  if (!params.destination) {
    throw Error(`missing destination folder`);
  }

  return {
    ...defaultEsBuildConfig,
    entryPoints: params.source.js,
    publicPath: params.public,
    outdir: params.destination,
    treeShaking: true,
    minify: false, //params.type === "production",
    sourcemap: true, //params.type !== "production",
    define: {
      ...defaultEsBuildConfig.define,
      "process.env.NODE_ENV": JSON.stringify(params.type),
    },
    plugins,
  };
}

/**
 * Build sources for prod environment
 */
export async function build(config: BuildParams) {
  const res = await esbuild.build(computeConfig(config));
  fs.writeFileSync(`${config.destination}/version.txt`, `${_package.version}`);
  return res;
}

const LIVE_RELOAD_SCRIPT =
  "./node_modules/@gnu-taler/web-util/lib/live-reload.mjs";

/**
 * Do startup for development environment
 */
export function initializeDev(
  config: BuildParams,
): () => Promise<esbuild.BuildResult> {
  function buildDevelopment() {
    const result = computeConfig(config);
    result.inject = [LIVE_RELOAD_SCRIPT];
    return esbuild.build(result);
  }
  return buildDevelopment;
}
