// @ts-check
import path from 'path';
import { loadConfig, tryLoadProjectConfig } from './config.js';
import { getPosterPath } from './poster.js';
import * as crypto from 'crypto';
import { existsSync, readFileSync, statSync } from 'fs';
import { fileURLToPath } from 'url';
import { needleLog } from './logging.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/** @type {Set<string>} */
const filesUsingHotReload = new Set();
let assetsDirectory = "";

/**
 * @param {"build" | "serve"} command
 * @param {import('../types/needleConfig').needleMeta | null} config
 * @param {import('../types').userSettings} userSettings
 * @returns {import('vite').Plugin | undefined}
 */
export function needleReload(command, config, userSettings) {
    if (command === "build") return undefined;

    if (userSettings?.noReload === true) return undefined;


    let isUpdatingConfig = false;
    const updateConfig = async () => {
        if (isUpdatingConfig) return;
        isUpdatingConfig = true;
        const res = await loadConfig();
        isUpdatingConfig = false;
        if (res) config = /** @type {import('../types/needleConfig').needleMeta} */ (res);
    }

    const projectConfig = tryLoadProjectConfig();
    assetsDirectory = path.resolve(projectConfig?.assetsDirectory || "assets");

    const buildDirectory = projectConfig?.buildDirectory?.length ? process.cwd().replaceAll("\\", "/") + "/" + projectConfig?.buildDirectory : "";
    if (buildDirectory?.length) {
        const relativeBuildDirectory = path.relative(process.cwd(), buildDirectory).replaceAll("\\", "/") || ".";
        setTimeout(() => needleLog("needle-reload", "Build directory: " + relativeBuildDirectory), 100);
    }

    // These ignore patterns will be injected into user config to better control vite reloading
    const ignorePatterns = ["dist/**/*", "src/generated/*", "**/package~/**/codegen/**/*", "**/codegen/register_types.js"];
    if (projectConfig?.buildDirectory?.length) ignorePatterns.push(`${projectConfig?.buildDirectory}/**/*`);
    if (projectConfig?.codegenDirectory?.length) ignorePatterns.push(`${projectConfig?.codegenDirectory}/**/*`);

    return {
        name: 'needle:reload',
        /** @param {import('vite').UserConfig} config */
        config(config) {
            if (!config.server) config.server = { watch: { ignored: [] } };
            else if (!config.server.watch) config.server.watch = { ignored: [] };
            else if (!config.server.watch?.ignored) config.server.watch.ignored = [];
            for (const pattern of ignorePatterns)
                // @ts-ignore - watch.ignored is guaranteed to be string[] by the guards above
                config.server.watch.ignored.push(pattern);
            if (userSettings?.debug === true)
                setTimeout(() => needleLog("needle-reload", "Updated server ignore patterns: " + JSON.stringify(config.server?.watch?.ignored)), 100);
        },
        /** @param {import('vite').HmrContext & {buildDirectory?: string}} args */
        handleHotUpdate(args) {
            args.buildDirectory = buildDirectory;
            return handleReload(args);
        },
        /** @param {string} src @param {string} id */
        transform(src, id) {
            if (!id.includes(".ts")) return undefined;
            updateConfig();
            if (config?.allowHotReload === false) return undefined;
            if (userSettings?.allowHotReload === false) return undefined;
            src = insertScriptRegisterHotReloadCode(src, id);
            return insertScriptHotReloadCode(src, id);
        },
        transformIndexHtml: {
            order: 'pre',
            /** @param {string} html @param {unknown} _ */
            handler(html, _) {
                if (config?.allowHotReload === false) return html;
                if (userSettings?.allowHotReload === false) return html;
                const file = path.join(__dirname, 'reload-client.js');
                const content = readFileSync(file, 'utf8');
                return {
                    html,
                    tags: [
                        {
                            tag: 'script',
                            attrs: {
                                type: 'module',
                            },
                            children: content,
                            injectTo: 'body',
                        },
                    ]
                }
            },
        }
    }
}


/** @type {string[]} */
const ignorePatterns = [];
const ignoreRegex = new RegExp(ignorePatterns.join("|"));

let lastReloadTime = 0;
const posterPath = getPosterPath();
let reloadIsScheduled = false;
const lockFileName = "needle.lock";

/** @param {import('vite').ViteDevServer} server */
function getHot(server) {
    return server.hot ?? server.ws;
}

/** @param {import('vite').ViteDevServer} server @param {string} [file] */
function notifyClientWillReload(server, file) {
    console.log("Send reload notification");
    getHot(server).send('needle:reload', { type: 'will-reload', file: file });
}

