/**
 * @file library_ops.c
 * @brief Music library management and scanning operations.
 *
 * Responsible for reading directories, and updating the in-memory library
 * representation used by playlists
 * and the UI browser.
 */

#include "library_ops.h"

#include "common/appstate.h"
#include "common/common.h"

#include "playlist_ops.h"
#include "track_manager.h"

#include "data/directorytree.h"
#include "loader/tagLibWrapper.h"

#include "ui/components.h"
#include "utils/file.h"
#include "utils/utils.h"

#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/time.h>

typedef struct
{
        char *path;
        bool wait_until_complete;
        AppState *state;
} UpdateLibraryThreadArgs;

static int current_sort = 0;

void reset_sort_library(void)
{
        FileSystemEntry *library = get_library();

        if (current_sort == 1) {
                sort_file_system_tree(library, compare_entry_natural);
                current_sort = 0;
        }
}

void sort_library(void)
{
        FileSystemEntry *library = get_library();

        if (current_sort == 0) {
                sort_file_system_tree(library, compare_folders_by_age_files_alphabetically);
                current_sort = 1;
        } else {
                sort_file_system_tree(library, compare_entry_natural);
                current_sort = 0;
        }

        set_dirty(DIRTY_LIBRARY);
}

int mark_as_enqueued(FileSystemEntry *root, char *path, int list_row_num)
{
        if (root == NULL)
                return 0;

        if (path == NULL)
                return 0;

        if (list_row_num == 0)
                list_row_num = 1;

        if (!root->is_directory) {
                if (strcmp(root->full_path, path) == 0) {
                        root->is_enqueued = list_row_num;
                        return root->id;
                }
        } else {
                FileSystemEntry *child = root->children;
                int found = 0;
                while (child != NULL) {
                        found = mark_as_enqueued(child, path, list_row_num);
                        child = child->next;

                        if (found)
                                break;
                }

                if (found) {
                        root->is_enqueued = list_row_num;

                        return found;
                }
        }

        return 0;
}

void mark_list_as_enqueued(FileSystemEntry *root, PlayList *playlist)
{
        Node *node = playlist->head;
        int list_row_num = 1;

        for (int i = 0; i < playlist->count; i++) {
                if (node == NULL)
                        break;

                if (node->song.file_path == NULL)
                        break;

                int id = mark_as_enqueued(root, node->song.file_path, list_row_num);

                list_row_num++;

                if (id > 0)
                        node->id = id;

                node = node->next;
        }

        root->is_enqueued = false; // Don't mark the absolute root
}

bool mark_as_dequeued(FileSystemEntry *root, char *path)
{
        int num_children_enqueued = 0;

        if (root == NULL)
                return false;

        if (!root->is_directory) {
                if (strcmp(root->full_path, path) == 0) {
                        root->is_enqueued = false;
                        return true;
                }
        } else {
                FileSystemEntry *child = root->children;
                bool found = false;
                while (child != NULL) {
                        found = mark_as_dequeued(child, path);
                        child = child->next;

                        if (found)
                                break;
                }

                if (found) {
                        child = root->children;

                        while (child != NULL) {
                                if (child->is_enqueued)
                                        num_children_enqueued++;

                                child = child->next;
                        }

                        if (num_children_enqueued == 0)
                                root->is_enqueued = false;

                        return true;
                }
        }

        return false;
}

void clear_all_m3u_enqueued_flags(FileSystemEntry *root)
{
        if (root == NULL)
                return;

        if (!root->is_directory && is_m3u_file(root) && root->is_enqueued) {
                root->is_enqueued = false;
                return;
        }

        FileSystemEntry *child = root->children;
        while (child != NULL) {
                clear_all_m3u_enqueued_flags(child);
                child = child->next;
        }
}

typedef struct
{
        char *path;
        Model *model;
} UpdateLibraryArgs;

