/*
 * factory.c -- the factory method interfaces
 * Copyright (C) 2025-2026 Meltytech, LLC
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include "mlt_openfx.h"
#include <glib.h>
#include <stdbool.h>

extern OfxHost MltOfxHost;
mlt_properties mltofx_context;
mlt_properties mltofx_dl;

#if defined(__linux__) || defined(__FreeBSD__)

#define OFX_DIRLIST_SEP_CHARS ":;"
#define OFX_DIRSEP "/"
#include <dirent.h>

static const char *getArchStr()
{
    if (sizeof(void *) == 4) {
#if defined(__linux__)
        return "Linux-x86";
#else
        return "FreeBSD-x86";
#endif
    } else {
#if defined(__linux__)
        return "Linux-x86-64";
#else
        return "FreeBSD-x86-64";
#endif
    }
}

#define OFX_ARCHSTR getArchStr()

#elif defined(__APPLE__)

#define OFX_DIRLIST_SEP_CHARS ";:"
#if defined(__x86_64) || defined(__x86_64__)
#define OFX_ARCHSTR "MacOS-x86-64"
#else
#define OFX_ARCHSTR "MacOS"
#endif
#define OFX_DIRSEP "/"
#include <dirent.h>

#elif defined(WINDOWS) || defined(WIN32) || defined(_WIN32) || defined(__MINGW32__) \
    || defined(__MINGW64__)
#define OFX_DIRLIST_SEP_CHARS ";"
#if defined(_WIN64) || defined(__MINGW64__)
#define OFX_ARCHSTR "Win64"
#else
#define OFX_ARCHSTR "Win32"
#endif
#define OFX_DIRSEP "\\"

#include "shlobj.h"
#include "tchar.h"
#include <dirent.h>
#endif

extern mlt_filter filter_openfx_init(mlt_profile profile,
                                     mlt_service_type type,
                                     const char *id,
                                     char *arg);

static void plugin_mgr_destroy(mlt_properties p)
{
    int cN = mlt_properties_count(mltofx_context);
    for (int j = 0; j < cN; ++j) {
        char *id = mlt_properties_get_name(mltofx_context, j);
        mlt_properties pb = mlt_properties_get_properties(mltofx_context, id);
        char *dli = mlt_properties_get(pb, "dli");
        int index = mlt_properties_get_int(pb, "index");
        void *dlhandle = mlt_properties_get_data(mltofx_dl, dli, NULL);
        OfxGetPluginFn GetPluginFn = dlsym(dlhandle, "OfxGetPlugin");

        if (GetPluginFn != NULL) {
            OfxPlugin *pt = GetPluginFn(index);
            if (pt == NULL)
                continue;
            pt->mainEntry(kOfxActionUnload, NULL, NULL, NULL);
        }
    }
    mlt_properties_close(mltofx_context);
    mlt_properties_close(mltofx_dl);
    mlt_properties_close((mlt_properties) MltOfxHost.host);
}

static void add_metadata_image_formats(mlt_properties metadata, mlt_properties image_effect)
{
    if (!metadata || !image_effect)
        return;

    mlt_properties image_formats = mlt_properties_new();
    if (!image_formats)
        return;

    int format_index = 0;
    char key[16] = {'\0'};

    mltofx_components_mask components = mltofx_plugin_supported_components(image_effect);
    mltofx_depths_mask depths = mltofx_plugin_supported_depths(image_effect);

    if ((components & mltofx_components_rgb) && (depths & mltofx_depth_byte)) {
        snprintf(key, sizeof(key), "%d", format_index++);
        mlt_properties_set(image_formats, key, "rgb");
    }

    if ((components & mltofx_components_rgba) && (depths & mltofx_depth_byte)) {
        snprintf(key, sizeof(key), "%d", format_index++);
        mlt_properties_set(image_formats, key, "rgba");
    }

    if (depths & (mltofx_depth_short | mltofx_depth_half | mltofx_depth_float)) {
        snprintf(key, sizeof(key), "%d", format_index++);
        mlt_properties_set(image_formats, key, "rgba64");
    }

    mlt_properties_set_data(metadata,
                            "image_formats",
                            image_formats,
                            0,
                            (mlt_destructor) mlt_properties_close,
                            NULL);
}

static mlt_properties metadata(mlt_service_type type, const char *id, void *data)
{
    char file[PATH_MAX];
    snprintf(file, PATH_MAX, "%s/openfx/%s", mlt_environment("MLT_DATA"), (char *) data);
    mlt_properties result = mlt_properties_parse_yaml(file);
    if (result == NULL) {
        mlt_log_error(NULL, "Failed to parse OpenFX plugin metadata file: %s\n", file);
        return NULL;
    }

    mlt_properties pb = mlt_properties_get_properties(mltofx_context, id);
    char *dli = mlt_properties_get(pb, "dli");
    int index = mlt_properties_get_int(pb, "index");
    void *dlhandle = mlt_properties_get_data(mltofx_dl, dli, NULL);
    OfxGetPluginFn getPluginFn = dlsym(dlhandle, "OfxGetPlugin");
    if (!getPluginFn) {
        mlt_log_error(NULL, "Failed to get OfxGetPlugin function from plugin: %s\n", id);
        return NULL;
    }
    OfxPlugin *pt = getPluginFn(index);

    mlt_properties_set(result, "identifier", id);

    // parameters
    mlt_properties params = mlt_properties_new();
    mlt_properties image_effect = mltofx_fetch_params(pt, params, result);
    add_metadata_image_formats(result, image_effect);

    // Append the MLT-level mlt_origin parameter after all OFX plugin parameters.
    {
        char key[20];
        snprintf(key, sizeof(key), "%d", mlt_properties_count(params));
        mlt_properties p = mlt_properties_new();
        mlt_properties_set_data(params, key, p, 0, (mlt_destructor) mlt_properties_close, NULL);
        mlt_properties_set(p, "identifier", "mlt_origin");
        mlt_properties_set(p, "title", "Top-Left Origin");
        mlt_properties_set(p,
                           "description",
                           "Set to 1 to use MLT top-left image origin instead of the OFX "
                           "bottom-left origin. Use for plugins that crash or produce incorrect "
                           "output with negative row bytes.");
        mlt_properties_set(p, "type", "boolean");
        mlt_properties_set(p, "default", "0");
        mlt_properties_set(p, "minimum", "0");
        mlt_properties_set(p, "maximum", "1");
        mlt_properties_set(p, "widget", "checkbox");
    }

    mlt_properties_set_data(result,
                            "parameters",
                            params,
                            0,
                            (mlt_destructor) mlt_properties_close,
                            NULL);
    mlt_properties_close(image_effect);

    // Fall back to the plugin identifier if the plugin did not provide a label.
    if (!mlt_properties_get(result, "title"))
        mlt_properties_set(result, "title", id);
    return result;
}

static void scan_ofx_dir(mlt_repository repository, const char *dir, int *dli, int depth)
{
    size_t archstr_len = strlen(OFX_ARCHSTR);
    size_t dir_len = strlen(dir);

    DIR *d = opendir(dir);
    if (!d)
        return;

    struct dirent *de = readdir(d);
    while (de) {
        char *name = de->d_name;
        char *bni = NULL;
        if ((bni = strstr(name, ".ofx.bundle")) != NULL && bni[11] == '\0') {
            // bundle found at this level — load it
            char *barename = g_strndup(name, (int) (bni - name) + 4);
            size_t name_len = (size_t) (bni - name) + 4 + 7;
            // 12b sizeof `Contents` word, 1 sizeof null byte
            char *binpath = malloc(dir_len + name_len + 12 + (name_len - 7) + archstr_len + 1);
            sprintf(binpath, "%s/%s/Contents/%s/%s", dir, name, OFX_ARCHSTR, barename);
            void *dlhandle = dlopen(binpath, RTLD_LOCAL | RTLD_LAZY);
            free(binpath);
            free(barename);
            if (!dlhandle) {
                de = readdir(d);
                continue;
            }

            OfxGetPluginFn ofx_get_plugin = dlsym(dlhandle, "OfxGetPlugin");
            OfxGetNumberOfPluginsFn ofx_get_number_of_plugins = dlsym(dlhandle,
                                                                      "OfxGetNumberOfPlugins");
            if (!ofx_get_plugin || !ofx_get_number_of_plugins) {
                dlclose(dlhandle);
                de = readdir(d);
                continue;
            }

            char dl_n[16] = {'\0'};
            sprintf(dl_n, "%d", *dli);
            mlt_properties_set_data(mltofx_dl, dl_n, dlhandle, 0, (mlt_destructor) dlclose, NULL);
            (*dli)++;

            int plugin_count = ofx_get_number_of_plugins();
            for (int i = 0; i < plugin_count; ++i) {
                OfxPlugin *plugin_ptr = ofx_get_plugin(i);
                if (!plugin_ptr)
                    break;

                int detected = mltofx_detect_plugin(plugin_ptr);

                if (!detected)
                    continue;

                char *s = NULL;
                size_t pluginIdentifier_len = strlen(plugin_ptr->pluginIdentifier);
                s = malloc(pluginIdentifier_len + 8);
                sprintf(s, "openfx.%s", plugin_ptr->pluginIdentifier);

                // if colon `:` exists in plugin identifier
                // change it to accent sign `^` because `:`
                // can cause issues with mlt if put in filter
                // name
                char *str_ptr = strchr(s, ':');
                while (str_ptr != NULL) {
                    *str_ptr++ = '^';
                    str_ptr = strchr(str_ptr, ':');
                }

                mlt_properties p = mlt_properties_new();
                mlt_properties_set_properties(mltofx_context, s, p);
                mlt_properties_close(p);
                mlt_properties_set(p, "dli", dl_n);
                mlt_properties_set_int(p, "index", i);
                MLT_REGISTER(mlt_service_filter_type, s, filter_openfx_init);
                MLT_REGISTER_METADATA(mlt_service_filter_type, s, metadata, "filter_openfx.yml");
                free(s);
            }
        } else if (depth == 0 && name[0] != '.') {
            // Try one level of subdirectory (e.g. vendor subfolders inside an OFX plugins dir)
            char *subdir = malloc(dir_len + strlen(name) + 2);
            sprintf(subdir, "%s/%s", dir, name);
            scan_ofx_dir(repository, subdir, dli, 1);
            free(subdir);
        }
        de = readdir(d);
    }

    closedir(d);
}

MLT_REPOSITORY
{
    MltOfxHost.host = (OfxPropertySetHandle) mlt_properties_new();
    mltofx_init_host_properties(MltOfxHost.host);

    // Quiet warnings from plugins that use OCIO
    char *ocio = getenv("OCIO");
    if (!ocio || !strcmp(ocio, "")) {
        // If the OCIO environment variable is not set, set it to a built-in default config
        setenv("OCIO", "ocio://ocio://default", 1);
    }

    mltofx_context = mlt_properties_new();
    mltofx_dl = mlt_properties_new();

    int dli = 0;

    // Scan standard platform default paths per the OpenFX specification
#if defined(__linux__) || defined(__FreeBSD__)
    scan_ofx_dir(repository, "/usr/OFX/Plugins", &dli, 0);
    scan_ofx_dir(repository, "/usr/local/OFX/Plugins", &dli, 0);
#elif defined(__APPLE__)
    scan_ofx_dir(repository, "/Library/OFX/Plugins", &dli, 0);
    const char *home = getenv("HOME");
    if (home) {
        char user_ofx[PATH_MAX];
        snprintf(user_ofx, PATH_MAX, "%s/Library/OFX/Plugins", home);
        scan_ofx_dir(repository, user_ofx, &dli, 0);
    }
#elif defined(WINDOWS) || defined(WIN32) || defined(_WIN32) || defined(__MINGW32__) \
    || defined(__MINGW64__)
    char common_files[MAX_PATH];
    if (SHGetFolderPathA(NULL, CSIDL_PROGRAM_FILES_COMMON, NULL, 0, common_files) == S_OK) {
        char win_ofx[MAX_PATH + 13]; // +13 for "\OFX\Plugins" + NUL
        snprintf(win_ofx, sizeof(win_ofx), "%s\\OFX\\Plugins", common_files);
        scan_ofx_dir(repository, win_ofx, &dli, 0);
    }
#endif

    // Also scan OFX_PLUGIN_PATH if set (supplements default paths per OpenFX spec)
    char *openfx_path = getenv("OFX_PLUGIN_PATH");
    if (openfx_path) {
        char *path_copy = strdup(openfx_path);
        for (char *strptr = path_copy;; strptr = NULL) {
#ifdef _WIN32
            char *dir = strtok(strptr, MLT_DIRLIST_DELIMITER);
#else
            char *saveptr;
            char *dir = strtok_r(strptr, MLT_DIRLIST_DELIMITER, &saveptr);
#endif
            if (dir == NULL)
                break;
            scan_ofx_dir(repository, dir, &dli, 0);
        }
        free(path_copy);
    }

    mlt_factory_register_for_clean_up(mltofx_dl, (mlt_destructor) plugin_mgr_destroy);
}