/**
 * @param {import('vite').HmrContext & {buildDirectory?: string}} param0
 * @returns {Promise<any>}
 */
async function handleReload({ file, server, modules: _modules, read, buildDirectory }) {

    // dont reload the full page on css changes
    const isCss = file.endsWith(".css") || file.endsWith(".scss") || file.endsWith(".sass")
    if (isCss) return null;

    // Dont reload the whole server when a file that is using hot reload changes
    if (filesUsingHotReload.has(file)) {
        console.log("File is using hot reload: " + file);
        return null;
    }

    // the poster is generated after the server has started
    // we dont want to specifically handle the png or webp that gets generated
    if (file.includes(posterPath)) return null;

    if (file.endsWith("build.log")) return null;

    // if (file.endsWith("/codegen/register_types.js" || file.endsWith("/generated/register_types.js"))) {
    //     console.log("Ignore change in codegen file: " + file);
    //     return [];
    // }

    // This was a test for ignoring files via regex patterns
    // instead of relying on the vite server watch ignore array
    // we could here also match paths that we know we dont want to track
    if (ignorePatterns.length > 0 && ignoreRegex.test(file)) {
        console.log("Ignore change in file: " + getFileNameLog(file));
        return [];
    }

    // Ignore files changing in output directory
    // this happens during build time when e.g. dist is not ignored in vite config
    // we still dont want to reload the local server in that case
    if (buildDirectory?.length) {
        const dir = path.dirname(file).replaceAll("\\", "/");
        if (dir.startsWith(buildDirectory)) {
            console.log("Ignore change in build directory: " + getFileNameLog(file));
            return [];
        }
    }

    // Check if codegen files actually changed their content
    // this will return false if its the first update
    // meaning if its the first export after the server starts those will not trigger a reload
    const shouldCheckIfContentChanged = file.includes("/codegen/") || file.includes("/generated/") || file.endsWith("gen.js");// || file.endsWith(".glb") || file.endsWith(".gltf") || file.endsWith(".bin");
    if (shouldCheckIfContentChanged) {
        if (reloadIsScheduled) {
            return [];
        }
        if (await testIfFileContentChanged(file, read) === false) {
            console.log("File content didnt change: " + getFileNameLog(file));
            return [];
        }
    }

    // these are known file types we export from integrations
    const knownExportFileTypes = [ ".glb", ".gltf", ".bin", "exr", ".ktx2", ".mp3", ".ogg", ".mp4", ".webm" ];
    if (!knownExportFileTypes.some((type) => file.endsWith(type)))
        return null;

    // we only care about exports into "assets"
    if (!path.resolve(file).startsWith(assetsDirectory))
        return null;

    if (file.endsWith(".svelte") || file.endsWith(".vue") || file.endsWith(".ts") || file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".tsx"))
        return null;

    if (file.endsWith(lockFileName)) return null;
    /** @type {number} */
    let fileSize = 0;
    const isGlbOrGltfFile = file.endsWith(".glb") || file.endsWith(".bin");
    if (isGlbOrGltfFile && existsSync(file)) {
        fileSize = statSync(file).size;
        // the file is about to be created/written to
        if (fileSize <= 0) {
            // console.log("> File is changing: " + getFileNameLog(file));
            return null;
        }
    }

    console.log("> Detected file change: ", getFileNameLog(file) + " (" + ((fileSize / (1024 * 1024)).toFixed(1)) + " MB)");
    notifyClientWillReload(server);
    scheduleReload(server);
    return [];
}


/** @param {import('vite').ViteDevServer} server @param {number} [level] */
async function scheduleReload(server, level = 0) {
    if (reloadIsScheduled && level === 0) return;
    reloadIsScheduled = true;

    const lockFile = path.join(process.cwd(), lockFileName);
    if (existsSync(lockFile)) {
        if (level === 0)
            console.log("Lock file exists, waiting for export to finish...");
        setTimeout(() => scheduleReload(server, level += 1), 300);
        return;
    }

    reloadIsScheduled = false;

    const timeDiff = Date.now() - lastReloadTime;
    if (timeDiff < 1000) {
        // Sometimes file changes happen immediately after triggering a reload 
        // we dont want to reload again in that case
        console.log("Ignoring reload, last reload was too recent", timeDiff);
        return;
    }

    lastReloadTime = Date.now();
    const readableTime = new Date(lastReloadTime).toLocaleTimeString();
    console.log("< Reloading... " + readableTime)
    getHot(server).send({
        type: 'full-reload',
        path: '*'
    });
}