void *update_library_thread(void *arg)
{
        UpdateLibraryArgs *args = arg;

        if (args == NULL)
                return NULL;

        Model *model = args->model;

        if (model->updating_library)
                return NULL;

        model->updating_library = true;

        char *path = args->path;
        int tmp_directory_tree_entries = 0;

        char expanded_path[KEW_PATH_MAX];
        expand_path(path, expanded_path, KEW_PATH_MAX);

        FileSystemEntry *tmp =
            create_directory_tree(expanded_path, &tmp_directory_tree_entries);

        if (!tmp) {
                perror("create_directory_tree");
                free(args->path);
                free(args);
                model->updating_library = false;
                return NULL;
        }

        pthread_mutex_lock(&(model->state.library_mutex));

        component_library_helper_reset(model);

        FileSystemEntry *old = model->library;

        copy_is_enqueued(old, tmp);

        model->library = tmp;

        model->library_updated = true;

        model->state.ui.numDirectoryTreeEntries = tmp_directory_tree_entries;

        free_tree(old);

        c_sleep(1000); // Don't refresh immediately or we risk the error message
                       // not clearing

        free(args->path);
        free(args);
        model->updating_library = false;

        pthread_mutex_unlock(&(model->state.library_mutex));

        return NULL;
}

void update_library(char *path, bool wait_until_complete)
{
        Model *model = get_model();
        pthread_t thread_id;

        UpdateLibraryArgs *args = malloc(sizeof(UpdateLibraryArgs));

        if (!args)
                return; // handle allocation failure

        args->path = strdup(path);
        args->model = model;

        if (pthread_create(&thread_id, NULL, update_library_thread, args) != 0) {
                perror("Failed to create thread");
                return;
        }

        if (wait_until_complete)
                pthread_join(thread_id, NULL);

        model->state.settings.last_time_app_ran = time(NULL);
        model->state.settings.currentSongId = -1;
        model->state.settings.currentSongSeconds = 0;
}

time_t get_modification_time(struct stat *path_stat)
{
        return path_stat->st_mtime;
}

void *update_if_top_level_folders_mtimes_changed_thread(void *arg)
{
        UpdateLibraryThreadArgs *args = (UpdateLibraryThreadArgs *)arg;

        char *path = args->path;
        AppState *state = args->state;
        UISettings *ui = &(state->settings);

        struct stat path_stat;

        if (stat(path, &path_stat) == -1) {
                if (args->path)
                        free(args->path);
                free(args);
                return NULL;
        }

        if (get_modification_time(&path_stat) > ui->last_time_app_ran &&
            ui->last_time_app_ran > 0) {

                update_library(path, args->wait_until_complete);

                if (args->path)
                        free(args->path);
                free(args);
                return NULL;
        }

        DIR *dir = opendir(path);
        if (!dir) {
                perror("opendir");
                if (args->path)
                        free(args->path);
                free(args);
                return NULL;
        }

        struct dirent *entry;

        while ((entry = readdir(dir)) != NULL) {

                if (strcmp(entry->d_name, ".") == 0 ||
                    strcmp(entry->d_name, "..") == 0) {
                        continue;
                }

                char full_path[1024];
                snprintf(full_path, sizeof(full_path), "%s/%s", path, entry->d_name);

                if (stat(full_path, &path_stat) == -1)
                        continue;

                if (S_ISDIR(path_stat.st_mode)) {

                        if (get_modification_time(&path_stat) >
                                ui->last_time_app_ran &&
                            ui->last_time_app_ran > 0) {

                                update_library(path, args->wait_until_complete);
                                break;
                        }
                }
        }

        closedir(dir);

        if (args->path)
                free(args->path);
        free(args);

        return NULL;
}

