#!/usr/bin/env -S deno run --allow-all
const { FileSystem } = await import(`https://deno.land/x/quickr@0.6.3/main/file_system.js`)
const { OperatingSystem } = await import(`https://deno.land/x/quickr@0.6.3/main/operating_system.js`)
const { Console } = await import(`https://deno.land/x/quickr@0.6.3/main/console.js`)
const { run, throwIfFails, zipInto, mergeInto, returnAsString, Timeout, Env, Cwd, Stdin, Stdout, Stderr, Out, Overwrite, AppendTo, checkCommands } = await import("https://deno.land/x/quickr@0.6.3/main/run.js")

function paths() {
    const spliter = OperatingSystem.commonChecks.isWindows ? ";" : ":"
    return Deno.env.get("PATH").split(spliter)
}

if (!OperatingSystem.commonChecks.isWindows) {
    // TODO: need to have it copy the target, patch the copy, and then link to the copy
    //       especially because some of the targets that need to be changed are in nix/store
    

    async function directInfo(sharedObjectPath) {
        let result = (await run`readelf -d ${sharedObjectPath} ${Stdout(returnAsString)}`).replace(/\n$/,"")
        // IMPERFECTION: a malicious/crafted file path could probably cause this to fail
        const chunks = result.match(/(^|\n) 0x[0-9a-f]{16} \((.|\s)*?(?=(\n 0x|$))/g)
        if (!chunks) {
            console.debug(`FAILED for path: ${JSON.stringify(sharedObjectPath)}\ncommand result was:`,result)
            return { needed:[]}
        }
        let rpath = ""
        let runpath = ""
        const needed = []
        const other = {}
        for (const each of chunks) {
            const match = each.match(/(?:^|\n) 0x([0-9a-f]{16}) \((.+?)\)\s+(.+): \[((?:.|\s)+)\]/)
            if (match) {
                const [ _, address, kind, otherName, path ] = match
                if (kind == "NEEDED") {
                    needed.push(path)
                } else if (kind == "RPATH") {
                    rpath = path
                } else if (kind == "RUNPATH") {
                    runpath = path
                }
            }
        }
        
        return {
            rpath,
            runpath,
            needed,
        }
    }
    
    async function forceRpath(sharedObjectPath, newFolder, ldPath) {
        const { rpath, runpath } = await directInfo(sharedObjectPath)
        const sources = ([newFolder, rpath, ldPath, runpath]).filter(each=>each)
        const newRPath = sources.join(":")
        
        // delete runpath
        var {success} = await run`chrpath -d ${sharedObjectPath}`
        if (success) {
            // set rpath
            var { success } = await run`patchelf --force-rpath --set-rpath ${newRPath} ${sharedObjectPath}`
        }
        return success
    }
    
    async function getDirectDynamicLinks(sharedObjectPath, ldPath) {
        let { rpath, runpath, needed } = await directInfo(sharedObjectPath)
        if (runpath) {
            rpath = ""
        }
        if (ldPath) {
            runpath = ldPath + runpath
        }
        const sourcesString = [rpath, runpath].filter(each=>each).join(":")
        // IMPERFECTION: doesn't account for escaping $ORIGIN (not sure how escaping works)
        const sources = sourcesString.replace(/\$ORIGIN/, FileSystem.makeAbsolutePath(FileSystem.parentPath(sharedObjectPath))).split(":")
        const mapping = {}
        for (let each of needed) {
            mapping[each] = null
        }
        for (const eachPath of sources) {
            for (const eachName of needed) {
                if (!mapping[eachName]) {
                    if ((await FileSystem.info(eachName)).exists) {
                        // IMPERFECTION: probably needs particular permissions to count
                        mapping[eachName] = eachPath
                    }
                }
            }
        }
        return mapping
    }
    
    async function hardDynamicLink({ sharedObjectPath, item, newFolder, offLimits }) {
        offLimits = [...(offLimits || new Set(([...offLimits]).map(each=>FileSystem.makeAbsolutePath(each))))]
        const fullPath = FileSystem.makeAbsolutePath(sharedObjectPath)
        if (offLimits.some(each=>fullPath.startsWith(each))) {
            return
        }
        const mapping = await getDirectDynamicLinks(sharedObjectPath)
        for (const [name, path] of Object.entries(mapping)) {
            if (!path && name == item) {
                console.log(`        patching: ${JSON.stringify(name)} for ${JSON.stringify(sharedObjectPath)}`)
                await forceRpath(sharedObjectPath, newFolder)
            } else if (path) {
                // recursively check all children
                await hardDynamicLink({ sharedObjectPath: path, item, newFolder, offLimits })
            }
        }
    }
    var handleBrokenDynamicLinks = async function({ folder, mapping, offLimits }) {
        mapping = mapping||{}
        const promises = []
        const badKeys = new Set()
        for (const eachItem of await FileSystem.listFileItemsIn(folder, { recursively: true })) {
            // find all the shared object files
            if (eachItem.basename.match(/\.so\b/)) {
                console.debug(`    checking:`, eachItem.path)
                const eachMapping = await getDirectDynamicLinks(eachItem.path)
                for (const [linkName, linkedPath] of Object.entries(eachMapping)) {
                    if (!linkedPath) {
                        if (mapping[linkName]) {
                            promises.push(hardDynamicLink({
                                sharedObjectPath: eachItem.path,
                                item: linkName,
                                newFolder: mapping[linkName],
                                offLimits 
                            }))
                        } else {
                            console.log(`        missing: ${linkName}`)
                            badKeys.add(linkName)
                        }
                    }
                }
            }
        }
        await Promise.all(promises)
        return [...badKeys]
    }

    if (OperatingSystem.commonChecks.isMac) {
        console.warn(`This system does not yet support .dylib files (only .so files)`)
        if (!await Console.askFor.yesNo("do you want to continue anyways?")) {
            Deno.exit(1)
        }
    }

    const { missing } = await checkCommands([ "chrpath", "patchelf", "readelf"  ])
    if (missing.length > 0) {
        throw Error(`
            Sorry looks like you're trying to clean up some shared object files, but I
            need access to the following commands:
                ${missing}

            I don't see them, but here's where I looked:
                ${paths().join("                \n")}
        `)
    
        // for MacOS
            // brew install chrpath
            // nixi patchelf
            // nixi binutils
        // dyld_info -dependents "file"
        
    }
} else {
    throw Error(`sorry this function isn't available on your system yet`)
}



// 
// 
// handle the CLI
// 
// 

let mapping = {}
let offLimits = []
try { mapping = JSON.parse(Deno.args[1]) } catch (error) {}
try { offLimits = JSON.parse(Deno.args[2]) } catch (error) {}
const brokenDynamicLinks = await handleBrokenDynamicLinks({
    folder: Deno.args[0],
    mapping,
    offLimits
})
if (!brokenDynamicLinks) {
    console.log(`No broken links found`)
} else {
    console.debug(`discovered brokenDynamicLinks to:`, brokenDynamicLinks)
}