--[[
	fuzzydir / by sibwaf / https://github.com/sibwaf/mpv-scripts

	Allows using "**" wildcards in sub-file-paths and audio-file-paths
    so you don't have to specify all the possible directory names.

    Basically, allows you to do this and never have the need to edit any paths ever again:
    audio-file-paths = **
    sub-file-paths = **

	MIT license - do whatever you want, but I'm not responsible for any possible problems.
	Please keep the URL to the original repository. Thanks!
]]

--[[
    Configuration:

    # enabled

    Determines whether the script is enabled or not

    # max_search_depth

    Determines the max depth of recursive search, should be >= 1

    Examples for "sub-file-paths = **":
    "max_search_depth = 1" => mpv will be able to find [xyz.ass, subs/xyz.ass]
    "max_search_depth = 2" => mpv will be able to find [xyz.ass, subs/xyz.ass, subs/moresubs/xyz.ass]

    Please be careful when setting this value too high as it can result in awful performance or even stack overflow


    # discovery_threshold

    fuzzydir will skip paths which contain more than discovery_threshold directories in them

    This is done to keep at least some garbage from getting into *-file-paths properties in case of big collections:
    - dir1 <- will be ignored on opening video.mp4 as it's probably unrelated to the file
    - ...
    - dir999 <- will be ignored
    - video.mp4

    Use 0 to disable this behavior completely


    # use_powershell

    fuzzydir will use PowerShell to traverse directories when it's available

    Can be faster in some cases, but can also be significantly slower
]]

local msg = require 'mp.msg'
local utils = require 'mp.utils'
local options = require 'mp.options'

o = {
    enabled = true,
    max_search_depth = 3,
    discovery_threshold = 10,
    use_powershell = false,
}
options.read_options(o, _, function() end)

----------

local default_audio_paths = mp.get_property_native("options/audio-file-paths")
local default_sub_paths = mp.get_property_native("options/sub-file-paths")

function foreach(list, action)
    for _, item in pairs(list) do
        action(item)
    end
end

function starts_with(str, prefix)
    return string.sub(str, 1, string.len(prefix)) == prefix
end

function ends_with(str, suffix)
    return suffix == "" or string.sub(str, -string.len(suffix)) == suffix
end

function add_all(to, from)
    for index, element in pairs(from) do
        table.insert(to, element)
    end
end

function contains(t, e)
    for index, element in pairs(t) do
        if element == e then
            return true
        end
    end
    return false
end

function normalize(path)
    if path == "." then
        return ""
    end

    if starts_with(path, "./") or starts_with(path, ".\\") then
        path = string.sub(path, 3, -1)
    end
    if ends_with(path, "/") or ends_with(path, "\\") then
        path = string.sub(path, 1, -2)
    end

    return path
end

function call_command(command)
    local command_string = ""
    for _, part in pairs(command) do
        command_string = command_string .. part .. " "
    end

    msg.trace("Calling external command:", command_string)

    local process = mp.command_native({
        name = "subprocess",
        playback_only = false,
        capture_stdout = true,
        capture_stderr = true,
        args = command,
    })

    if process.status ~= 0 then
        msg.verbose("External command failed with status " .. process.status .. ": " .. command_string)
        if process.stderr ~= "" then
            msg.debug(process.stderr)
        end

        return nil
    end

    local result = {}
    for line in string.gmatch(process.stdout, "([^\r\n]+)") do
        table.insert(result, line)
    end
    return result
end

-- Platform-dependent optimization

local powershell_version = nil
if o.use_powershell then
    powershell_version = call_command({
        "powershell",
        "-NoProfile",
        "-Command",
        "$Host.Version.Major",
    })
end
if powershell_version ~= nil then
    powershell_version = tonumber(powershell_version[1])
end
if powershell_version == nil then
    powershell_version = -1
end
msg.debug("PowerShell version", powershell_version)

function fast_readdir(path)
    if powershell_version >= 3 then
        msg.trace("Scanning", path, "with PowerShell")
        result = call_command({
            "powershell",
            "-NoProfile",
            "-Command",
            [[
            $dirs = Get-ChildItem -LiteralPath ]] .. string.format("%q", path) .. [[ -Directory
            foreach($dir in $dirs) {
                $u8clip = [System.Text.Encoding]::UTF8.GetBytes($dir.Name)
                [Console]::OpenStandardOutput().Write($u8clip, 0, $u8clip.Length)
                Write-Host ""
            } ]],
        })
        msg.trace("Finished scanning", path, "with PowerShell")
        return result
    end

    msg.trace("Scanning", path, "with default readdir")
    result = utils.readdir(path, "dirs")
    msg.trace("Finished scanning", path, "with default readdir")
    return result
end

-- Platform-dependent optimization end

function traverse(search_path, current_path, level, cache)
    local full_path = utils.join_path(search_path, current_path)

    if level > o.max_search_depth then
        msg.trace("Traversed too deep, skipping scan for", full_path)
        return {}
    end

    if cache[full_path] ~= nil then
        msg.trace("Returning from cache for", full_path)
        return cache[full_path]
    end

    local result = {}

    local discovered_paths = fast_readdir(full_path)
    if discovered_paths == nil then
        -- noop
        msg.debug("Unable to scan " .. full_path .. ", skipping")
    elseif o.discovery_threshold > 0 and #discovered_paths > o.discovery_threshold then
        -- noop
        msg.debug("Too many directories in " .. full_path .. ", skipping")
    else
        for _, discovered_path in pairs(discovered_paths) do
            local new_path = utils.join_path(current_path, discovered_path)

            table.insert(result, new_path)
            add_all(result, traverse(search_path, new_path, level + 1, cache))
        end
    end

    cache[full_path] = result

    return result
end

function explode(raw_paths, search_path, cache)
    local result = {}
    for _, raw_path in pairs(raw_paths) do
        local parent, leftover = utils.split_path(raw_path)
        if leftover == "**" then
            msg.trace("Expanding wildcard for", raw_path)
            table.insert(result, parent)
            add_all(result, traverse(search_path, parent, 1, cache))
        else
            msg.trace("Path", raw_path, "doesn't have a wildcard, keeping as-is")
            table.insert(result, raw_path)
        end
    end

    local normalized = {}
    for index, path in pairs(result) do
        local normalized_path = normalize(path)
        if not contains(normalized, normalized_path) and normalized_path ~= "" then
            table.insert(normalized, normalized_path)
        end
    end

    return normalized
end

function explode_all()
    if not o.enabled then return end
    msg.debug("max_search_depth = ".. o.max_search_depth .. ", discovery_threshold = " .. o.discovery_threshold)

    local video_path = mp.get_property("path")
    local search_path, _ = utils.split_path(video_path)
    msg.debug("search_path = " .. search_path)

    local cache = {}

    foreach(default_audio_paths, function(it) msg.debug("audio-file-paths:", it) end)
    local audio_paths = explode(default_audio_paths, search_path, cache)
    foreach(audio_paths, function(it) msg.debug("Adding to audio-file-paths:", it) end)
    mp.set_property_native("options/audio-file-paths", audio_paths)

    msg.verbose("Done expanding audio-file-paths")

    foreach(default_sub_paths, function(it) msg.debug("sub-file-paths:", it) end)
    local sub_paths = explode(default_sub_paths, search_path, cache)
    foreach(sub_paths, function(it) msg.debug("Adding to sub-file-paths:", it) end)
    mp.set_property_native("options/sub-file-paths", sub_paths)

    msg.verbose("Done expanding sub-file-paths")

    msg.debug("Done expanding paths")
end

mp.add_hook("on_load", 50, explode_all)