// This only checks the library mtime and toplevel subfolders mtimes
void update_library_if_changed_detected(bool wait_until_complete)
{
        AppState *state = get_app_state();
        pthread_t tid;

        UpdateLibraryThreadArgs *args = malloc(sizeof(UpdateLibraryThreadArgs));
        if (args == NULL) {
                perror("malloc");
                return;
        }

        AppSettings *settings = get_app_settings();
        char expanded[KEW_PATH_MAX];
        expand_path(settings->path, expanded, KEW_PATH_MAX);

        args->path = strdup(expanded);
        if (args->path == NULL) {
                perror("strdup");
                free(args);
                return;
        }

        args->wait_until_complete = wait_until_complete;
        args->state = state;

        if (pthread_create(&tid, NULL, update_if_top_level_folders_mtimes_changed_thread, (void *)args) != 0) {
                perror("pthread_create");
                free(args->path);
                free(args);
                return;
        }

        if (wait_until_complete) {
                pthread_join(tid, NULL);
        } else {
                pthread_detach(tid); // Let the thread clean up itself
        }
}

void save_library(void)
{
        FileSystemEntry *library = get_library();
        char *filepath = get_library_file_path();

        reset_sort_library();

        write_tree_to_binary(library, filepath);

        free(filepath);
}

void library_shutdown(void)
{
        update_library_if_changed_detected(true);
        save_library();

        Model *model = get_model();
        free_tree(model->library);
}

void library_init(bool set_enqueued_status)
{
        Model *model = get_model();
        AppSettings *settings = get_app_settings();
        AppState *state = get_app_state();
        FileSystemEntry *library = NULL;

        char expanded[KEW_PATH_MAX];

        expand_path(settings->path, expanded, KEW_PATH_MAX);

        size_t len = strlen(expanded);
        if (len > 1 && (expanded[len - 1] == '/' || expanded[len - 1] == '\\') )
                expanded[len - 1] = '\0';

        char *lib_path = get_library_file_path();

        library = read_tree_from_binary(
            lib_path, expanded,
            &(state->ui.numDirectoryTreeEntries), set_enqueued_status);

        free(lib_path);

        pthread_mutex_lock(&(model->state.library_mutex));

        model->library = library;

        pthread_mutex_unlock(&(model->state.library_mutex));

        bool wait_until_complete = true;
        update_library_if_changed_detected(wait_until_complete);

        bool library_path_changed = false;
        if (model->library && strcmp(model->library->full_path, expanded) != 0)
                library_path_changed = true;

        if (model->library == NULL || model->library->children == NULL || library_path_changed) {

                char expanded[KEW_PATH_MAX];

                expand_path(settings->path, expanded, KEW_PATH_MAX);

                FileSystemEntry *tmp = create_directory_tree(expanded, &(state->ui.numDirectoryTreeEntries));

                pthread_mutex_lock(&(model->state.library_mutex));

                model->library = tmp;

                pthread_mutex_unlock(&(model->state.library_mutex));
        }

        if (model->library == NULL || model->library->children == NULL) {
                char message[KEW_PATH_MAX + 64];

                snprintf(message, KEW_PATH_MAX + 64, "No music found at %s.", settings->path);

                set_error_message(message);
        }

        if (model->library != NULL) {
                char lib_real[KEW_PATH_MAX];
                if (path_realpath(model->library->full_path, lib_real) != NULL &&
                    strcmp(lib_real, model->library->full_path) != 0)
                        set_library_real_path_if_diff(lib_real);
                else
                        set_library_real_path_if_diff(NULL);
        }
}

void enqueue_song(FileSystemEntry *child)
{
        if (!child)
                return;

        int id = child->id;

        if (id <= 0)
                id = increment_node_id();

        PlayList *unshuffled_playlist = get_unshuffled_playlist();
        PlayList *playlist = get_playlist();

        Node *node = NULL;
        create_node(&node, child->full_path, id);
        if (add_to_list(unshuffled_playlist, node) == -1)
                destroy_node(node);

        Node *node2 = NULL;
        create_node(&node2, child->full_path, id);
        if (add_to_list(playlist, node2) == -1)
                destroy_node(node2);

        child->is_enqueued = playlist->count;

        if (node->prev)
        {
                // Get the last entries enqueued number. This is the row number.
                // It can be larger than the playlist count because we don't adjust it when songs are dequeued
                // Our number has to be one larger than that number.
                FileSystemEntry *library = get_library();
                FileSystemEntry *prev_entry = find_corresponding_entry(library, node->prev->song.file_path);
                if (prev_entry)
                        child->is_enqueued = prev_entry->is_enqueued + 1;
        }

        child->parent->is_enqueued = 1;
}

