#include "options.hpp"
#include "backend.hpp"
#include "daemon_backend.hpp"
#include "proxy_backend.hpp"
#include "log.hpp"
#include "thirdparty/CLI11.hpp"
#include "data.h"
#include "license.hpp"
#include <SDL.h>
#include "assets/assets.h"
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#ifdef __HAIKU__
#include <os/AppKit.h>
#include <image.h>
#endif
#include "web_functions.hpp"
#include "cats.hpp"
#include <json/value.h>
#include <chrono>
using namespace Looper;
using namespace Looper::Options;
using namespace Looper::Log;
#ifdef __EMSCRIPTEN__
extern "C" {
    void quit();
}
#else
//#include <curl/curl.h>
#endif
std::vector<CatData> &get_cat_data() {
    static std::vector<CatData> data;
    return data;
}
std::unordered_set<LicenseData> license_data;
std::unordered_set<LicenseData> &get_license_data() {
    return license_data;
}
char *executable_path;
#ifdef __ANDROID__
#include <SDL.h>
#include <jni.h>
extern jclass MainActivity;
extern jobject mainActivity;
extern jmethodID GetUserDir_Method;
extern jmethodID OpenFilePicker_Method;
extern jmethodID GetPickedFile_Method;
extern jmethodID ClearSelected_Method;
extern jmethodID IsLoading_Method;
extern JNIEnv *env;
void initNative() {
    MainActivity = reinterpret_cast<jclass>(env->NewGlobalRef(env->FindClass("com/complecwaft/looper/MainActivity")));
    GetUserDir_Method = env->GetStaticMethodID(MainActivity, "GetUserDir",
                                               "()Ljava/lang/String;");
    OpenFilePicker_Method = env->GetStaticMethodID(MainActivity, "OpenFilePicker",
                                             "(Ljava/lang/Object;)V");
    GetPickedFile_Method = env->GetStaticMethodID(MainActivity, "GetPickedFile",
                                                  "()Ljava/lang/String;");
    ClearSelected_Method = env->GetStaticMethodID(MainActivity, "ClearSelected", "()V");
    IsLoading_Method = env->GetStaticMethodID(MainActivity, "IsLoading", "()Z");
    jfieldID singleton = env->GetStaticFieldID(MainActivity, "mSingleton", "Lcom/complecwaft/looper/MainActivity;");
    mainActivity = reinterpret_cast<jobject>(env->NewGlobalRef(env->GetStaticObjectField(MainActivity, singleton)));
}
#endif
extern "C" int looper_run_as_executable(std::vector<std::string> args) {
#ifdef __ANDROID__
    env = (JNIEnv*)SDL_AndroidGetJNIEnv();
    initNative();
#endif
#ifdef __EMSCRIPTEN__
    EM_ASM({ Module.wasmTable = wasmTable; });
#endif
    CLI::App app{DESCRIPTION};
    std::string filename = "";
    app.allow_extras();
    std::string ui_backend_option = "";
    bool full_help = false;
    bool open_window = false;
    app.add_option("-l, --log-level", LogStream::log_level, "Sets the minimum log level to display in the logs.");
    app.add_option("-u, --ui-backend", ui_backend_option, "Specifies which UI backend to use.");
#ifdef DBUS_ENABLED
    bool daemonize = false;
    bool disable_gui = false;
    bool quit = false;
    app.add_flag("-d, --daemon", daemonize, "Daemonizes the program.");
    app.add_flag("-n, --no-gui", disable_gui, "Don't open the GUI when there is a daemon and there are settings or commands for it. Ignored in daemon mode, or when no changes in state are issued.");
    app.add_flag("-q, --quit", quit, "Quits an existing instance.");
#endif
    try {
        app.parse(args);
    } catch (const CLI::ParseError &e) {
        if (app.get_help_ptr()->get_callback_run()) {
            full_help = true;
        } else {
            exit(app.exit(e));
        }
    }
#ifdef DBUS_ENABLED
    if (daemonize) {
        ui_backend_option = "daemon";
    }
#endif
    args.clear();
    args = app.remaining(false);
    int new_argc = args.size();
    char **new_argv = (char**)malloc(new_argc * sizeof(char*));

#ifdef DBUS_ENABLED
    if (quit) {
        DBusAPISender *sender = DBusAPISender::Create();
        if (sender != nullptr) {
            sender->Quit();
        } else {
            ERROR.writeln("Did not find existing instance to quit.");
            return 1;
        }
        return 0;
    }
#endif
    {
        auto looper_mit = LicenseData("Looper (MIT)", "MIT");
        auto looper_gpl = LicenseData("Looper (GPL)", "GPL-3.0-or-later");
        auto looper = LicenseData("Looper", "MIT OR GPL-3.0-or-later");
        auto tomlpp = LicenseData("TOML++", "MIT");
        auto cli11 = LicenseData("CLI11", "BSD-3-Clause");
        auto jsoncpp = LicenseData("JsonCpp", "MIT");
        auto soundtouch = LicenseData("SoundTouch", "LGPL-2.1-only");
        LOAD_LICENSE(looper_mit, looper_mit);
        LOAD_LICENSE(looper_gpl, looper_gpl);
        LOAD_LICENSE(looper, looper);
        LOAD_LICENSE(tomlpp, tomlplusplus);
        LOAD_LICENSE(cli11, cli11);
        LOAD_LICENSE(jsoncpp, jsoncpp);
        LOAD_LICENSE(soundtouch, lgpl_2_1);
        license_data.insert(looper);
        license_data.insert(looper_gpl);
        license_data.insert(looper_mit);
        license_data.insert(tomlpp);
        license_data.insert(cli11);
        license_data.insert(jsoncpp);
        license_data.insert(soundtouch);
    }
    DEBUG.writeln("Command line arguments after first parse:");
    for (size_t i = 0; i < new_argc; i++) {
        auto &arg = args[i];
        DEBUG.writefln(" - '%s'", arg.c_str());
        new_argv[i] = strdup(arg.c_str());
    }
    DEBUG.writeln("Initializing frontends...");
    init_backends();
    init_playback_backends();
    init_audio_data();
#ifdef DBUS_ENABLED
    ProxyGlueBackend *proxy_backend = nullptr;
    if ((disable_gui && !daemonize) || quit) {
        if (!DBusAPISender::isOnlyInstance()) {
            UIBackend::register_backend<ProxyGlueBackend>();
            if (disable_gui) {
                proxy_backend = UIBackend::get_backend<ProxyGlueBackend>().value_or(nullptr);
            }
        }
        if (quit && proxy_backend == nullptr) {
            proxy_backend = UIBackend::get_backend<ProxyGlueBackend>().value_or(nullptr);
        }
    }
    if (daemonize) {
        UIBackend::register_backend<DaemonGlueBackend>();
    }
#endif
    for (auto kv : UIBackend::backends) {
        kv.second->add_licenses();
    }
    for (auto kv : PlaybackBackendHelper()) {
	    kv.second->add_licenses();
    }
    DEBUG.writeln("Loaded frontends: ");
    for (auto kv : UIBackend::backends) {
        DEBUG.writefln(" - '%s'", kv.first.c_str());
    }
    DEBUG.writeln("Loaded playback backends: ");
    for (auto kv : PlaybackBackendHelper()) {
        DEBUG.writefln(" - '%s'", kv.first.c_str());
    }
    DEBUG.writeln("Loading options file...");
    load_options();
    {
        auto &cat_data = get_cat_data();
        cat_data.push_back(CatData(catoc_data, catoc_size, "Cat OC (Built-In)", "catoc.png"));
        #ifndef __EMSCRIPTEN__
        fs::path baseDir = get_prefs_path() / fs::path("looper") / fs::path("cats");
        if (!fs::exists(baseDir)) {
            fs::create_directories(baseDir);
        }
        for (auto const&dir_entry : std::filesystem::directory_iterator(baseDir)) {
            if (dir_entry.is_regular_file()) {
                cat_data.push_back(CatData(dir_entry.path()));
            } else if (dir_entry.is_directory()) {
                fs::path json_path = dir_entry.path() / fs::path("catpack.json");
                if (fs::exists(json_path)) {
                    std::ifstream stream(json_path);
                    Json::Value config;
                    stream >> config;
                    std::string name = dir_entry.path().stem().string();
                    if (config.isMember("name")) {
                        name = config["name"].asString();
                    }
                    if (!config.isMember("default")) {
                        continue;
                    }
                    std::string str = config["default"].asString();
                    if (config.isMember("variants")) {
                        Json::Value variants = config["variants"];
                        int prev_score = INT_MAX;
                        auto now = std::chrono::system_clock::now();
                        time_t now_tt = std::chrono::system_clock::to_time_t(now);
                        tm local_tm = *localtime(&now_tt);
                        auto cur_day = local_tm.tm_mday;
                        auto cur_month = local_tm.tm_mon;
                        auto cur_yday = local_tm.tm_yday;
			DEBUG.writefln("Current date: %d ([MM/DD] %02d/%02d)", cur_yday, cur_month + 1, cur_day);

                        for (auto &variant : variants) {
                            if (variant.isMember("startTime") && variant.isMember("endTime")) {
                                Json::Value startTime = variant["startTime"];
                                Json::Value endTime = variant["endTime"];
                                int score = 0;
                                int start = 0;
                                int end = 0;
				tm start_tm;
				tm end_tm;
                                if (startTime.isMember("day") && startTime.isMember("month")) {
                                    int day = startTime["day"].asInt();
                                    int month = startTime["month"].asInt();
                                    tm tmp{};
                                    tmp.tm_year = local_tm.tm_year;
                                    tmp.tm_mon = month - 1;
                                    tmp.tm_mday = day;
                                    time_t t = std::mktime(&tmp);
                                    tmp = *std::localtime(&t);
                                    start = tmp.tm_yday;
				    start_tm = tmp;
                                }
                                if (endTime.isMember("day") && endTime.isMember("month")) {
                                    int day = endTime["day"].asInt();
                                    int month = endTime["month"].asInt();
                                    tm tmp{};
                                    tmp.tm_year = local_tm.tm_year;
                                    tmp.tm_mon = month - 1;
                                    tmp.tm_mday = day;
                                    time_t t = std::mktime(&tmp);
                                    tmp = *std::localtime(&t);
                                    end = tmp.tm_yday;
				    end_tm = tmp;
                                }
				if (variant.isMember("path")) DEBUG.writefln("Variant %s: %d-%d ([MM/DD] %02d/%02d-%02d/%02d)", variant["path"].asCString(), start, end, start_tm.tm_mon+1, start_tm.tm_mday, end_tm.tm_mon+1, end_tm.tm_mday);
                                if (end < start) {
                                    if (cur_yday > end && cur_yday < start) continue;
                                    score = end - start + 365;
                                } else {
                                    if (cur_yday > end || cur_yday < start) continue;
                                    score = end - start;
                                }
				DEBUG.writefln("Score: %d (prev: %d)", score, prev_score);
                                if (variant.isMember("path")) {
                                    if (prev_score > score) {
                                        auto newPath =  variant["path"].asString();
                                        if (!fs::exists(dir_entry.path() / fs::path(newPath))) {
						continue;
                                        }
                                        str = newPath;
                                        prev_score = score;
                                    }
                                }
                            }
                        }
                    }
                    fs::path usedPath = dir_entry.path() / fs::path(str);
                    if (fs::exists(usedPath)) {
			    auto data = CatData(usedPath);
			    data.name = name;
			    cat_data.push_back(data);
		    }
                }
            }
        }
        #endif
    }
    std::string backend_id = get_option<std::string>("ui.frontend", "imgui");
    UIBackend *backend = UIBackend::get_backend(ui_backend_option).value_or(UIBackend::get_backend(backend_id).value_or(UIBackend::get_first_backend()));
    int output = 0;
    if (backend == nullptr) {
        ERROR.writeln("No UI backend could be found.");
        return -1;
    } else {
        DEBUG.writefln("Using backend: '%s'...", backend->get_id().c_str());
        UIBackend::running_ui_backend = backend;
        if (full_help) {
            args.clear();
            args.push_back("--help");
        }
        try {
#ifdef DBUS_ENABLED
            if (proxy_backend != nullptr && !quit) {
                if (!proxy_backend->run(args, new_argc, new_argv)) {
                    throw 0;
                }
                proxy_backend = nullptr;
            }
            if (!quit) {
                UIBackend::unregister_backend<ProxyGlueBackend>();
            }
#endif
            output = backend->run(args, new_argc, new_argv);
#ifdef DBUS_ENABLED
            if (quit && proxy_backend != nullptr) {
                proxy_backend->quitDaemon();
                proxy_backend->unregister_self();
            }
#endif
        } catch (int return_code) {
            if (full_help) {
                std::string helpstr = app.help();
                helpstr = helpstr.substr(helpstr.find("\n") + 1);
                helpstr = helpstr.substr(helpstr.find("\n") + 1);
                helpstr = helpstr.substr(helpstr.find("\n") + 1);
                helpstr = helpstr.substr(helpstr.find("\n") + 1);
                helpstr = helpstr.substr(helpstr.find("\n") + 1);
                printf("%s", helpstr.c_str());
            }
            output = return_code;
        }
    }
    DEBUG.writeln("Exiting...");
    UIBackend::deinit_backends();
    for (int i = 0; i < new_argc; i++) {
        free(new_argv[i]);
    }
    free(new_argv);
    save_options();

#ifdef __EMSCRIPTEN__
    quit();
#endif
    return output;
}
std::string current_process_type;
extern int looper_run_playback_process(std::vector<std::string> args);
int main(int argc, char **argv) {
    if (argc == 0) {
        throw std::exception();
    }
#ifdef _WIN32
    size_t size = std::max(10240, PATH_MAX) + 1;
    executable_path = malloc(size);
    size = GetModuleFileNameA(NULL, executable_path, size);
    realloc(executable_path, size);
#elif defined(__HAIKU__)
	{
		int32 cookie = 0;
		image_info info;
		while (get_next_image_info(B_CURRENT_TEAM, &cookie, &info) == B_OK) {
			if (info.type == B_APP_IMAGE) {
				executable_path = strdup(info.name);
				break;
			}
		}
	}
#else
    executable_path = strdup(fs::canonical("/proc/self/exe").c_str());
#endif
    CLI::App app{DESCRIPTION};
    std::string process_type = "normal";
    app.add_option<std::string, std::string>("--process-type", process_type);
    app.add_option("-l, --log-level", LogStream::log_level, "Sets the minimum log level to display in the logs.");
    app.allow_extras();
    init_logging();
#ifndef __EMSCRIPTEN__
	//curl_global_init(CURL_GLOBAL_ALL);
#endif
    try {
	    app.parse(argc, argv);
	    executable_path = argv[0];
	    current_process_type = "host";
	    if (process_type == "playback") {
	        current_process_type = "playback";
	        return looper_run_playback_process(app.remaining(false));
	    } else if (process_type == "daemon") {
	        std::vector<std::string> opts = app.remaining(false);
	        opts.push_back("-d");
	        return looper_run_as_executable(opts);
	    } else {
	        return looper_run_as_executable(app.remaining(false));
	    }
    } catch (CLI::CallForHelp) {
    	return looper_run_as_executable(std::vector<std::string>({"--help"}));
    }
    free(executable_path);
    return 255;
}