/*
 * This file is part of the ufo utility.
 * Copyright (C) 2018-2021, 2025-2026 Eugene 'Vindex' Stulin
 * Distributed under the BSL 1.0 or the GNU GPL 3.0 or later.
 */

import core.stdc.stdlib;

import std.algorithm :
    canFind, endsWith, filter, map, remove, sort, startsWith, strip;
import std.array : array, empty, replace;
import std.file : exists, mkdirRecurse, readLink, readText;
static import std.file;
import std.getopt;
import std.path : buildNormalizedPath, buildPath, dirName;
import std.process : environment, execute, executeShell, spawnProcess, wait;
import std.stdio;
import std.string : split, splitLines, strip;
import std.typecons;

import amalthea.dataprocessing : trimPrefix;
import amalthea.fs : DesktopEntry;
import amalthea.fileformats;
static import amalthea.fs;
import amalthea.langlocal : initLocalization, getCurrentLanguage, _s;
import amalthea.sys : getAppPath;

import de;
static import toml;

enum formatappsConfig = "/etc/ufo/formatapps.toml";
immutable string appVersion = import("version").strip;
immutable string homeDir;
immutable string helpDir;
immutable string homeConfigDir;
immutable string homeConfig;
immutable string homeFormatConfigDir;
immutable translations = mixin(import("translations.dtxt").strip);
immutable formatConfExampleText = import("examples/formatapps.toml");
immutable string wrongUsingMsg;
immutable string hint;
immutable string mimeAppsList;
string[][string] mimeAppsSections;
bool mimeAppsListExists = false;


shared static this() {
    string currentApp = readLink("/proc/self/exe");
    string shareDir = buildNormalizedPath(dirName(currentApp), "..", "share");
    helpDir = shareDir ~ "/help/";

    initLocalization(translations);
    wrongUsingMsg = "Incorrect use of ufo."._s;
    hint = "See: ufo --help"._s;

    homeDir = environment["HOME"];
    mimeAppsList = buildPath(homeDir, ".config/mimeapps.list");
    if (mimeAppsList.exists) {
        mimeAppsListExists = true;
        mimeAppsSections = getIniSections(mimeAppsList);
    }
    homeConfigDir = buildPath(homeDir, ".config", "ufo");
    homeConfig = buildPath(homeConfigDir, "formatapps.toml");
    homeFormatConfigDir = buildPath(homeConfigDir, "formatapps.toml.d");
    mkdirRecurse(homeConfigDir);
    mkdirRecurse(homeFormatConfigDir);
}


string readFormatConfiguration() {
    string[] configs;
    if (exists(homeConfig)) {
        configs ~= homeConfig;
    }
    configs ~= amalthea.fs.getRegularFiles(homeFormatConfigDir)
        .filter!(f => f.path.endsWith(".toml"))
        .array
        .sort!((a, b) => a.path < b.path)
        .map!(a => a.path)
        .array;
    string fullConfiguration;
    foreach(f; configs) {
        fullConfiguration ~= readText(f) ~ "\n";
    }
    if (exists(formatappsConfig)) {
        string etcConfig = readText(formatappsConfig);
        fullConfiguration ~= etcConfig;
    }
    return fullConfiguration;
}


void showHelp() {
    immutable string app = "ufo";
    immutable lang = getCurrentLanguage();
    string pathToHelpFile = helpDir ~ lang ~ "/" ~ app ~ "/help.txt";
    if (!exists(pathToHelpFile)) {
        pathToHelpFile = helpDir ~ "en_US/" ~ app ~ "/help.txt";
    }
    writeln(readText(pathToHelpFile).strip);
}


void showVersion() {
    writeln(appVersion);
}