void set_childrens_queued_status_on_parents(FileSystemEntry *parent, bool wanted_status)
{
        if (parent == NULL)
                return;

        bool is_enqueued = false;

        FileSystemEntry *ch = parent->children;

        while (ch != NULL) {
                if (ch->is_enqueued && !is_m3u_file(ch)) {
                        is_enqueued = true;
                        break;
                }
                ch = ch->next;
        }

        if (is_enqueued == wanted_status) {
                parent->is_enqueued = wanted_status;
        }

        parent = parent->parent;

        if (parent && parent->parent != NULL)
                set_childrens_queued_status_on_parents(parent, wanted_status);
}

void dequeue_song(FileSystemEntry *child)
{
        PlayList *unshuffled_playlist = get_unshuffled_playlist();
        PlayList *playlist = get_playlist();

        Node *node1 =
            find_last_path_in_playlist(child->full_path, unshuffled_playlist);

        Node *current = get_current_song();

        if (node1 == NULL)
                return;

        if (current != NULL && current->id == node1->id) {
                remove_currently_playing_song();
        } else {
                if (get_song_to_start_from() != NULL) {
                        set_song_to_start_from(get_list_next(node1));
                }
        }

        int id = node1->id;

        Node *node2 = find_selected_entry_by_id(playlist, id);

        if (node1 != NULL)
                delete_from_list(unshuffled_playlist, node1);

        if (node2 != NULL)
                delete_from_list(playlist, node2);

        child->is_enqueued = 0;
}

void dequeue_children(FileSystemEntry *parent)
{
        FileSystemEntry *child = parent->children;
        FileSystemEntry *library = get_library();

        while (child != NULL) {
                if (child->is_directory) {
                        if (child->children != NULL) {
                                dequeue_children(child);
                        }
                } else if (is_m3u_file(child)) {
                        if (child->is_enqueued) {
                                dequeue_m3u(child->full_path, library);
                                child->is_enqueued = 0;
                        }
                } else {
                        dequeue_song(child);
                }

                child = child->next;
        }

        set_childrens_queued_status_on_parents(parent, false);
}

int enqueue_album(FileSystemEntry *firstChild, FileSystemEntry **first_enqueued)
{
        int num_enqueued = 0;
        if (firstChild == NULL)
                return num_enqueued;

        FileSystemEntry *entry = firstChild;
        int numberOfEntries = 0;

        FileSystemEntry *discArray[MAX_SORT_SIZE] = {0};

        while (entry != NULL && numberOfEntries < MAX_SORT_SIZE) {
                if (!entry->is_directory && is_music_file(entry->name)) {
                        uint32_t disc_number = 0, track_number = 0;
                        getTrackInfo(entry->full_path, &track_number, &disc_number);

                        entry->track_number = track_number;
                        entry->disc_number = disc_number;
                        discArray[numberOfEntries] = entry;

                        numberOfEntries++;
                }
                entry = entry->next;
        }

        qsort(discArray, numberOfEntries, sizeof(FileSystemEntry *), compare_tracks_from_pointer);

        if (*first_enqueued == NULL) {
                *first_enqueued = discArray[0];
        }

        for (int i = 0; i < numberOfEntries; i++) {
                if (!discArray[i]->is_enqueued)
                {
                        enqueue_song(discArray[i]);
                        num_enqueued++;
                }
        }

        return num_enqueued;
}

