import { normalize, resolve, sep, dirname } from "node:path";
import { tmpdir } from "node:os";
import { readFile, writeFile, rm, mkdtemp, mkdir, stat } from "node:fs/promises";
import { spawn } from "node:child_process";
import { argv0 } from "node:process";
import { platform } from "node:process";
import * as nodeUtils from "node:util";

export const isWindows = platform === "win32";

export const ASYNC_WASI_IMPORTS = [
    "wasi:io/poll#poll",
    "wasi:io/poll#[method]pollable.block",
    "wasi:io/streams#[method]input-stream.blocking-read",
    "wasi:io/streams#[method]input-stream.blocking-skip",
    "wasi:io/streams#[method]output-stream.blocking-flush",
    "wasi:io/streams#[method]output-stream.blocking-write-and-flush",
    "wasi:io/streams#[method]output-stream.blocking-write-zeroes-and-flush",
    "wasi:io/streams#[method]output-stream.blocking-splice",
];

export const ASYNC_WASI_EXPORTS = ["wasi:cli/run#run", "wasi:http/incoming-handler#handle"];

export const DEFAULT_ASYNC_MODE = "sync";

/** Path of WIT files by default when one is not specified */
export const DEFAULT_WIT_PATH = "./wit";

let _showSpinner = false;
export function setShowSpinner(val) {
    _showSpinner = val;
}
export function getShowSpinner() {
    const showSpinner = _showSpinner;
    _showSpinner = false;
    return showSpinner;
}

export function sizeStr(num) {
    num /= 1024;
    if (num < 1000) {
        return `${fixedDigitDisplay(num, 4)} KiB`;
    }
    num /= 1024;
    if (num < 1000) {
        return `${fixedDigitDisplay(num, 4)} MiB`;
    }
}

export function fixedDigitDisplay(num, maxChars) {
    const significantDigits = String(num).split(".")[0].length;
    let str;
    if (significantDigits >= maxChars - 1) {
        str = String(Math.round(num));
    } else {
        const decimalPlaces = maxChars - significantDigits - 1;
        const rounding = 10 ** decimalPlaces;
        str = String(Math.round(num * rounding) / rounding);
    }
    if (maxChars - str.length < 0) {
        return str;
    }
    return " ".repeat(maxChars - str.length) + str;
}

/**
 * Generate tabular output
 *
 * @param {string[][]} rows - data to put in rows
 * @param {string[]} align - alignment of columns
 * @returns string
 */
export function table(rows, align = []) {
    if (rows.length === 0) {
        return "";
    }
    const colLens = rows.reduce(
        (maxLens, cur) => maxLens.map((len, i) => Math.max(len, cur[i].length)),
        rows[0].map((cell) => cell.length),
    );
    let outTable = "";
    for (const row of rows) {
        for (const [i, cell] of row.entries()) {
            if (align[i] === "right") {
                outTable += " ".repeat(colLens[i] - cell.length) + cell;
            } else {
                outTable += cell + " ".repeat(colLens[i] - cell.length);
            }
        }
        outTable += "\n";
    }
    return outTable;
}

/**
 * Securely creates a temporary directory and returns its path.
 *
 * The new directory is created using `fsPromises.mkdtemp()`.
 *
 * @returns {Promise<string>} A `Promise` that resovles to a created temporary directory path
 */
export async function getTmpDir() {
    return await mkdtemp(normalize(tmpdir() + sep));
}

/**
 * Read a given file, throwing a formatted error if one occurs
 *
 * @param {string} filePath - path to teh file to read
 * @param {encoding} encoding - file encoding
 * @returns {Promise<Buffer>} A promise that resolves to the contents of the file
 */
async function readFileCli(filePath, encoding) {
    try {
        return await readFile(filePath, encoding);
    } catch {
        throw `Unable to read file ${styleText("bold", filePath)}`;
    }
}
export { readFileCli as readFile };

/**
 * Spawn a command that processes a given wasm binary bytes with some
 * command.
 *
 * The command invocations that are generated by this function
 * take the following form:
 *
 * ```
 * <cmd> <input wasm file> <...arguments> <output wasm file>
 * ```
 *
 * @param {string} cmd - the command to run
 * @param {Buffer<ArrayBufferLike>} inputWasmBytes - bytes that of the input WebAssembly binary
 * @param {string[]} args - arguments to pass to the command (after the input file and before the output file)
 * @returns {Promise<Buffer<ArrayBufferLike>>} A `Promise` that resolves when the command has exited
 */
export async function spawnIOTmp(cmd, inputWasmBytes, args) {
    const tmpDir = await getTmpDir();
    try {
        const inFile = resolve(tmpDir, "in.wasm");
        let outFile = resolve(tmpDir, "out.wasm");

        await writeFile(inFile, inputWasmBytes);

        const cp = spawn(argv0, [cmd, inFile, ...args, outFile], {
            stdio: "pipe",
        });

        let stderr = "";
        const p = new Promise((resolve, reject) => {
            cp.stderr.on("data", (data) => (stderr += data.toString()));
            cp.on("error", (e) => {
                reject(e);
            });
            cp.on("exit", (code) => {
                if (code === 0) {
                    resolve();
                } else {
                    reject(stderr);
                }
            });
        });

        await p;
        var output = await readFile(outFile);
        return output;
    } finally {
        await rm(tmpDir, { recursive: true });
    }
}

/**
 * Given an object that has file names as keys and file contents as values,
 * write out the files to a their locations.
 *
 * This function also prints out the files that were written
 *
 * @param {Record<string, string>} files - object which contains files to be written out
 * @param {boolean} summaryTitle - whether to print the summary after writing out files
 * @returns {Promise<void>>} A `Promise` that resolves when the fiels are all written
 *
 */
export async function writeFiles(files, summaryTitle) {
    await Promise.all(
        Object.entries(files).map(async ([filePath, contents]) => {
            await mkdir(dirname(filePath), { recursive: true });
            await writeFile(filePath, contents);
        }),
    );
    if (!summaryTitle) {
        return;
    }

    let rows = Object.entries(files).map(([name, source]) => [
        ` - ${styleText("italic", name)}  `,
        `${styleText(["black", "italic"], sizeStr(source.length))}`,
    ]);
    console.log(`
  ${styleText("bold", summaryTitle + ":")}

${table(rows)}`);
}

/**
 * Resolve the deafult WIT path, given a possibly
 *
 * @param {string | undefined} [witPath]
 * @returns {Promise<string>}
 */
export async function resolveDefaultWITPath(witPath) {
    if (witPath) {
        return witPath;
    }

    // Use a default/standard current-folder WIT directory (wit) if we can find it
    const witDirExists = await stat(DEFAULT_WIT_PATH)
        .then((p) => p.isDirectory())
        .catch(() => false);
    if (!witDirExists) {
        throw new Error("Failed to determine WIT directory, please specify WIT directory argument");
    }
    witPath = resolve(DEFAULT_WIT_PATH);
    console.error(`no WIT directory specified, using detected WIT directory @ [${DEFAULT_WIT_PATH}]`);
    return witPath;
}

/**
 * Partial polyfill for 'node:util' `styleText()`
 *
 * @param {string | string[]} styles - styles to apply to the given text
 * @param {string} text - text that should be styled
 * @returns {string} The styled string
 */
export function styleText(styles, text) {
    if (nodeUtils.styleText) {
        return nodeUtils.styleText(styles, text);
    }
    return text;
}