int main(string[] args) {
    if (args.length == 2 && args[1] == "--help") {
        showHelp();
        return 0;
    } else if (args.length == 2 && args[1] == "--version") {
        showVersion();
        return 0;
    } else if (args.length == 2 && args[1] == "--make-config") {
        string baseConfig = buildPath(homeConfigDir, "formatapps.toml");
        if (exists(baseConfig)) {
            stderr.writeln(baseConfig ~ ": file exists."._s);
            return 1;
        }
        std.file.write(baseConfig, formatConfExampleText);
        return 0;
    }

    bool useTerminal = false;
    bool useBrowser = false;
    try {
        args.getopt(
            config.caseSensitive,
            "t", &useTerminal,
            "b", &useBrowser
        );
    } catch(GetOptException e) {
        stderr.writeln(e.msg);
        stderr.writeln(wrongUsingMsg);
        stderr.writeln(hint);
        return 1;
    }
    if (!useBrowser) {
        for (size_t i = 1; i < args.length; i++) {
            // check file and remove argument if it does not exist
            if (exists(args[i]) || args[i].canFind("://")) {
                continue;
            }
            stderr.writeln(args[i], ": file not found."._s);
            args = args[0 .. i];
            if (i != args.length) {
                args ~= args[i + 1 .. $];
            }
            i--;
        }
    }
    auto files = args[1 .. $];  // wtihout program name
    if (files.length == 0) {
        stderr.writeln(wrongUsingMsg);
        stderr.writeln(hint);
        return 1;
    }
    try {
        if (useBrowser) {
            openWithBrowser(files);
        } else if (0 != executeByAmaltheaFormat(files, useTerminal)) {
            return executeByMimeType(files);
        }
    } catch (Exception e) {
        stderr.writeln(e.msg);
    }
    return 1;
}


void openWithBrowser(string[] paths) {
    foreach(ref p; paths) {
        if (p.endsWith(".desktop")) {  // for URL-based desktop-files
            try {
                string url = DesktopEntry.getURL(p);
                if (!url.empty) {
                    p = url;
                }
            } catch (Exception e) {}
        }
    }
    string desktopFile = getBrowserByDefault();
    auto cmdOptions = getCmdByDesktopFile(desktopFile, paths);
    spawnProcess(cmdOptions);
}


int executeByMimeType(string[] files) {
    /*
    Additional information about field codes (%f, %F, %u, %U ...):
    https://standards.freedesktop.org/desktop-entry-spec/latest/ar01s07.html
    */

    string[][] rawCommands;   // commands with fields "%f", "%F"
    string[][] finalCommands; // prepared commands with opening files

    int exitCode = 0;

    const couldNot = "ufo could not determine the program to open the file."._s;

    foreach(i, f; files) {
        string[] arguments;
        if (amalthea.fs.isSymlink(f)) {
            f = amalthea.fs.readLinkRecurse(f);
        }
        if (amalthea.fs.isDir(f)) {
            string opener = de.chooseOpenerByDefault();
            arguments = opener.split.array;
        } else if (amalthea.fs.isRegularFile(f)) {
            arguments = getCmdLineOptionsByFile(f);
        }
        if (arguments.empty) {
            stderr.writeln(f ~ ": " ~ couldNot);
            exitCode = 2;
            continue;
        }
        exitCode = 0;
        if (!canFind(rawCommands, arguments)) {
            rawCommands ~= arguments;
            string[] newFinalCommand = arguments ~ f;
            finalCommands ~= newFinalCommand;
        } else {
            if (canFind(arguments, "%F")) {
                string program = arguments[0];
                foreach(ref finalCommand; finalCommands) {
                    auto currProgram = finalCommand[0];
                    if (currProgram == program) {
                        // expand file list for this program
                        finalCommand ~= f;
                        break;
                    }
                }
            } else {
                finalCommands ~= arguments ~ [f];
            }
        }
    }
    foreach(ref cmd; finalCommands) {
        cmd = remove!(`a == "%F" || a == "%f"`)(cmd);
    }
    foreach(command; finalCommands) {
        writeln(command);
        spawnProcess(command);
    }
    return exitCode;
}


string[] getCmdLineOptionsByFile(string f)
in {
    assert(amalthea.fs.isRegularFile(f));
}
do {
    string mimeType = amalthea.fileformats.contentTypeGuess(f)[0];
    string[] cmdOptions;
    if (!mimeType.empty) {
        string desktopFile = getAppDesktopFilesForMimeType(mimeType);
        if (!desktopFile.empty) {
            desktopFile = desktopFile.strip(';').split(";")[$-1];
            cmdOptions = getCmdLineOptionsByDesktopFile(desktopFile);
        }
    }
    if (cmdOptions.empty) {
        string opener = de.chooseOpenerByDefault();
        if (!opener.empty) {
            cmdOptions = opener.split.array ~ ["%f"];
        }
    }
    return cmdOptions;
}