int enqueue_children(FileSystemEntry *child,
                     FileSystemEntry **first_enqueued_entry,
                     bool sort)
{
        int num_enqueued = 0;

        if (!child)
                return num_enqueued;

        FileSystemEntry *parent = child->parent;

        while (child != NULL) {
                if (child->is_directory) {
                        if (child->children != NULL) {
                                int num_enq_children = enqueue_children(child->children, first_enqueued_entry, sort);

                                if (num_enq_children >= 1)
                                        child->is_enqueued = 1;

                                num_enqueued += num_enq_children;
                        }
                } else if (!is_m3u_file(child)) {

                        if (*first_enqueued_entry == NULL && !sort)
                                *first_enqueued_entry = child;

                        int num_enq_files = 0;

                        if (sort)
                        {
                                num_enq_files = enqueue_album(child, first_enqueued_entry);
                        }
                        else {
                                if (!child->is_enqueued)
                                {
                                        enqueue_song(child);
                                        num_enq_files = 1;
                                }
                        }

                        num_enqueued += num_enq_files;
                }

                child = child->next;
        }

        set_childrens_queued_status_on_parents(parent, true);

        return num_enqueued;
}

bool has_song_children(FileSystemEntry *entry)
{
        if (!entry)
                return false;

        FileSystemEntry *child = entry->children;
        int num_songs = 0;

        while (child != NULL) {
                if (!child->is_directory)
                        num_songs++;

                child = child->next;
        }

        if (num_songs == 0) {
                return false;
        }

        return true;
}

bool has_dequeued_children(FileSystemEntry *parent)
{
        FileSystemEntry *child = parent->children;

        bool isDequeued = false;
        while (child != NULL) {
                if (!child->is_enqueued) {
                        if ((!child->is_directory && !is_m3u_file(child)) ||
                            has_dequeued_children(child))
                                isDequeued = true;
                }
                child = child->next;
        }

        return isDequeued;
}

bool is_contained_within(FileSystemEntry *entry, FileSystemEntry *containing_entry)
{
        if (entry == NULL || containing_entry == NULL)
                return false;

        FileSystemEntry *tmp = entry->parent;

        while (tmp != NULL) {
                if (strcmp(tmp->full_path, containing_entry->full_path) == 0)
                        return true;

                tmp = tmp->parent;
        }

        return false;
}

// Normalize a song path to match the library's full_path prefix.
// If the library root is a symlink, the M3U may resolve to the real path
// while the library uses the symlink path — rewrite the prefix so that
// mark_as_enqueued() and find_path_in_playlist() can match correctly.
// Returns a newly allocated string; caller must g_free().
static gchar *normalize_to_library_path(const char *path, FileSystemEntry *library)
{
        gchar *canonicalized = g_canonicalize_filename(path, NULL);
        if (canonicalized == NULL)
                return NULL;

        const char *lib_real = get_library_real_path_if_diff();

        if (lib_real[0] != '\0') {
                size_t real_len = strlen(lib_real);
                if (strncmp(canonicalized, lib_real, real_len) == 0) {
                        gchar *rewritten = g_strconcat(library->full_path,
                                                       canonicalized + real_len,
                                                       NULL);
                        g_free(canonicalized);
                        return rewritten;
                }
        }

        return canonicalized;
}