const projectDirectory = process.cwd().replaceAll("\\", "/");

/** @param {string} file @returns {string} */
function getFileNameLog(file) {
    if (file.startsWith(projectDirectory)) {
        return file.substring(projectDirectory.length);
    }
    return file;
}

/** @type {Map<string, string>} */
const hashes = new Map();
const hash256 = crypto.createHash('sha256');

/** @param {string} file @param {(file?: string) => string | Promise<string>} read @returns {Promise<boolean>} */
async function testIfFileContentChanged(file, read) {
    let content = await read(file);
    content = removeVersionQueryArgument(content);

    const hash = hash256.copy();
    hash.update(content);
    // compare if hash string changed
    const newHash = hash.digest('hex');
    const oldHash = hashes.get(file);
    if (oldHash !== newHash) {
        // console.log("Update hash for file: " + getFileNameLog(file) + " to " + newHash);
        hashes.set(file, newHash);
        // if its the first update we dont want to trigger a reload
        if (!oldHash) return false;
        return true;
    }
    return false;
}
/** @param {string} content @returns {string} */
function removeVersionQueryArgument(content) {
    if (typeof content === "string") {
        // Some codegen files include hashes for loading glb files (e.g. ?v=213213124)
        // Or context.hash = "54543453"
        // We dont want to use those hashes for detecting if the file changed
        let res = content.replaceAll(/(v=[0-9]+)/g, "");
        res = res.replaceAll(/(hash = \"[0-9]+\")/g, "hash = \"\"");
        return res;
    }
    return content;
}



/** @param {string} src @param {string} filePath @returns {string} */
function insertScriptRegisterHotReloadCode(src, filePath) {

    // We only want to inject the hot reload code in the needle-engine root file
    if(!filePath.includes("/src/needle-engine.ts")) {
        return src;
    }
    console.log("[Needle HMR] Hot reload is enabled");
    // this code let's the engine know that we are in hot reload mode
    const code = `
globalThis.NEEDLE_HOT_RELOAD_ENABLED = true;
`
    return code + src;
}


const HOT_RELOAD_START_MARKER = "NEEDLE_HOT_RELOAD_BEGIN";
const HOT_RELOAD_END_MARKER = "NEEDLE_HOT_RELOAD_END";

/** @param {string} src @param {string} filePath @returns {{code: string, map: null} | undefined} */
function insertScriptHotReloadCode(src, filePath) {
    if (filePath.includes("engine_hot_reload")) return undefined;
    if (filePath.includes(".vite")) return undefined;

    const originalFilePath = filePath;

    // default import path when outside package
    let importPath = "@needle-tools/engine";

    if (filePath.includes("package~/engine")) {
        // convert local dev path to project node_modules path 
        const folderName = "package~";
        const startIndex = filePath.indexOf(folderName);
        const newPath = process.cwd() + "/node_modules/@needle-tools/engine" + filePath.substring(startIndex + folderName.length);
        filePath = newPath;
    }

    if (filePath.includes("@needle-tools/engine")) {
        // only make engine components hot reloadable
        if (!filePath.includes("engine/engine-components"))
            return undefined;
        // make import path from engine package
        const fullPathToHotReload = process.cwd() + "/node_modules/@needle-tools/engine/src/engine/engine_hot_reload.ts";
        // console.log(fullPathToHotReload);
        const fileDirectory = path.dirname(filePath);
        // console.log("DIR", fileDirectory)
        const relativePath = path.relative(fileDirectory, fullPathToHotReload);
        importPath = relativePath.replace(/\\/g, "/");
        // console.log("importPath: ", importPath);
    }

    // console.log(importPath, ">", filePath);


    const code = `

// ${HOT_RELOAD_START_MARKER}
// Inserted by needle reload plugin (vite)
//@ts-ignore
if (import.meta.hot) {
    import("${importPath}").then(mod => {
        const applyHMRChanges = mod.applyHMRChanges;
        //@ts-ignore
        import.meta.hot.accept((newModule, oldModule) => {
            if (newModule) {
                const success = applyHMRChanges(newModule, oldModule);
                if(success === false){
                    //@ts-ignore
                    import.meta.hot.invalidate()
                }
            }
        })
    });
}
// ${HOT_RELOAD_END_MARKER}
`

    if (!filesUsingHotReload.has(originalFilePath))
        filesUsingHotReload.add(originalFilePath);

    return {
        code: src + "\n" + code,
        map: null
    }

}