string getAppDesktopFilesForMimeType(string mimeType) {
    if (mimeAppsListExists) {
        auto mainSection = mimeAppsSections["Default Applications"];
        auto addedAssociations = mimeAppsSections["Added Associations"];
        auto beginOfMimeAppLine = mimeType ~ "=";
        foreach(line; addedAssociations) {
            if (line.startsWith(beginOfMimeAppLine)) {
                return line[beginOfMimeAppLine.length .. $].strip.idup;
            }
        }
        foreach(line; mainSection) {
            if (line.startsWith(beginOfMimeAppLine)) {
                return line[beginOfMimeAppLine.length .. $].strip.idup;
            }
        }
    } else {  // doubtful
        auto cmd = `xdg-mime query default ` ~ `"` ~ mimeType ~ `"`;
        auto answer = executeShell(cmd);
        return (answer.status != 0) ? "" : strip(cast(string)(answer.output));
    }
    return "";
}


string[] getCmdLineOptionsByDesktopFile(string desktopFileName) {
    string[] desktopStorages = [
        buildPath(homeDir, ".local/share/applications/"),
        "/usr/local/share/applications/",
        "/usr/share/applications/"
    ];
    string desktopFile;
    foreach(s; desktopStorages) {
        desktopFile = buildPath(s, desktopFileName);
        if (desktopFile.exists) {
            break;
        }
        desktopFile = "";
    }
    if (desktopFile == "") {
        return [];
    }
    string command;
    auto f = File(desktopFile, "r");
    foreach(line; f.byLine) {
        if (line.startsWith("Exec=")) {
            command = line["Exec=".length .. $].idup;
            break;
        }
    }

    string[] comArray = command.split(' ').array;
    string[] specFieldSet = ["%f", "%F", "%u", "%U"];
    foreach(ref arg; comArray) {
        // unknown argument (%i, %c or other)
        if (arg.startsWith("%") && !canFind(specFieldSet, arg)) {
            arg = "";
        } else if (arg == "%u") {
            arg = "%f";  // for single file or URL
        } else if (arg == "%U") {
            arg = "%F";  // for multiple files of URLs
        }
    }

    return comArray;
}


bool programExists(string app) {
    return !getAppPath(app).empty;
}


int executeByAmaltheaFormat(string[] files, bool useTerminal) {
    if (files.length == 0) {
        return 0;
    }
    string[][AppInfo] appsAndFiles;  // keys are applications
    bool fillAppsAndFiles(AppInfo[] apps, string[] files) {
        bool appForFileFound = false;
        foreach(appInfo; apps) {
            if (appInfo.useInTerminal && !useTerminal) {
                continue;
            }
            if (!appInfo.useInTerminal && useTerminal) {
                continue;
            }
            if (appInfo !in appsAndFiles) {
                appsAndFiles[appInfo] = [];
            }
            if (programExists(appInfo.app)) {
                appsAndFiles[appInfo] ~= files;
                appForFileFound = true;
            }
        }
        return appForFileFound;
    }

    toml.TomlTable table = toml.parseToml(readFormatConfiguration()).getTable;
    AppInfo[][string] groups = findAllGroups(table);
    AppInfo[][string] formats = findAllFormats(table);

    bool appForFileFound = false;
    foreach(f; files) {
        auto flist = useTerminal ? files : [f];
        if (amalthea.fs.isSymlink(f)) {
            f = amalthea.fs.readLinkRecurse(f);
        }
        if (amalthea.fs.isDir(f)) {
            appForFileFound = fillAppsAndFiles(formats["Directory"], flist);
            if (!appForFileFound) {
                return 1;
            }
            if (useTerminal) {
                break;
            }
            continue;
        }
        if (!amalthea.fs.isRegularFile(f)) {
            stderr.writeln(f ~ ": unable to open a file of this type."._s);
            continue;
        }
        FileFormat ff = getFileFormat(f);
        if (ff.group !in groups && ff.format !in formats) {
            return 1;
        }
        if (ff.format in formats) {
            appForFileFound = fillAppsAndFiles(formats[ff.format], flist);
        } else if (ff.group in groups) {
            appForFileFound = fillAppsAndFiles(groups[ff.group], flist);
        }
        if (!appForFileFound) {
            return 1;
        }
        if (useTerminal) {
            break;
        }
    }
    string[][] commandsToExecute;
    foreach(AppInfo info, string[] fpaths; appsAndFiles) {
        string[] firstArgs = [info.app] ~ info.preArgs;
        if (!info.takeSinglePath) {
            commandsToExecute ~= firstArgs ~ fpaths;
        } else {
            foreach(f; fpaths) {
                commandsToExecute ~= firstArgs ~ [f];
            }
        }
    }
    if (!useTerminal) {
        foreach(string[] cmd; commandsToExecute) {
            writeln(cmd);
            spawnProcess(cmd);
        }
    } else {
        assert(commandsToExecute.length == 1);
        writeln(commandsToExecute[0]);
        auto pid = spawnProcess(commandsToExecute[0]);
        wait(pid);
    }
    return 0;
}


