
import { resolve, join, isAbsolute, relative } from 'path'
import { existsSync, statSync, mkdirSync, readdirSync, copyFileSync, mkdir, rmSync } from 'fs';
import { builtAssetsDirectory, tryLoadProjectConfig } from './config.js';
import { copyFilesSync } from '../common/files.js';
import { needleBlue, needleDim, needleLog, needleSupportsColor } from './logging.js';

const pluginName = "needle-copy-files";


/** Copy files on build from assets to dist.
 * @param {"build" | "serve"} command
 * @param {import('../types/needleConfig').needleMeta | null | undefined} config
 * @param {import('../types').userSettings} userSettings
 * @returns {import('vite').Plugin | null}
 */
export function needleCopyFiles(command, config, userSettings) {

    if (config?.noCopy === true || userSettings?.noCopy === true) {
        return null;
    }

    return {
        name: 'needle-copy-files',
        // explicitly don't enforce post or pre because we want to run before the build-pipeline plugin
        enforce: "pre",
        buildStart() {
            return run("start", config);
        },
        closeBundle() {
            return run("end", config);
        },
    }
}

/**
 * @param {"start" | "end"} buildstep
 * @param {import('../types').userSettings} config
 */
async function run(buildstep, config) {
    const copyIncludesFromEngine = config?.copyIncludesFromEngine ?? true;
    const copied = { count: 0, bytes: 0 };

    const baseDir = process.cwd();
    const override = buildstep === "start";
    const supportsColor = needleSupportsColor();
    const key = (text) => supportsColor ? needleBlue(text) : text;
    const rel = (pathValue) => {
        const result = relative(baseDir, pathValue).replaceAll("\\", "/");
        if (!result || result.length === 0) return ".";
        return result;
    };

    let assetsDirName = "assets";
    let outdirName = "dist";

    const needleConfig = tryLoadProjectConfig();
    if (needleConfig) {
        assetsDirName = needleConfig.assetsDirectory || assetsDirName;
        while (assetsDirName.startsWith('/')) assetsDirName = assetsDirName.substring(1);

        if (needleConfig.buildDirectory)
            outdirName = needleConfig.buildDirectory;
    }

    // For the "end" step, the first log line carries a leading newline to visually
    // separate this section from the build-pipeline output above it.
    let firstLog = true;
    const logLine = (msg) => {
        needleLog(pluginName, msg, "log", {
            dimBody: false,
            leadingNewline: firstLog && buildstep === "end",
        });
        firstLog = false;
    };

    if (copyIncludesFromEngine !== false) {
        // copy include from engine
        const engineIncludeDir = resolve(baseDir, 'node_modules', '@needle-tools', 'engine', 'src', 'include');
        if (existsSync(engineIncludeDir)) {
            logLine(needleDim(`Engine include: ${rel(engineIncludeDir)} → ${rel(resolve(baseDir, 'include'))}`));
            const projectIncludeDir = resolve(baseDir, 'include');
            copyFilesSync(engineIncludeDir, projectIncludeDir, true, copied);
        }
    }

    const outDir = resolve(baseDir, outdirName);
    if (!existsSync(outDir)) {
        mkdirSync(outDir, { recursive: true });
    }

    // copy a list of files or directories declared in build.copy = [] in the needle.config.json
    /*
    "build": {
        "copy": ["myFolder", "myFile.txt"]
    }
    */
    if (needleConfig?.build?.copy) {
        const arr = needleConfig.build.copy;
        for (let i = 0; i < arr.length; i++) {
            const entry = arr[i];
            if (Array.isArray(entry)) {
                console.log("WARN: build.copy can only contain string paths to copy to. Found array instead.");
                continue;
            }
            const src = resolve(baseDir, entry);
            const dest = resolvePath(outDir, entry);
            if (existsSync(src) && dest) {
                logLine(`Configured copy: ${rel(src)} → ${rel(dest)}`);
                copyFilesSync(src, dest, override, copied);
            }
        }
    }

    // copy assets dir
    const assetsDir = resolve(baseDir, assetsDirName);
    if (existsSync(assetsDir)) {
        const targetDir = resolve(outDir, 'assets');
        // ensure that the target directory exists and is cleared if it already exists
        // otherwise we might run into issues where the build pipeline is running for already compressed files
        if (override && existsSync(targetDir)) {
            logLine(needleDim(`Clearing target directory "${rel(targetDir)}"`));
            rmSync(targetDir, { recursive: true, force: true });
        }
        logLine(`${key("Copying assets")}: ${rel(assetsDir)} → ${rel(targetDir)}`);
        copyFilesSync(assetsDir, targetDir, override, copied);
    }
    else logLine(`No assets directory found. Skipping copy of ${assetsDirName} resolved to ${rel(assetsDir)}`);

    // copy include dir
    const includeDir = resolve(baseDir, 'include');
    if (existsSync(includeDir)) {
        const targetDir = resolve(outDir, 'include');
        logLine(needleDim(`Include: ${rel(includeDir)} → ${rel(targetDir)}`));
        copyFilesSync(includeDir, targetDir, override, copied);
    }

    logLine(`${key("Copied files")}: ${copied.count}, ${formatBytes(copied.bytes || 0)}`);
    if (buildstep === "end") {
        logLine("✨ Happy creating! 🌵");
    }
}

function formatBytes(bytes) {
    const value = Number(bytes || 0);
    if (value < 1024) return `${value} B`;
    if (value < 1024 * 1024) return `${(value / 1024).toFixed(2)} kB`;
    if (value < 1024 * 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MB`;
    return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}

/** resolves relative or absolute paths to a path inside the out directory
 * for example D:/myFile.txt would resolve to outDir/myFile.txt
 * wherereas "some/relative/path" would become outDir/some/relative/path
 * @param {string} outDir
 * @param {string} pathValue
 */
function resolvePath(outDir, pathValue) {
    if (isAbsolute(pathValue)) {
        var exists = existsSync(pathValue);
        if (!exists) return null;
        var stats = exists && statSync(pathValue);
        if (stats.isDirectory()) {
            const dirName = pathValue.replaceAll('\\', '/').split('/').pop();
            if (!dirName) return null;
            return resolve(outDir, dirName);
        }
        const fileName = pathValue.replaceAll('\\', '/').split('/').pop();
        if (!fileName) return null;
        return resolve(outDir, fileName);
    }
    return resolve(outDir, pathValue);
}