/*
 * factory.c -- the factory method interfaces
 * Copyright (c) 2008 Marco Gittler <g.marco@freenet.de>
 * Copyright (C) 2009-2022 Meltytech, LLC
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include <framework/mlt.h>
#include <frei0r.h>
#include <string.h>

#include <dirent.h>
#include <dlfcn.h>
#include <limits.h>
#include <math.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>

#ifdef _WIN32
#define LIBSUF ".dll"
#define FREI0R_PLUGIN_PATH "\\lib\\frei0r-1"
#else
#define LIBSUF ".so"
#ifdef RELOCATABLE
#ifdef __APPLE__
#define FREI0R_PLUGIN_PATH "/PlugIns/frei0r-1"
#else
#define FREI0R_PLUGIN_PATH "/lib/frei0r-1"
#endif
#else
#define FREI0R_PLUGIN_PATH \
    "/usr/lib/frei0r-1:/usr/lib64/frei0r-1:/opt/local/lib/frei0r-1:/usr/local/lib/frei0r-1:$HOME/" \
    ".frei0r-1/lib"
#endif
#endif

extern mlt_filter filter_frei0r_init(mlt_profile profile,
                                     mlt_service_type type,
                                     const char *id,
                                     char *arg);
extern mlt_filter filter_cairoblend_mode_init(mlt_profile profile,
                                              mlt_service_type type,
                                              const char *id,
                                              char *arg);
extern mlt_frame filter_process(mlt_filter filter, mlt_frame frame);
extern void filter_close(mlt_filter filter);
extern int producer_get_frame(mlt_producer producer, mlt_frame_ptr frame, int index);
extern void producer_close(mlt_producer producer);
extern void transition_close(mlt_transition transition);
extern mlt_frame transition_process(mlt_transition transition, mlt_frame a_frame, mlt_frame b_frame);

static char *get_frei0r_path()
{
#if defined(_WIN32) || defined(RELOCATABLE)
    char *dirname = malloc(strlen(mlt_environment("MLT_APPDIR")) + strlen(FREI0R_PLUGIN_PATH) + 1);
    strcpy(dirname, mlt_environment("MLT_APPDIR"));
    strcat(dirname, FREI0R_PLUGIN_PATH);
    return dirname;
#else
    return strdup(getenv("FREI0R_PATH")              ? getenv("FREI0R_PATH")
                  : getenv("MLT_FREI0R_PLUGIN_PATH") ? getenv("MLT_FREI0R_PLUGIN_PATH")
                                                     : FREI0R_PLUGIN_PATH);
#endif
}

static void check_thread_safe(mlt_properties properties, const char *name)
{
    char dirname[PATH_MAX];
    snprintf(dirname, PATH_MAX, "%s/frei0r/not_thread_safe.txt", mlt_environment("MLT_DATA"));
    mlt_properties not_thread_safe = mlt_properties_load(dirname);
    double version = mlt_properties_get_double(properties, "version");
    int i;

    for (i = 0; i < mlt_properties_count(not_thread_safe); i++) {
        if (!strcmp(name, mlt_properties_get_name(not_thread_safe, i))) {
            double thread_safe_version = mlt_properties_get_double(not_thread_safe, name);
            if (thread_safe_version == 0.0 || version < thread_safe_version)
                mlt_properties_set_int(properties, "_not_thread_safe", 1);
            break;
        }
    }
    mlt_properties_close(not_thread_safe);
}

static mlt_properties fill_param_info(mlt_service_type type, const char *service_name, char *name)
{
    char file[PATH_MAX];
    char servicetype[1024] = "";
    struct stat stat_buff;

    switch (type) {
    case mlt_service_producer_type:
        strcpy(servicetype, "producer");
        break;
    case mlt_service_filter_type:
        strcpy(servicetype, "filter");
        break;
    case mlt_service_transition_type:
        strcpy(servicetype, "transition");
        break;
    default:
        strcpy(servicetype, "");
    }

    snprintf(file,
             PATH_MAX,
             "%s/frei0r/%s_%s.yml",
             mlt_environment("MLT_DATA"),
             servicetype,
             service_name);
    memset(&stat_buff, 0, sizeof(stat_buff));
    stat(file, &stat_buff);

    if (S_ISREG(stat_buff.st_mode)) {
        return mlt_properties_parse_yaml(file);
    }

    void *handle = dlopen(name, RTLD_LAZY);
    if (!handle)
        return NULL;
    void (*plginfo)(f0r_plugin_info_t *) = dlsym(handle, "f0r_get_plugin_info");
    void (*param_info)(f0r_param_info_t *, int param_index) = dlsym(handle, "f0r_get_param_info");
    void (*f0r_init)(void) = dlsym(handle, "f0r_init");
    void (*f0r_deinit)(void) = dlsym(handle, "f0r_deinit");
    f0r_instance_t (*f0r_construct)(unsigned int, unsigned int) = dlsym(handle, "f0r_construct");
    void (*f0r_destruct)(f0r_instance_t) = dlsym(handle, "f0r_destruct");
    void (*f0r_get_param_value)(f0r_instance_t instance, f0r_param_t param, int param_index)
        = dlsym(handle, "f0r_get_param_value");
    if (!plginfo || !param_info) {
        dlclose(handle);
        return NULL;
    }
    mlt_properties metadata = mlt_properties_new();
    f0r_plugin_info_t info;
    char string[48];
    int j = 0;

    f0r_init();
    f0r_instance_t instance = f0r_construct(720, 576);
    if (!instance) {
        f0r_deinit();
        dlclose(handle);
        mlt_properties_close(metadata);
        return NULL;
    }
    plginfo(&info);
    snprintf(string, sizeof(string), "%d", info.minor_version);
    mlt_properties_set(metadata, "schema_version", "7.0");
    mlt_properties_set(metadata, "title", info.name);
    mlt_properties_set_double(metadata,
                              "version",
                              info.major_version + info.minor_version / pow(10, strlen(string)));
    mlt_properties_set(metadata, "identifier", service_name);
    mlt_properties_set(metadata, "description", info.explanation);
    mlt_properties_set(metadata, "creator", info.author);
    switch (type) {
    case mlt_service_producer_type:
        mlt_properties_set(metadata, "type", "producer");
        break;
    case mlt_service_filter_type:
        mlt_properties_set(metadata, "type", "filter");
        break;
    case mlt_service_transition_type:
        mlt_properties_set(metadata, "type", "transition");
        break;
    default:
        break;
    }

    mlt_properties tags = mlt_properties_new();
    mlt_properties_set_data(metadata, "tags", tags, 0, (mlt_destructor) mlt_properties_close, NULL);
    mlt_properties_set(tags, "0", "Video");

    mlt_properties parameter = mlt_properties_new();
    mlt_properties_set_data(metadata,
                            "parameters",
                            parameter,
                            0,
                            (mlt_destructor) mlt_properties_close,
                            NULL);

    for (j = 0; j < info.num_params; j++) {
        snprintf(string, sizeof(string), "%d", j);
        mlt_properties pnum = mlt_properties_new();
        mlt_properties_set_data(parameter,
                                string,
                                pnum,
                                0,
                                (mlt_destructor) mlt_properties_close,
                                NULL);
        f0r_param_info_t paraminfo;
        param_info(&paraminfo, j);
        mlt_properties_set(pnum, "identifier", string);
        mlt_properties_set(pnum, "title", paraminfo.name);
        mlt_properties_set(pnum, "description", paraminfo.explanation);
        if (paraminfo.type == F0R_PARAM_DOUBLE) {
            double deflt = 0;
            mlt_properties_set(pnum, "type", "float");
            mlt_properties_set(pnum, "minimum", "0");
            mlt_properties_set(pnum, "maximum", "1");
            f0r_get_param_value(instance, &deflt, j);
            mlt_properties_set_double(pnum, "default", CLAMP(deflt, 0.0, 1.0));
            mlt_properties_set(pnum, "mutable", "yes");
            mlt_properties_set(pnum, "animation", "yes");
            mlt_properties_set(pnum, "widget", "spinner");
        } else if (paraminfo.type == F0R_PARAM_BOOL) {
            double deflt = 0;
            mlt_properties_set(pnum, "type", "boolean");
            mlt_properties_set(pnum, "minimum", "0");
            mlt_properties_set(pnum, "maximum", "1");
            f0r_get_param_value(instance, &deflt, j);
            mlt_properties_set_int(pnum, "default", deflt != 0.0);
            mlt_properties_set(pnum, "mutable", "yes");
            mlt_properties_set(pnum, "animation", "yes");
            mlt_properties_set(pnum, "widget", "checkbox");
        } else if (paraminfo.type == F0R_PARAM_COLOR) {
            char colorstr[8];
            f0r_param_color_t deflt = {0, 0, 0};

            mlt_properties_set(pnum, "type", "color");
            f0r_get_param_value(instance, &deflt, j);
            sprintf(colorstr,
                    "#%02x%02x%02x",
                    (unsigned) CLAMP(deflt.r * 255, 0, 255),
                    (unsigned) CLAMP(deflt.g * 255, 0, 255),
                    (unsigned) CLAMP(deflt.b * 255, 0, 255));
            colorstr[7] = 0;
            mlt_properties_set(pnum, "default", colorstr);
            mlt_properties_set(pnum, "mutable", "yes");
            mlt_properties_set(pnum, "animation", "yes");
            mlt_properties_set(pnum, "widget", "color");
        } else if (paraminfo.type == F0R_PARAM_STRING) {
            char *deflt = NULL;
            mlt_properties_set(pnum, "type", "string");
            f0r_get_param_value(instance, &deflt, j);
            mlt_properties_set(pnum, "default", deflt);
            mlt_properties_set(pnum, "mutable", "yes");
            mlt_properties_set(pnum, "widget", "text");
        }
    }
    f0r_destruct(instance);
    f0r_deinit();
    dlclose(handle);

    return metadata;
}

static void *load_lib(mlt_profile profile, mlt_service_type type, void *handle, const char *name)
{
    int i = 0;
    void (*f0r_get_plugin_info)(f0r_plugin_info_t *), *f0r_construct, *f0r_update, *f0r_destruct,
        (*f0r_get_param_info)(f0r_param_info_t * info, int param_index), (*f0r_init)(void),
        *f0r_deinit,
        (*f0r_set_param_value)(f0r_instance_t instance, f0r_param_t param, int param_index),
        (*f0r_get_param_value)(f0r_instance_t instance, f0r_param_t param, int param_index),
        (*f0r_update2)(f0r_instance_t instance,
                       double time,
                       const uint32_t *inframe1,
                       const uint32_t *inframe2,
                       const uint32_t *inframe3,
                       uint32_t *outframe);

    if ((f0r_construct = dlsym(handle, "f0r_construct"))
        && (f0r_destruct = dlsym(handle, "f0r_destruct"))
        && (f0r_get_plugin_info = dlsym(handle, "f0r_get_plugin_info"))
        && (f0r_get_param_info = dlsym(handle, "f0r_get_param_info"))
        && (f0r_set_param_value = dlsym(handle, "f0r_set_param_value"))
        && (f0r_get_param_value = dlsym(handle, "f0r_get_param_value"))
        && (f0r_init = dlsym(handle, "f0r_init")) && (f0r_deinit = dlsym(handle, "f0r_deinit"))) {
        f0r_update = dlsym(handle, "f0r_update");
        f0r_update2 = dlsym(handle, "f0r_update2");

        f0r_plugin_info_t info;
        f0r_get_plugin_info(&info);

        void *ret = NULL;
        mlt_properties properties = NULL;
        char minor[12];

        if (type == mlt_service_producer_type && info.plugin_type == F0R_PLUGIN_TYPE_SOURCE) {
            mlt_producer producer = mlt_producer_new(profile);
            if (producer) {
                producer->get_frame = producer_get_frame;
                producer->close = (mlt_destructor) producer_close;
                f0r_init();
                properties = MLT_PRODUCER_PROPERTIES(producer);

                for (i = 0; i < info.num_params; i++) {
                    f0r_param_info_t pinfo;
                    f0r_get_param_info(&pinfo, i);
                }

                ret = producer;
            }
        } else if (type == mlt_service_filter_type && info.plugin_type == F0R_PLUGIN_TYPE_FILTER) {
            mlt_filter filter = mlt_filter_new();
            if (filter) {
                filter->process = filter_process;
                filter->close = filter_close;
                f0r_init();
                properties = MLT_FILTER_PROPERTIES(filter);

                for (i = 0; i < info.num_params; i++) {
                    f0r_param_info_t pinfo;
                    f0r_get_param_info(&pinfo, i);
                }

                ret = filter;
            }
        } else if (type == mlt_service_transition_type
                   && info.plugin_type == F0R_PLUGIN_TYPE_MIXER2) {
            mlt_transition transition = mlt_transition_new();
            if (transition) {
                transition->process = transition_process;
                transition->close = transition_close;
                f0r_init();
                properties = MLT_TRANSITION_PROPERTIES(transition);
                mlt_properties_set_int(properties, "_transition_type", 1);

                ret = transition;
            }
        }
        mlt_properties_set_data(properties, "_dlclose_handle", handle, sizeof(handle), NULL, NULL);
        mlt_properties_set_data(properties, "_dlclose", dlclose, sizeof(void *), NULL, NULL);
        mlt_properties_set_data(properties,
                                "f0r_construct",
                                f0r_construct,
                                sizeof(f0r_construct),
                                NULL,
                                NULL);
        mlt_properties_set_data(properties, "f0r_update", f0r_update, sizeof(f0r_update), NULL, NULL);
        if (f0r_update2)
            mlt_properties_set_data(properties,
                                    "f0r_update2",
                                    f0r_update2,
                                    sizeof(f0r_update2),
                                    NULL,
                                    NULL);
        mlt_properties_set_data(properties,
                                "f0r_destruct",
                                f0r_destruct,
                                sizeof(f0r_destruct),
                                NULL,
                                NULL);
        mlt_properties_set_data(properties,
                                "f0r_get_plugin_info",
                                f0r_get_plugin_info,
                                sizeof(void *),
                                NULL,
                                NULL);
        mlt_properties_set_data(properties,
                                "f0r_get_param_info",
                                f0r_get_param_info,
                                sizeof(void *),
                                NULL,
                                NULL);
        mlt_properties_set_data(properties,
                                "f0r_set_param_value",
                                f0r_set_param_value,
                                sizeof(void *),
                                NULL,
                                NULL);
        mlt_properties_set_data(properties,
                                "f0r_get_param_value",
                                f0r_get_param_value,
                                sizeof(void *),
                                NULL,
                                NULL);

        // Let frei0r plugin version be serialized using same format as metadata
        snprintf(minor, sizeof(minor), "%d", info.minor_version);
        mlt_properties_set_double(properties,
                                  "version",
                                  info.major_version + info.minor_version / pow(10, strlen(minor)));
        check_thread_safe(properties, name);

        // Use the global param name map for backwards compatibility when
        // param names change and setting frei0r params by name instead of index.
        mlt_properties param_name_map = mlt_properties_get_data(mlt_global_properties(),
                                                                "frei0r.param_name_map",
                                                                NULL);
        if (param_name_map) {
            // Lookup my plugin in the map
            param_name_map = mlt_properties_get_data(param_name_map, name, NULL);
            mlt_properties_set_data(properties, "_param_name_map", param_name_map, 0, NULL, NULL);
        }
        param_name_map = mlt_properties_get_data(mlt_global_properties(),
                                                 "frei0r.resolution_scale",
                                                 NULL);
        if (param_name_map) {
            // Lookup my plugin in the map
            param_name_map = mlt_properties_get_data(param_name_map, name, NULL);
            mlt_properties_set_data(properties, "_resolution_scale", param_name_map, 0, NULL, NULL);
        }
        return ret;
    } else {
        mlt_log_error(NULL, "frei0r plugin \"%s\" is missing a function\n", name);
        dlerror();
    }
    return NULL;
}

static void *create_frei0r_item(mlt_profile profile,
                                mlt_service_type type,
                                const char *id,
                                void *arg)
{
    mlt_tokeniser tokeniser = mlt_tokeniser_init();
    char *frei0r_path = get_frei0r_path();
    int dircount = mlt_tokeniser_parse_new(tokeniser, frei0r_path, MLT_DIRLIST_DELIMITER);
    void *ret = NULL;
    while (dircount-- && !ret) {
        char soname[PATH_MAX];
        char *myid = strdup(id);

#ifdef _WIN32
        char *firstname = strtok(myid, ".");
#else
        char *save_firstptr = NULL;
        char *firstname = strtok_r(myid, ".", &save_firstptr);
#endif
        char *directory = mlt_tokeniser_get_string(tokeniser, dircount);

#ifdef _WIN32
        firstname = strtok(NULL, ".");
#else
        firstname = strtok_r(NULL, ".", &save_firstptr);
#endif
        if (strncmp(directory, "$HOME", 5))
            snprintf(soname, PATH_MAX, "%s/%s" LIBSUF, directory, firstname);
        else
            snprintf(soname,
                     PATH_MAX,
                     "%s%s/%s" LIBSUF,
                     getenv("HOME"),
                     strchr(directory, '/'),
                     firstname);

        if (firstname) {
            void *handle = dlopen(soname, RTLD_LAZY);

            if (handle) {
                ret = load_lib(profile, type, handle, firstname);
            } else {
                dlerror();
            }
        }
        free(myid);
    }
    mlt_tokeniser_close(tokeniser);
    free(frei0r_path);
    return ret;
}

static mlt_properties metadata(mlt_service_type type, const char *id, void *data)
{
    char file[PATH_MAX];
    snprintf(file, PATH_MAX, "%s/frei0r/%s", mlt_environment("MLT_DATA"), (char *) data);
    return mlt_properties_parse_yaml(file);
}

MLT_REPOSITORY
{
    int i = 0;
    mlt_tokeniser tokeniser = mlt_tokeniser_init();
    char *frei0r_path = get_frei0r_path();
    int dircount = mlt_tokeniser_parse_new(tokeniser, frei0r_path, MLT_DIRLIST_DELIMITER);
    char dirname[PATH_MAX];
    snprintf(dirname, PATH_MAX, "%s/frei0r/blacklist.txt", mlt_environment("MLT_DATA"));
    mlt_properties blacklist = mlt_properties_load(dirname);

    // Load a param name map into global properties for backwards compatibility when
    // param names change and setting frei0r params by name instead of index.
    snprintf(dirname, PATH_MAX, "%s/frei0r/param_name_map.yaml", mlt_environment("MLT_DATA"));
    mlt_properties_set_data(mlt_global_properties(),
                            "frei0r.param_name_map",
                            mlt_properties_parse_yaml(dirname),
                            0,
                            (mlt_destructor) mlt_properties_close,
                            NULL);

    // Load a list of parameters impacted by consumer scale into global properties.
    snprintf(dirname, PATH_MAX, "%s/frei0r/resolution_scale.yml", mlt_environment("MLT_DATA"));
    mlt_properties_set_data(mlt_global_properties(),
                            "frei0r.resolution_scale",
                            mlt_properties_parse_yaml(dirname),
                            0,
                            (mlt_destructor) mlt_properties_close,
                            NULL);

    while (dircount--) {
        mlt_properties direntries = mlt_properties_new();
        char *directory = mlt_tokeniser_get_string(tokeniser, dircount);

        if (strncmp(directory, "$HOME", 5))
            snprintf(dirname, PATH_MAX, "%s", directory);
        else
            snprintf(dirname, PATH_MAX, "%s%s", getenv("HOME"), strchr(directory, '/'));
        mlt_properties_dir_list(direntries, dirname, "*" LIBSUF, 1);

        for (i = 0; i < mlt_properties_count(direntries); i++) {
            char *name = mlt_properties_get_value(direntries, i);
            char *shortname = name + strlen(dirname) + 1;
#ifdef _WIN32
            char *firstname = strtok(shortname, ".");
#else
            char *save_firstptr = NULL;
            char *firstname = strtok_r(shortname, ".", &save_firstptr);
#endif
            char pluginname[1024] = "frei0r.";
            if (firstname)
                strncat(pluginname, firstname, sizeof(pluginname) - strlen(pluginname) - 1);

            if (firstname && mlt_properties_get(blacklist, firstname))
                continue;

            void *handle = dlopen(strcat(name, LIBSUF), RTLD_LAZY);
            if (handle) {
                void (*plginfo)(f0r_plugin_info_t *) = dlsym(handle, "f0r_get_plugin_info");

                if (plginfo) {
                    f0r_plugin_info_t info;
                    plginfo(&info);
                    if (firstname && info.plugin_type == F0R_PLUGIN_TYPE_SOURCE) {
                        if (mlt_properties_get(mlt_repository_producers(repository), pluginname)) {
                            dlclose(handle);
                            continue;
                        }
                        MLT_REGISTER(mlt_service_producer_type, pluginname, create_frei0r_item);
                        MLT_REGISTER_METADATA(mlt_service_producer_type,
                                              pluginname,
                                              fill_param_info,
                                              name);
                    } else if (firstname && info.plugin_type == F0R_PLUGIN_TYPE_FILTER) {
                        if (mlt_properties_get(mlt_repository_filters(repository), pluginname)) {
                            dlclose(handle);
                            continue;
                        }
                        MLT_REGISTER(mlt_service_filter_type, pluginname, create_frei0r_item);
                        MLT_REGISTER_METADATA(mlt_service_filter_type,
                                              pluginname,
                                              fill_param_info,
                                              name);
                    } else if (firstname && info.plugin_type == F0R_PLUGIN_TYPE_MIXER2) {
                        if (mlt_properties_get(mlt_repository_transitions(repository), pluginname)) {
                            dlclose(handle);
                            continue;
                        }
                        MLT_REGISTER(mlt_service_transition_type, pluginname, create_frei0r_item);
                        MLT_REGISTER_METADATA(mlt_service_transition_type,
                                              pluginname,
                                              fill_param_info,
                                              name);
                    }
                }
                dlclose(handle);
            }
        }
        mlt_factory_register_for_clean_up(direntries, (mlt_destructor) mlt_properties_close);
    }
    mlt_tokeniser_close(tokeniser);
    mlt_properties_close(blacklist);
    free(frei0r_path);
    MLT_REGISTER(mlt_service_filter_type, "cairoblend_mode", filter_cairoblend_mode_init);
    MLT_REGISTER_METADATA(mlt_service_filter_type,
                          "cairoblend_mode",
                          metadata,
                          "filter_cairoblend_mode.yml");
}