struct AppInfo {
    string app;
    string[] preArgs;
    bool takeSinglePath;
    bool useInTerminal;
}


private AppInfo[][string] findAllGroupsOrFormats(
    toml.TomlTable table,
    bool findGroups
) {
    AppInfo[][string] result;
    foreach(string k, toml.TomlNode v; table) {
        if (findGroups && !k.startsWith("Group:")) {
            continue;
        }
        if (!findGroups && k.startsWith("Group:")) {
            continue;
        }
        k = k.trimPrefix("Group:");
        result[k] = [];
        toml.TomlNode[] arrayOfTables = v.getArray();
        foreach(toml.TomlNode elem; arrayOfTables) {
            toml.TomlTable t = elem.getTable();
            if ("app" !in t) {
                continue;
            }
            string app = t["app"].getStringValue();
            bool one = ("one" in t) ? t["one"].getBooleanValue() : false;
            bool tty = ("tty" in t) ? t["tty"].getBooleanValue() : false;
            string[] pre;
            if ("pre" in t) {
                foreach(arg; t["pre"].getArray()) {
                    pre ~= arg.getStringValue();
                }
            }
            result[k] ~= AppInfo(app, pre, one, tty);
        }
    }
    return result;
}


AppInfo[][string] findAllGroups(toml.TomlTable table) {
    return findAllGroupsOrFormats(table, findGroups: true);
}


AppInfo[][string] findAllFormats(toml.TomlTable table) {
    return findAllGroupsOrFormats(table, findGroups: false);
}


auto getIniSections(string filepath) {
    string[][string] sections;
    auto text = readText(filepath);
    size_t[] indexesOfSectionBegins;
    string[] splittedText = text.splitLines;
    foreach(i, line; splittedText) {
        if (line.empty) {
            continue;
        } else if (line[0] == '[' && line[$-1] == ']') {
            indexesOfSectionBegins ~= i;
        }
    }
    foreach(i, index; indexesOfSectionBegins) {
        string sectionName = splittedText[index][1 .. $-1];
        string[] sectionLines;

        size_t lastLineNumber;  // for current section
        if (i + 1 != indexesOfSectionBegins.length) {
            lastLineNumber = indexesOfSectionBegins[i + 1] - 1;
        } else {
            lastLineNumber = splittedText.length - 1;
        }

        sectionLines = splittedText[index + 1 .. lastLineNumber + 1];
        sections[sectionName] = sectionLines;
    }
    return sections;
}


string getBrowserByDefault() {
    auto answer = executeShell("xdg-settings get default-web-browser");
    return strip(cast(string)(answer.output));
}


string[] getCmdByDesktopFile(string desktopFileName, string[] filesToOpen) {
    string[] desktopStorages = [
        buildPath(homeDir, ".local/share/applications/"),
        "/usr/local/share/applications/",
        "/usr/share/applications/"
    ];
    string desktopFile;
    foreach(s; desktopStorages) {
        desktopFile = buildPath(s, desktopFileName);
        if (desktopFile.exists) {
            break;
        }
        desktopFile = "";
    }
    if (desktopFile == "") {
        return [];
    }
    string command;
    auto f = File(desktopFile, "r");
    foreach(line; f.byLine) {
        if (line.startsWith("Exec=")) {
            command = line["Exec=".length .. $].idup;
            break;
        }
    }
    string[] comArray = command.split(' ').array;
    string[] specFieldSet = ["%f", "%F", "%u", "%U"];
    foreach(ref arg; comArray) {
        if (arg.startsWith("%") && !canFind(specFieldSet, arg)) {
            arg = "";  // unknown argument (%i, %c or other)
        } else if (arg == "%u") {
            arg = "%U";
        } else if (arg == "%f") {
            arg = "%U";
        } else if (arg == "%F") {
            arg = "%U";
        }
    }
    comArray = comArray.replace("%U", filesToOpen);
    return comArray;
}