void enqueue_m3u(const char *filepath, FileSystemEntry *library,
                 Node **first_enqueued_node, bool dont_dequeue)
{
        PlayList *unshuffled_playlist = get_unshuffled_playlist();
        PlayList *playlist = get_playlist();

        bool root_was_enqueued = library->is_enqueued;

        GError *error = NULL;
        gchar *contents;

        char filename[KEW_PATH_MAX];
        expand_path(filepath, filename, KEW_PATH_MAX);

        if (!g_file_get_contents(filename, &contents, NULL, &error)) {
                g_clear_error(&error);
                return;
        }

        gchar *directory = g_path_get_dirname(filename);
        gchar **lines = g_strsplit(contents, "\n", -1);

        int list_row_num = 1;

        for (gint i = 0; lines[i] != NULL; i++) {
                gchar *line = lines[i];

                line = g_strdelimit(line, "\r", '\0');
                gchar *trimmed_line = g_strstrip(line);

                if (trimmed_line[0] == '#' || trimmed_line[0] == '\0')
                        continue;

                gchar *songPath;

                if (g_path_is_absolute(trimmed_line)) {
                        songPath = g_strdup(trimmed_line);
                } else {
                        songPath = g_build_filename(directory, trimmed_line, NULL);
                }

                if (songPath == NULL)
                        continue;

                gchar *normalized = normalize_to_library_path(songPath, library);
                g_free(songPath);

                if (normalized == NULL)
                        continue;

                if (exists_file(normalized) < 0) {
                        g_free(normalized);
                        continue;
                }

                Node *found = find_path_in_playlist(normalized, playlist);

                if (dont_dequeue && found) {
                        if (*first_enqueued_node == NULL)
                                *first_enqueued_node = found;
                } else {

                        // Don't add songs that are already enqueued
                        if (find_path_in_playlist(normalized, playlist) != NULL) {
                                g_free(normalized);
                                continue;
                        }

                        int id = mark_as_enqueued(library, normalized, list_row_num);
                        list_row_num++;

                        if (id <= 0)
                                id = increment_node_id();

                        Node *node1 = NULL;
                        create_node(&node1, normalized, id);
                        if (add_to_list(unshuffled_playlist, node1) == -1) {
                                destroy_node(node1);
                                g_free(normalized);
                                continue;
                        }

                        Node *node2 = NULL;
                        create_node(&node2, normalized, id);
                        if (add_to_list(playlist, node2) == -1)
                                destroy_node(node2);


                        if (*first_enqueued_node == NULL)
                                *first_enqueued_node = node1;
                }

                g_free(normalized);
        }

        if (!root_was_enqueued)
                library->is_enqueued = false;

        g_free(directory);
        g_strfreev(lines);
        g_free(contents);
}

void dequeue_m3u(const char *filepath, FileSystemEntry *library)
{
        PlayList *unshuffled_playlist = get_unshuffled_playlist();
        PlayList *playlist = get_playlist();

        GError *error = NULL;
        gchar *contents;

        char filename[KEW_PATH_MAX];
        expand_path(filepath, filename, KEW_PATH_MAX);

        if (!g_file_get_contents(filename, &contents, NULL, &error)) {
                g_clear_error(&error);
                return;
        }

        gchar *directory = g_path_get_dirname(filename);
        gchar **lines = g_strsplit(contents, "\n", -1);

        for (gint i = 0; lines[i] != NULL; i++) {
                gchar *line = lines[i];

                line = g_strdelimit(line, "\r", '\0');
                gchar *trimmed_line = g_strstrip(line);

                if (trimmed_line[0] == '#' || trimmed_line[0] == '\0')
                        continue;

                gchar *songPath;

                if (g_path_is_absolute(trimmed_line)) {
                        songPath = g_strdup(trimmed_line);
                } else {
                        songPath = g_build_filename(directory, trimmed_line, NULL);
                }

                if (songPath == NULL)
                        continue;

                gchar *normalized = normalize_to_library_path(songPath, library);
                g_free(songPath);

                if (normalized == NULL)
                        continue;

                // Remove one instance of this path — symmetric with enqueue_m3u()
                // which adds one node per path.
                Node *node1 = find_last_path_in_playlist(normalized, unshuffled_playlist);

                if (node1 != NULL) {
                        Node *current = get_current_song();

                        if (current != NULL && current->id == node1->id) {
                                remove_currently_playing_song();
                        } else {
                                if (get_song_to_start_from() != NULL)
                                        set_song_to_start_from(get_list_next(node1));
                        }

                        int id = node1->id;
                        Node *node2 = find_selected_entry_by_id(playlist, id);

                        delete_from_list(unshuffled_playlist, node1);

                        if (node2 != NULL)
                                delete_from_list(playlist, node2);

                        // mark_as_dequeued() checks siblings before clearing
                        // parent flags, so other enqueued entries are unaffected.
                        mark_as_dequeued(library, normalized);
                }

                g_free(normalized);
        }

        g_free(directory);
        g_strfreev(lines);
        g_free(contents);
}
