#define G_LOG_DOMAIN "fsearch-database-sort"

#include "fsearch_database_sort.h"

#include "fsearch_database_entry.h"

static bool
sort_order_affects_folders(FsearchDatabaseIndexProperty sort_order) {
    if (sort_order == DATABASE_INDEX_PROPERTY_EXTENSION || sort_order == DATABASE_INDEX_PROPERTY_FILETYPE) {
        // Folders are stored in a different array than files, so they all have the same sort_order and extension
        // (none), therefore we don't need to sort them in such cases.
        return false;
    }
    return true;
}

static DynamicArrayCompareDataFunc
get_sort_func(FsearchDatabaseIndexProperty sort_order) {
    DynamicArrayCompareDataFunc func = NULL;
    switch (sort_order) {
    case DATABASE_INDEX_PROPERTY_NAME:
        func = (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_name;
        break;
    case DATABASE_INDEX_PROPERTY_PATH:
        func = (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_path;
        break;
    case DATABASE_INDEX_PROPERTY_PATH_FULL:
        func = (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_full_path;
        break;
    case DATABASE_INDEX_PROPERTY_SIZE:
        func = (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_size;
        break;
    case DATABASE_INDEX_PROPERTY_EXTENSION:
        func = (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_extension;
        break;
    case DATABASE_INDEX_PROPERTY_FILETYPE:
        func = (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_type;
        break;
    case DATABASE_INDEX_PROPERTY_MODIFICATION_TIME:
        func = (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_modification_time;
        break;
    default:
        func = (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_position;
    }
    return func;
}

static DynamicArray *
get_entries_sorted_from_reference_list(DynamicArray *old_list, DynamicArray *sorted_reference_list) {
    const uint32_t num_items = darray_get_num_items(old_list);
    DynamicArray *new = darray_new(num_items);
    for (uint32_t i = 0; i < num_items; ++i) {
        FsearchDatabaseEntry *entry = darray_get_item(old_list, i);
        db_entry_set_mark(entry, 1);
    }
    uint32_t num_marked_found = 0;
    const uint32_t num_items_in_sorted_reference_list = darray_get_num_items(sorted_reference_list);
    for (uint32_t i = 0; i < num_items_in_sorted_reference_list && num_marked_found < num_items; ++i) {
        FsearchDatabaseEntry *entry = darray_get_item(sorted_reference_list, i);
        if (db_entry_get_mark(entry)) {
            db_entry_set_mark(entry, 0);
            darray_add_item(new, entry);
            num_marked_found++;
        }
    }
    return new;
}

static DynamicArray *
sort_entries(DynamicArray *entries_in,
             DynamicArrayCompareDataFunc sort_func,
             GCancellable *cancellable,
             bool parallel_sort,
             void *data) {
    DynamicArray *entries = darray_copy(entries_in);
    if (parallel_sort) {
        darray_sort_multi_threaded(entries, sort_func, cancellable, data);
    }
    else {
        darray_sort(entries, sort_func, cancellable, data);
    }
    return entries;
}

static DynamicArray *
fast_sort(FsearchDatabaseIndexProperty new_sort_order, DynamicArray *entries_in, DynamicArray *fast_sort_index) {
    if (darray_get_num_items(entries_in) == darray_get_num_items(fast_sort_index)) {
        // We're matching everything, and we have the entries already sorted in our index.
        // So we can just return references to the sorted indices.
        return darray_ref(fast_sort_index);
    }
    else {
        // Another fast path. First we mark all entries we have currently in the view, then we walk the sorted
        // index in order and add all marked entries to a new array.
        return get_entries_sorted_from_reference_list(entries_in, fast_sort_index);
    }
}

void
fsearch_database_sort_results(FsearchDatabaseIndexProperty old_sort_order,
                              FsearchDatabaseIndexProperty old_secondary_sort_order,
                              FsearchDatabaseIndexProperty new_sort_order,
                              DynamicArray *files_in,
                              DynamicArray *folders_in,
                              DynamicArray *files_fast_sort_index,
                              DynamicArray *folders_fast_sort_index,
                              DynamicArray **files_out,
                              DynamicArray **folders_out,
                              FsearchDatabaseIndexProperty *sort_order_out,
                              FsearchDatabaseIndexProperty *secondary_sort_order_out,
                              GCancellable *cancellable) {
    g_return_if_fail(files_in);
    g_return_if_fail(folders_in);
    g_return_if_fail(files_out);
    g_return_if_fail(folders_out);
    g_return_if_fail(sort_order_out);

    if (old_sort_order == new_sort_order) {
        // Sort order didn't change, use the old results
        *files_out = darray_ref(files_in);
        *folders_out = darray_ref(folders_in);
        *sort_order_out = new_sort_order;
        *secondary_sort_order_out = old_secondary_sort_order;
        return;
    }

    if (files_fast_sort_index && folders_fast_sort_index) {
        // Use the fast-sort indices
        *files_out = fast_sort(new_sort_order, files_in, files_fast_sort_index);
        *folders_out = fast_sort(new_sort_order, folders_in, folders_fast_sort_index);
        *sort_order_out = new_sort_order;
        // Fast sorted indexes don't have a secondary sort order
        *secondary_sort_order_out = DATABASE_INDEX_PROPERTY_NONE;
        return;
    }

    DynamicArrayCompareDataFunc func = get_sort_func(new_sort_order);
    bool parallel_sort = true;

    FsearchDatabaseEntryCompareContext *comp_ctx = db_entry_compare_context_new(NULL, NULL, NULL);
    if (new_sort_order == DATABASE_INDEX_PROPERTY_FILETYPE) {
        // Sorting by type can be really slow, because it accesses the filesystem to determine the type of files
        // To mitigate that issue to a certain degree we cache the filetype for each file
        // To avoid duplicating the filetype in memory for each file, we also store each filetype only once in
        // a separate hash table.
        // We also disable parallel sorting.
        parallel_sort = false;
    }

    if (sort_order_affects_folders(new_sort_order)) {
        *folders_out = sort_entries(folders_in, func, cancellable, parallel_sort, comp_ctx);
    }
    else {
        *folders_out = darray_copy(folders_in);
    }
    *files_out = sort_entries(files_in, func, cancellable, parallel_sort, comp_ctx);
    *sort_order_out = new_sort_order;
    *secondary_sort_order_out = old_sort_order;

    g_clear_pointer(&comp_ctx, db_entry_compare_context_free);

    if (g_cancellable_is_cancelled(cancellable)) {
        g_clear_pointer(folders_out, darray_unref);
        g_clear_pointer(files_out, darray_unref);
        *sort_order_out = old_sort_order;
        *secondary_sort_order_out = old_secondary_sort_order;
    }
}

static int
compare_by_db_index(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b) {
    const uint32_t idx_a = db_entry_get_db_index(*a);
    const uint32_t idx_b = db_entry_get_db_index(*b);
    return (idx_a > idx_b) - (idx_a < idx_b);
}

static int
compare_by_path(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b) {
    const int res = db_entry_compare_entries_by_path(a, b);
    if (G_LIKELY(res != 0)) {
        return res;
    }

    return compare_by_db_index(a, b);
}

static int
compare_by_full_path(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b) {
    const int res = db_entry_compare_entries_by_full_path(a, b);
    if (G_LIKELY(res != 0)) {
        return res;
    }

    return compare_by_db_index(a, b);
}

static int
compare_by_name(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b) {
    const int res = db_entry_compare_entries_by_name(a, b);
    if (G_LIKELY(res != 0)) {
        return res;
    }
    else {
        return compare_by_path(a, b);
    }
}

static int
compare_by_size(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b) {
    const int res = db_entry_compare_entries_by_size(a, b);
    if (G_LIKELY(res != 0)) {
        return res;
    }
    return compare_by_name(a, b);
}

static int
compare_by_modification_time(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b) {
    const int res = db_entry_compare_entries_by_modification_time(a, b);
    if (G_LIKELY(res != 0)) {
        return res;
    }
    else {
        return compare_by_name(a, b);
    }
}

static int
compare_by_extension(FsearchDatabaseEntry **a, FsearchDatabaseEntry **b) {
    const int res = db_entry_compare_entries_by_extension(a, b);
    if (G_LIKELY(res != 0)) {
        return res;
    }
    else {
        return compare_by_name(a, b);
    }
}

DynamicArrayCompareDataFunc
fsearch_database_sort_get_compare_func_for_property(FsearchDatabaseIndexProperty property, bool is_dir) {
    switch (property) {
    case DATABASE_INDEX_PROPERTY_NAME:
        return (DynamicArrayCompareDataFunc)compare_by_name;
    case DATABASE_INDEX_PROPERTY_PATH:
        return (DynamicArrayCompareDataFunc)compare_by_path;
    case DATABASE_INDEX_PROPERTY_PATH_FULL:
        return (DynamicArrayCompareDataFunc)compare_by_full_path;
    case DATABASE_INDEX_PROPERTY_SIZE:
        return (DynamicArrayCompareDataFunc)compare_by_size;
    case DATABASE_INDEX_PROPERTY_MODIFICATION_TIME:
        return (DynamicArrayCompareDataFunc)compare_by_modification_time;
    case DATABASE_INDEX_PROPERTY_EXTENSION:
        // Folders don't have extensions and hence are simply sorted by name
        if (!is_dir) {
            return (DynamicArrayCompareDataFunc)compare_by_extension;
        }
        else {
            return (DynamicArrayCompareDataFunc)compare_by_name;
        }
    case DATABASE_INDEX_PROPERTY_FILETYPE:
        return (DynamicArrayCompareDataFunc)db_entry_compare_entries_by_type;
    default:
        return NULL;
    }
}