import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type } from "../script.js";
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, delay, getCharaFilename, deepClone } from "./utils.js";
import { getContext } from "./extensions.js";
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./authors-note.js";
import { registerSlashCommand } from "./slash-commands.js";
import { deviceInfo } from "./RossAscends-mods.js";

export {
    world_info,
    world_info_budget,
    world_info_depth,
    world_info_recursive,
    world_info_case_sensitive,
    world_info_match_whole_words,
    world_info_character_strategy,
    world_names,
    checkWorldInfo,
    deleteWorldInfo,
    setWorldInfoSettings,
    getWorldInfoPrompt,
}

const world_info_insertion_strategy = {
    evenly: 0,
    character_first: 1,
    global_first: 2,
};

let world_info = {};
let selected_world_info = [];
let world_names;
let world_info_depth = 2;
let world_info_budget = 25;
let world_info_recursive = false;
let world_info_case_sensitive = false;
let world_info_match_whole_words = false;
let world_info_character_strategy = world_info_insertion_strategy.character_first;
const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), 1000);
const saveSettingsDebounced = debounce(() => {
    Object.assign(world_info, { globalSelect: selected_world_info })
    saveSettings()
}, 1000);
const sortFn = (a, b) => b.order - a.order;

const world_info_position = {
    before: 0,
    after: 1,
    ANTop: 2,
    ANBottom: 3,

};

const worldInfoCache = {};

async function getWorldInfoPrompt(chat2, maxContext) {
    let worldInfoString = "", worldInfoBefore = "", worldInfoAfter = "";

    const activatedWorldInfo = await checkWorldInfo(chat2, maxContext);
    worldInfoBefore = activatedWorldInfo.worldInfoBefore;
    worldInfoAfter = activatedWorldInfo.worldInfoAfter;
    worldInfoString = worldInfoBefore + worldInfoAfter;

    return { worldInfoString, worldInfoBefore, worldInfoAfter };
}

function setWorldInfoSettings(settings, data) {
    if (settings.world_info_depth !== undefined)
        world_info_depth = Number(settings.world_info_depth);
    if (settings.world_info_budget !== undefined)
        world_info_budget = Number(settings.world_info_budget);
    if (settings.world_info_recursive !== undefined)
        world_info_recursive = Boolean(settings.world_info_recursive);
    if (settings.world_info_case_sensitive !== undefined)
        world_info_case_sensitive = Boolean(settings.world_info_case_sensitive);
    if (settings.world_info_match_whole_words !== undefined)
        world_info_match_whole_words = Boolean(settings.world_info_match_whole_words);
    if (settings.world_info_character_strategy !== undefined)
        world_info_character_strategy = Number(settings.world_info_character_strategy);

    // Migrate old settings
    if (world_info_budget > 100) {
        world_info_budget = 25;
    }

    // Reset selected world from old string and delete old keys
    // TODO: Remove next release
    const existingWorldInfo = settings.world_info;
    if (typeof existingWorldInfo === "string") {
        delete settings.world_info;
        selected_world_info = [existingWorldInfo];
    } else if (Array.isArray(existingWorldInfo)) {
        delete settings.world_info;
        selected_world_info = existingWorldInfo;
    }

    world_info = settings.world_info ?? {}

    $("#world_info_depth_counter").text(world_info_depth);
    $("#world_info_depth").val(world_info_depth);

    $("#world_info_budget_counter").text(world_info_budget);
    $("#world_info_budget").val(world_info_budget);

    $("#world_info_recursive").prop('checked', world_info_recursive);
    $("#world_info_case_sensitive").prop('checked', world_info_case_sensitive);
    $("#world_info_match_whole_words").prop('checked', world_info_match_whole_words);

    $(`#world_info_character_strategy option[value='${world_info_character_strategy}']`).prop('selected', true);
    $("#world_info_character_strategy").val(world_info_character_strategy);

    world_names = data.world_names?.length ? data.world_names : [];

    // Add to existing selected WI if it exists
    selected_world_info = selected_world_info.concat(settings.world_info?.globalSelect?.filter((e) => world_names.includes(e)) ?? []);

    if (world_names.length > 0) {
        $("#world_info").empty();
    }

    world_names.forEach((item, i) => {
        $("#world_info").append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
        $("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
    });

    $("#world_editor_select").trigger("change");

    // Update settings
    saveSettingsDebounced();
}

// World Info Editor
async function showWorldEditor(name) {
    if (!name) {
        hideWorldEditor();
        return;
    }

    const wiData = await loadWorldInfoData(name);
    displayWorldEntries(name, wiData);
}

async function loadWorldInfoData(name) {
    if (!name) {
        return;
    }

    if (worldInfoCache[name]) {
        return worldInfoCache[name];
    }

    const response = await fetch("/getworldinfo", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({ name: name }),
        cache: 'no-cache',
    });

    if (response.ok) {
        const data = await response.json();
        worldInfoCache[name] = data;
        return data;
    }

    return null;
}

async function updateWorldInfoList() {
    var result = await fetch("/getsettings", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({}),
    });

    if (result.ok) {
        var data = await result.json();
        world_names = data.world_names?.length ? data.world_names : [];
        $("#world_info").find('option[value!=""]').remove();
        $("#world_editor_select").find('option[value!=""]').remove();

        world_names.forEach((item, i) => {
            $("#world_info").append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
            $("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
        });
    }
}

function hideWorldEditor() {
    displayWorldEntries(null, null);
}

function getWIElement(name) {
    const wiElement = $("#world_info").children().filter(function () {
        return $(this).text().toLowerCase() === name.toLowerCase()
    });

    return wiElement;
}

function nullWorldInfo() {
    toastr.info("Create or import a new World Info file first.", "World Info is not set", { timeOut: 10000, preventDuplicates: true });
}

function displayWorldEntries(name, data) {
    $("#world_popup_entries_list").empty().show();

    if (!data || !("entries" in data)) {
        $("#world_popup_new").off('click').on('click', nullWorldInfo);
        $("#world_popup_name_button").off('click').on('click', nullWorldInfo);
        $("#world_popup_export").off('click').on('click', nullWorldInfo);
        $("#world_popup_delete").off('click').on('click', nullWorldInfo);
        $("#world_popup_entries_list").hide();
        return;
    }

    // Convert the data.entries object into an array
    const entriesArray = Object.keys(data.entries).map(uid => {
        const entry = data.entries[uid];
        entry.displayIndex = entry.displayIndex ?? entry.uid;
        return entry;
    });

    // Sort the entries array by displayIndex and uid
    entriesArray.sort((a, b) => a.displayIndex - b.displayIndex || a.uid - b.uid);

    // Loop through the sorted array and call appendWorldEntry
    for (const entry of entriesArray) {
        appendWorldEntry(name, data, entry);
    }

    $("#world_popup_new").off('click').on('click', () => {
        createWorldInfoEntry(name, data);
    });

    $("#world_popup_name_button").off('click').on('click', async () => {
        await renameWorldInfo(name, data);
    });

    $("#world_popup_export").off('click').on('click', () => {
        if (name && data) {
            const jsonValue = JSON.stringify(data);
            const fileName = `${name}.json`;
            download(jsonValue, fileName, "application/json");
        }
    });

    $("#world_popup_delete").off('click').on('click', async () => {
        const confirmation = await callPopup(`<h3>Delete the World/Lorebook: "${name}"?</h3>This action is irreversible!`, "confirm");

        if (!confirmation) {
            return;
        }

        if (world_info.charLore) {
            world_info.charLore.forEach((charLore, index) => {
                if (charLore.extraBooks?.includes(name)) {
                    const tempCharLore = charLore.extraBooks.filter((e) => e !== name);
                    if (tempCharLore.length === 0) {
                        world_info.charLore.splice(index, 1);
                    } else {
                        charLore.extraBooks = tempCharLore;
                    }
                }
            });

            saveSettingsDebounced();
        }

        // Selected world_info automatically refreshes
        await deleteWorldInfo(name);
    });

    // Check if a sortable instance exists
    if ($('#world_popup_entries_list').sortable('instance') !== undefined) {
        // Destroy the instance
        $('#world_popup_entries_list').sortable('destroy');
    }

    $("#world_popup_entries_list").sortable({
        handle: ".drag-handle",
        stop: async function (event, ui) {
            $('#world_popup_entries_list .world_entry').each(function (index) {
                const uid = $(this).data('uid');

                // Update the display index in the data array
                const item = data.entries[uid];

                if (!item) {
                    console.debug(`Could not find entry with uid ${uid}`);
                    return;
                }

                item.displayIndex = index;
                setOriginalDataValue(data, uid, 'extensions.display_index', index);
            });

            console.table(Object.keys(data.entries).map(uid => data.entries[uid]).map(x => ({ uid: x.uid, key: x.key.join(','), displayIndex: x.displayIndex })));

            await saveWorldInfo(name, data, true);
        }
    });
    //$("#world_popup_entries_list").disableSelection();
}

function setOriginalDataValue(data, uid, key, value) {
    if (data.originalData && Array.isArray(data.originalData.entries)) {
        let originalEntry = data.originalData.entries.find(x => x.uid === uid);

        if (!originalEntry) {
            return;
        }

        const keyParts = key.split('.');
        let currentObject = originalEntry;

        for (let i = 0; i < keyParts.length - 1; i++) {
            const part = keyParts[i];

            if (!currentObject.hasOwnProperty(part)) {
                currentObject[part] = {};
            }

            currentObject = currentObject[part];
        }

        currentObject[keyParts[keyParts.length - 1]] = value;
    }
}

function deleteOriginalDataValue(data, uid) {
    if (data.originalData && Array.isArray(data.originalData.entries)) {
        const originalIndex = data.originalData.entries.findIndex(x => x.uid === uid);

        if (originalIndex >= 0) {
            data.originalData.entries.splice(originalIndex, 1);
        }
    }
}

function appendWorldEntry(name, data, entry) {
    const template = $("#entry_edit_template .world_entry").clone();
    template.data("uid", entry.uid);

    // key
    const keyInput = template.find('textarea[name="key"]');
    keyInput.data("uid", entry.uid);
    keyInput.on("click", function (event) {
        // Prevent closing the drawer on clicking the input
        event.stopPropagation();
    });

    keyInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).val();
        resetScrollHeight(this);
        data.entries[uid].key = value
            .split(",")
            .map((x) => x.trim())
            .filter((x) => x);

        setOriginalDataValue(data, uid, "keys", data.entries[uid].key);
        saveWorldInfo(name, data);
    });
    keyInput.val(entry.key.join(",")).trigger("input");
    initScrollHeight(keyInput);

    // logic AND/NOT
    const selectiveLogicDropdown = template.find('select[name="entryLogicType"]');
    selectiveLogicDropdown.data("uid", entry.uid);

    selectiveLogicDropdown.on("input", function () {
        const uid = $(this).data("uid");
        const value = Number($(this).val());
        console.debug(`logic for ${entry.uid} set to ${value}`)
        data.entries[uid].selectiveLogic = !isNaN(value) ? value : 0;
        setOriginalDataValue(data, uid, "selectiveLogic", data.entries[uid].selectiveLogic);
        saveWorldInfo(name, data);

    });
    template
        .find(`select[name="entryLogicType"] option[value=${entry.selectiveLogic}]`)
        .prop("selected", true)
        .trigger("input");

    // keysecondary
    const keySecondaryInput = template.find('textarea[name="keysecondary"]');
    keySecondaryInput.data("uid", entry.uid);
    keySecondaryInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).val();
        resetScrollHeight(this);
        data.entries[uid].keysecondary = value
            .split(",")
            .map((x) => x.trim())
            .filter((x) => x);

        setOriginalDataValue(data, uid, "secondary_keys", data.entries[uid].keysecondary);
        saveWorldInfo(name, data);
    });

    keySecondaryInput.val(entry.keysecondary.join(",")).trigger("input");
    initScrollHeight(keySecondaryInput);

    // comment
    const commentInput = template.find('textarea[name="comment"]');
    const commentToggle = template.find('input[name="addMemo"]');
    commentInput.data("uid", entry.uid);
    commentInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).val();
        data.entries[uid].comment = value;

        setOriginalDataValue(data, uid, "comment", data.entries[uid].comment);
        saveWorldInfo(name, data);
    });
    commentToggle.data("uid", entry.uid);
    commentToggle.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).prop("checked");
        //console.log(value)
        const commentContainer = $(this)
            .closest(".world_entry")
            .find(".commentContainer");
        data.entries[uid].addMemo = value;
        saveWorldInfo(name, data);
        value ? commentContainer.show() : commentContainer.hide();
    });

    commentInput.val(entry.comment).trigger("input");
    commentToggle.prop("checked", true /* entry.addMemo */).trigger("input");
    commentToggle.parent().hide()

    // content
    const countTokensDebounced = debounce(function (that, value) {
        const numberOfTokens = getTokenCount(value);
        $(that)
            .closest(".world_entry")
            .find(".world_entry_form_token_counter")
            .text(numberOfTokens);
    }, 1000);

    const contentInput = template.find('textarea[name="content"]');
    contentInput.data("uid", entry.uid);
    contentInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).val();
        data.entries[uid].content = value;

        setOriginalDataValue(data, uid, "content", data.entries[uid].content);
        saveWorldInfo(name, data);

        // count tokens
        countTokensDebounced(this, value);
    });
    contentInput.val(entry.content).trigger("input");
    //initScrollHeight(contentInput);

    // selective
    const selectiveInput = template.find('input[name="selective"]');
    selectiveInput.data("uid", entry.uid);
    selectiveInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).prop("checked");
        data.entries[uid].selective = value;

        setOriginalDataValue(data, uid, "selective", data.entries[uid].selective);
        saveWorldInfo(name, data);

        const keysecondary = $(this)
            .closest(".world_entry")
            .find(".keysecondary");

        const keysecondarytextpole = $(this)
            .closest(".world_entry")
            .find(".keysecondarytextpole");

        const keyprimarytextpole = $(this)
            .closest(".world_entry")
            .find(".keyprimarytextpole");

        const keyprimaryHeight = keyprimarytextpole.outerHeight();
        keysecondarytextpole.css('height', keyprimaryHeight + 'px');

        value ? keysecondary.show() : keysecondary.hide();

    });
    //forced on, ignored if empty
    selectiveInput.prop("checked", true /* entry.selective */).trigger("input");
    selectiveInput.parent().hide();


    // constant
    const constantInput = template.find('input[name="constant"]');
    constantInput.data("uid", entry.uid);
    constantInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).prop("checked");
        data.entries[uid].constant = value;
        setOriginalDataValue(data, uid, "constant", data.entries[uid].constant);
        saveWorldInfo(name, data);
    });
    constantInput.prop("checked", entry.constant).trigger("input");

    // order
    const orderInput = template.find('input[name="order"]');
    orderInput.data("uid", entry.uid);
    orderInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = Number($(this).val());

        data.entries[uid].order = !isNaN(value) ? value : 0;
        setOriginalDataValue(data, uid, "insertion_order", data.entries[uid].order);
        saveWorldInfo(name, data);
    });
    orderInput.val(entry.order).trigger("input");

    // probability
    if (entry.probability === undefined) {
        entry.probability = null;
    }

    const probabilityInput = template.find('input[name="probability"]');
    probabilityInput.data("uid", entry.uid);
    probabilityInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = parseInt($(this).val());

        data.entries[uid].probability = !isNaN(value) ? value : null;

        // Clamp probability to 0-100
        if (data.entries[uid].probability !== null) {
            data.entries[uid].probability = Math.min(100, Math.max(0, data.entries[uid].probability));

            if (data.entries[uid].probability !== value) {
                $(this).val(data.entries[uid].probability);
            }
        }

        setOriginalDataValue(data, uid, "extensions.probability", data.entries[uid].probability);
        saveWorldInfo(name, data);
    });
    probabilityInput.val(entry.probability).trigger("input");

    // probability toggle
    if (entry.useProbability === undefined) {
        entry.useProbability = false;
    }

    const probabilityToggle = template.find('input[name="useProbability"]');
    probabilityToggle.data("uid", entry.uid);
    probabilityToggle.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).prop("checked");
        data.entries[uid].useProbability = value;
        const probabilityContainer = $(this)
            .closest(".world_entry")
            .find(".probabilityContainer");
        saveWorldInfo(name, data);
        value ? probabilityContainer.show() : probabilityContainer.hide();

        if (value && data.entries[uid].probability === null) {
            data.entries[uid].probability = 100;
        }

        if (!value) {
            data.entries[uid].probability = null;
        }

        probabilityInput.val(data.entries[uid].probability).trigger("input");
    });
    //forced on, 100% by default
    probabilityToggle.prop("checked", true /* entry.useProbability */).trigger("input");
    probabilityToggle.parent().hide();

    // position
    if (entry.position === undefined) {
        entry.position = 0;
    }

    const positionInput = template.find('select[name="position"]');
    positionInput.data("uid", entry.uid);
    positionInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = Number($(this).val());
        data.entries[uid].position = !isNaN(value) ? value : 0;
        // Spec v2 only supports before_char and after_char
        setOriginalDataValue(data, uid, "position", data.entries[uid].position == 0 ? 'before_char' : 'after_char');
        // Write the original value as extensions field
        setOriginalDataValue(data, uid, "extensions.position", data.entries[uid].position);
        saveWorldInfo(name, data);
    });

    template
        .find(`select[name="position"] option[value=${entry.position}]`)
        .prop("selected", true)
        .trigger("input");

    // display uid
    template.find(".world_entry_form_uid_value").text(entry.uid);

    // disable
    const disableInput = template.find('input[name="disable"]');
    disableInput.data("uid", entry.uid);
    disableInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).prop("checked");
        data.entries[uid].disable = value;
        setOriginalDataValue(data, uid, "enabled", !data.entries[uid].disable);
        saveWorldInfo(name, data);
    });
    disableInput.prop("checked", entry.disable).trigger("input");

    const excludeRecursionInput = template.find('input[name="exclude_recursion"]');
    excludeRecursionInput.data("uid", entry.uid);
    excludeRecursionInput.on("input", function () {
        const uid = $(this).data("uid");
        const value = $(this).prop("checked");
        data.entries[uid].excludeRecursion = value;
        setOriginalDataValue(data, uid, "extensions.exclude_recursion", data.entries[uid].excludeRecursion);
        saveWorldInfo(name, data);
    });
    excludeRecursionInput.prop("checked", entry.excludeRecursion).trigger("input");

    // delete button
    const deleteButton = template.find("input.delete_entry_button");
    deleteButton.data("uid", entry.uid);
    deleteButton.on("click", function () {
        const uid = $(this).data("uid");
        deleteWorldInfoEntry(data, uid);
        deleteOriginalDataValue(data, uid);
        $(this).closest(".world_entry").remove();
        saveWorldInfo(name, data);
    });

    template.appendTo("#world_popup_entries_list");
    template.find('.inline-drawer-content').css('display', 'none'); //entries start collapsed

    return template;
}

async function deleteWorldInfoEntry(data, uid) {
    if (!data || !("entries" in data)) {
        return;
    }

    delete data.entries[uid];
}

function createWorldInfoEntry(name, data) {
    const newEntryTemplate = {
        key: [],
        keysecondary: [],
        comment: "",
        content: "",
        constant: false,
        selective: true,
        selectiveLogic: 0,
        addMemo: false,
        order: 100,
        position: 0,
        disable: false,
        excludeRecursion: false,
        probability: 100,
        useProbability: true,
    };
    const newUid = getFreeWorldEntryUid(data);

    if (!Number.isInteger(newUid)) {
        console.error("Couldn't assign UID to a new entry");
        return;
    }

    const newEntry = { uid: newUid, ...newEntryTemplate };
    data.entries[newUid] = newEntry;

    const entryTemplate = appendWorldEntry(name, data, newEntry);
    entryTemplate.get(0).scrollIntoView({ behavior: "smooth" });
}

async function _save(name, data) {
    const response = await fetch("/editworldinfo", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({ name: name, data: data }),
    });
}

async function saveWorldInfo(name, data, immediately) {
    if (!name || !data) {
        return;
    }

    delete worldInfoCache[name];

    if (immediately) {
        return await _save(name, data);
    }

    saveWorldDebounced(name, data);
}

async function renameWorldInfo(name, data) {
    const oldName = name;
    const newName = await callPopup("<h3>Rename World Info</h3>Enter a new name:", 'input', oldName);

    if (oldName === newName || !newName) {
        console.debug("World info rename cancelled");
        return;
    }

    const entryPreviouslySelected = selected_world_info.findIndex((e) => e === oldName);

    await saveWorldInfo(newName, data, true);
    await deleteWorldInfo(oldName);

    const existingCharLores = world_info.charLore?.filter((e) => e.extraBooks.includes(oldName));
    if (existingCharLores && existingCharLores.length > 0) {
        existingCharLores.forEach((charLore) => {
            const tempCharLore = charLore.extraBooks.filter((e) => e !== oldName);
            tempCharLore.push(newName);
            charLore.extraBooks = tempCharLore;
        });
        saveSettingsDebounced();
    }

    if (entryPreviouslySelected !== -1) {
        const wiElement = getWIElement(newName);
        wiElement.prop("selected", true);
        $("#world_info").trigger('change');
    }

    const selectedIndex = world_names.indexOf(newName);
    if (selectedIndex !== -1) {
        $('#world_editor_select').val(selectedIndex).trigger('change');
    }
}

async function deleteWorldInfo(worldInfoName) {
    if (!world_names.includes(worldInfoName)) {
        return;
    }

    const response = await fetch("/deleteworldinfo", {
        method: "POST",
        headers: getRequestHeaders(),
        body: JSON.stringify({ name: worldInfoName }),
    });

    if (response.ok) {
        const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName);
        if (existingWorldIndex !== -1) {
            selected_world_info.splice(existingWorldIndex, 1);
            saveSettingsDebounced();
        }

        await updateWorldInfoList();
        $('#world_editor_select').trigger('change');

        if ($('#character_world').val() === worldInfoName) {
            $('#character_world').val('').trigger('change');
            setWorldInfoButtonClass(undefined, false);
            if (menu_type != 'create') {
                saveCharacterDebounced();
            }
        }
    }
}

function getFreeWorldEntryUid(data) {
    if (!data || !("entries" in data)) {
        return null;
    }

    const MAX_UID = 1_000_000; // <- should be safe enough :)
    for (let uid = 0; uid < MAX_UID; uid++) {
        if (uid in data.entries) {
            continue;
        }
        return uid;
    }

    return null;
}

function getFreeWorldName() {
    const MAX_FREE_NAME = 100_000;
    for (let index = 1; index < MAX_FREE_NAME; index++) {
        const newName = `New World (${index})`;
        if (world_names.includes(newName)) {
            continue;
        }
        return newName;
    }

    return undefined;
}

async function createNewWorldInfo(worldInfoName) {
    const worldInfoTemplate = { entries: {} };

    if (!worldInfoName) {
        return;
    }

    await saveWorldInfo(worldInfoName, worldInfoTemplate, true);
    await updateWorldInfoList();

    const selectedIndex = world_names.indexOf(worldInfoName);
    if (selectedIndex !== -1) {
        $('#world_editor_select').val(selectedIndex).trigger('change');
    } else {
        hideWorldEditor();
    }
}

// Gets a string that respects the case sensitivity setting
function transformString(str) {
    return world_info_case_sensitive ? str : str.toLowerCase();
}

async function getCharacterLore() {
    const character = characters[this_chid];
    const name = character?.name;
    let worldsToSearch = new Set();

    const baseWorldName = character?.data?.extensions?.world;
    if (baseWorldName) {
        worldsToSearch.add(baseWorldName);
    } else {
        console.debug(`Character ${name}'s base world could not be found or is empty! Skipping...`)
        return [];
    }

    // TODO: Maybe make the utility function not use the window context?
    const fileName = getCharaFilename(this_chid);
    const extraCharLore = world_info.charLore?.find((e) => e.name === fileName);
    if (extraCharLore) {
        worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]);
    }

    let entries = [];
    for (const worldName of worldsToSearch) {
        if (selected_world_info.includes(worldName)) {
            console.debug(`Character ${name}'s world ${worldName} is already activated in global world info! Skipping...`);
            continue;
        }

        const data = await loadWorldInfoData(worldName);
        const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
        entries = entries.concat(newEntries);
    }

    console.debug(`Character ${characters[this_chid]?.name} lore (${baseWorldName}) has ${entries.length} world info entries`);
    return entries;
}

async function getGlobalLore() {
    if (!selected_world_info) {
        return [];
    }

    let entries = [];
    for (const worldName of selected_world_info) {
        const data = await loadWorldInfoData(worldName);
        const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
        entries = entries.concat(newEntries);
    }

    console.debug(`Global world info has ${entries.length} entries`);

    return entries;
}

async function getSortedEntries() {
    try {
        const globalLore = await getGlobalLore();
        const characterLore = await getCharacterLore();

        let entries;

        switch (Number(world_info_character_strategy)) {
            case world_info_insertion_strategy.evenly:
                console.debug('WI using evenly')
                entries = [...globalLore, ...characterLore].sort(sortFn);
                break;
            case world_info_insertion_strategy.character_first:
                console.debug('WI using char first')
                entries = [...characterLore.sort(sortFn), ...globalLore.sort(sortFn)];
                break;
            case world_info_insertion_strategy.global_first:
                console.debug('WI using global first')
                entries = [...globalLore.sort(sortFn), ...characterLore.sort(sortFn)];
                break;
            default:
                console.error("Unknown WI insertion strategy: ", world_info_character_strategy, "defaulting to evenly");
                entries = [...globalLore, ...characterLore].sort(sortFn);
                break;
        }

        console.debug(`Sorted ${entries.length} world lore entries using strategy ${world_info_character_strategy}`);

        // Need to deep clone the entries to avoid modifying the cached data
        return deepClone(entries);
    }
    catch (e) {
        console.error(e);
        return [];
    }
}

async function checkWorldInfo(chat, maxContext) {
    const context = getContext();
    const messagesToLookBack = world_info_depth * 2 || 1;
    let textToScan = transformString(chat.slice(0, messagesToLookBack).join(""));
    let needsToScan = true;
    let count = 0;
    let allActivatedEntries = new Set();
    let failedProbabilityChecks = new Set();
    let allActivatedText = '';

    const budget = Math.round(world_info_budget * maxContext / 100) || 1;
    console.debug(`Context size: ${maxContext}; WI budget: ${budget} (${world_info_budget}%)`);
    const sortedEntries = await getSortedEntries();

    if (sortedEntries.length === 0) {
        return { worldInfoBefore: '', worldInfoAfter: '' };
    }

    while (needsToScan) {
        // Track how many times the loop has run
        count++;

        let activatedNow = new Set();

        for (let entry of sortedEntries) {
            if (failedProbabilityChecks.has(entry)) {
                continue;
            }

            if (allActivatedEntries.has(entry) || entry.disable == true || (count > 1 && world_info_recursive && entry.excludeRecursion)) {
                continue;
            }

            if (entry.constant) {
                entry.content = substituteParams(entry.content)
                activatedNow.add(entry);
                continue;
            }

            if (Array.isArray(entry.key) && entry.key.length) { //check for keywords existing
                primary: for (let key of entry.key) {
                    const substituted = substituteParams(key);
                    console.debug(`${entry.uid}: ${substituted}`)
                    if (substituted && matchKeys(textToScan, substituted.trim())) {
                        console.debug(`${entry.uid}: got primary match`)
                        //selective logic begins
                        if (
                            entry.selective && //all entries are selective now
                            Array.isArray(entry.keysecondary) && //always true
                            entry.keysecondary.length //ignore empties
                        ) {
                            console.debug(`uid:${entry.uid}: checking logic: ${entry.selectiveLogic}`)
                            secondary: for (let keysecondary of entry.keysecondary) {
                                const secondarySubstituted = substituteParams(keysecondary);
                                console.debug(`uid:${entry.uid}: filtering ${secondarySubstituted}`);

                                // If selectiveLogic isn't found, assume it's AND
                                const selectiveLogic = entry.selectiveLogic ?? 0;

                                //AND operator
                                if (selectiveLogic === 0) {
                                    console.debug('saw AND logic, checking..')
                                    if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
                                        console.log(`activating entry ${entry.uid} with AND found`)
                                        activatedNow.add(entry);
                                        break secondary;
                                    }
                                }
                                //NOT operator
                                if (selectiveLogic === 1) {
                                    console.debug(`uid ${entry.uid}: checking NOT logic for ${secondarySubstituted}`)
                                    if (secondarySubstituted && matchKeys(textToScan, secondarySubstituted.trim())) {
                                        console.debug(`uid ${entry.uid}: canceled; filtered out by ${secondarySubstituted}`)
                                        break primary;
                                    } else {
                                        console.debug(`${entry.uid}: activated; passed NOT filter`)
                                        activatedNow.add(entry);
                                        break secondary;
                                    }
                                }
                            }
                            //handle cases where secondary is empty
                        } else {
                            console.debug(`uid ${entry.uid}: activated without filter logic`)
                            activatedNow.add(entry);
                            break primary;
                        }
                    } else { console.debug('no active entries for logic checks yet') }
                }
            }
        }

        needsToScan = world_info_recursive && activatedNow.size > 0;
        const newEntries = [...activatedNow]
            .sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));
        let newContent = "";
        const textToScanTokens = getTokenCount(allActivatedText);
        const probabilityChecksBefore = failedProbabilityChecks.size;
        console.debug(`-- PROBABILITY CHECKS BEGIN --`)
        for (const entry of newEntries) {
            const rollValue = Math.random() * 100;


            if (entry.useProbability && rollValue > entry.probability) {
                console.debug(`WI entry ${entry.uid} ${entry.key} failed probability check, skipping`);
                failedProbabilityChecks.add(entry);
                continue;
            } else { console.debug(`uid:${entry.uid} passed probability check, inserting to prompt`) }

            newContent += `${substituteParams(entry.content)}\n`;

            if (textToScanTokens + getTokenCount(newContent) >= budget) {
                console.debug(`WI budget reached, stopping`);
                needsToScan = false;
                break;
            }

            allActivatedEntries.add(entry);
            console.debug('WI entry activated:', entry);
        }

        const probabilityChecksAfter = failedProbabilityChecks.size;

        if ((probabilityChecksAfter - probabilityChecksBefore) === activatedNow.size) {
            console.debug(`WI probability checks failed for all activated entries, stopping`);
            needsToScan = false;
        }

        if (needsToScan) {
            const text = newEntries
                .filter(x => !failedProbabilityChecks.has(x))
                .map(x => x.content).join('\n');
            const currentlyActivatedText = transformString(text);
            textToScan = (currentlyActivatedText + '\n' + textToScan);
            allActivatedText = (currentlyActivatedText + '\n' + allActivatedText);
        }
    }

    // Forward-sorted list of entries for joining
    const WIBeforeEntries = [];
    const WIAfterEntries = [];
    const ANTopEntries = [];
    const ANBottomEntries = [];

    // Appends from insertion order 999 to 1. Use unshift for this purpose
    [...allActivatedEntries].sort(sortFn).forEach((entry) => {
        switch (entry.position) {
            case world_info_position.before:
                WIBeforeEntries.unshift(substituteParams(entry.content));
                break;
            case world_info_position.after:
                WIAfterEntries.unshift(substituteParams(entry.content));
                break;
            case world_info_position.ANTop:
                ANTopEntries.unshift(entry.content);
                break;
            case world_info_position.ANBottom:
                ANBottomEntries.unshift(entry.content);
                break;
            default:
                break;
        }
    });

    const worldInfoBefore = WIBeforeEntries.length ? `${WIBeforeEntries.join("\n")}\n` : '';
    const worldInfoAfter = WIAfterEntries.length ? `${WIAfterEntries.join("\n")}\n` : '';

    if (shouldWIAddPrompt) {
        const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value;
        const ANWithWI = `${ANTopEntries.join("\n")}\n${originalAN}\n${ANBottomEntries.join("\n")}`
        context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
    }

    return { worldInfoBefore, worldInfoAfter };
}

function matchKeys(haystack, needle) {
    const transformedString = transformString(needle);

    if (world_info_match_whole_words) {
        const keyWords = transformedString.split(/\s+/);

        if (keyWords.length > 1) {
            return haystack.includes(transformedString);
        }
        else {
            const regex = new RegExp(`\\b${transformedString}\\b`);
            if (regex.test(haystack)) {
                return true;
            }
        }

    } else {
        return haystack.includes(transformedString);
    }

    return false;
}

function convertAgnaiMemoryBook(inputObj) {
    const outputObj = { entries: {} };

    inputObj.entries.forEach((entry, index) => {
        outputObj.entries[index] = {
            uid: index,
            key: entry.keywords,
            keysecondary: [],
            comment: entry.name,
            content: entry.entry,
            constant: false,
            selective: false,
            order: entry.weight,
            position: 0,
            disable: !entry.enabled,
            addMemo: !!entry.name,
            excludeRecursion: false,
            displayIndex: index,
            probability: null,
            useProbability: false,
        };
    });

    return outputObj;
}

function convertRisuLorebook(inputObj) {
    const outputObj = { entries: {} };

    inputObj.data.forEach((entry, index) => {
        outputObj.entries[index] = {
            uid: index,
            key: entry.key.split(',').map(x => x.trim()),
            keysecondary: entry.secondkey ? entry.secondkey.split(',').map(x => x.trim()) : [],
            comment: entry.comment,
            content: entry.content,
            constant: entry.alwaysActive,
            selective: entry.selective,
            order: entry.insertorder,
            position: world_info_position.before,
            disable: false,
            addMemo: true,
            excludeRecursion: false,
            displayIndex: index,
            probability: entry.activationPercent ?? null,
            useProbability: entry.activationPercent ?? false,
        };
    });

    return outputObj;
}

function convertNovelLorebook(inputObj) {
    const outputObj = {
        entries: {}
    };

    inputObj.entries.forEach((entry, index) => {
        const displayName = entry.displayName;
        const addMemo = displayName !== undefined && displayName.trim() !== '';

        outputObj.entries[index] = {
            uid: index,
            key: entry.keys,
            keysecondary: [],
            comment: displayName || '',
            content: entry.text,
            constant: false,
            selective: false,
            order: entry.contextConfig?.budgetPriority ?? 0,
            position: 0,
            disable: !entry.enabled,
            addMemo: addMemo,
            excludeRecursion: false,
            displayIndex: index,
            probability: null,
            useProbability: false,
        };
    });

    return outputObj;
}

function convertCharacterBook(characterBook) {
    const result = { entries: {}, originalData: characterBook };

    characterBook.entries.forEach((entry, index) => {
        // Not in the spec, but this is needed to find the entry in the original data
        if (entry.id === undefined) {
            entry.id = index;
        }

        result.entries[entry.id] = {
            uid: entry.id,
            key: entry.keys,
            keysecondary: entry.secondary_keys || [],
            comment: entry.comment || "",
            content: entry.content,
            constant: entry.constant || false,
            selective: entry.selective || false,
            order: entry.insertion_order,
            position: entry.extensions?.position ?? (entry.position === "before_char" ? world_info_position.before : world_info_position.after),
            excludeRecursion: entry.extensions?.exclude_recursion ?? false,
            disable: !entry.enabled,
            addMemo: entry.comment ? true : false,
            displayIndex: entry.extensions?.display_index ?? index,
            probability: entry.extensions?.probability ?? null,
            useProbability: entry.extensions?.useProbability ?? false,
        };
    });

    return result;
}

export function setWorldInfoButtonClass(chid, forceValue = undefined) {
    if (forceValue !== undefined) {
        $('#set_character_world, #world_button').toggleClass('world_set', forceValue);
        return;
    }

    if (!chid) {
        return;
    }

    const world = characters[chid]?.data?.extensions?.world;
    const worldSet = Boolean(world && world_names.includes(world));
    $('#set_character_world, #world_button').toggleClass('world_set', worldSet);
}

export function checkEmbeddedWorld(chid) {
    $('#import_character_info').hide();

    if (chid === undefined) {
        return false;
    }

    if (characters[chid]?.data?.character_book) {
        $('#import_character_info').data('chid', chid).show();

        // Only show the alert once per character
        const checkKey = `AlertWI_${characters[chid].avatar}`;
        const worldName = characters[chid]?.data?.extensions?.world;
        if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
            toastr.info(
                'To import and use it, select "Import Embedded World Info" in the Options dropdown menu on the character panel.',
                `${characters[chid].name} has an embedded World/Lorebook`,
                { timeOut: 10000, extendedTimeOut: 20000, positionClass: 'toast-top-center' },
            );
            localStorage.setItem(checkKey, 1);
        }
        return true;
    }

    return false;
}

export async function importEmbeddedWorldInfo() {
    const chid = $('#import_character_info').data('chid');

    if (chid === undefined) {
        return;
    }

    const bookName = characters[chid]?.data?.character_book?.name || `${characters[chid]?.name}'s Lorebook`;
    const confirmationText = (`<h3>Are you sure you want to import "${bookName}"?</h3>`) + (world_names.includes(bookName) ? 'It will overwrite the World/Lorebook with the same name.' : '');

    const confirmation = await callPopup(confirmationText, 'confirm');

    if (!confirmation) {
        return;
    }

    const convertedBook = convertCharacterBook(characters[chid].data.character_book);

    await saveWorldInfo(bookName, convertedBook, true);
    await updateWorldInfoList();
    $('#character_world').val(bookName).trigger('change');

    toastr.success(`The world "${bookName}" has been imported and linked to the character successfully.`, 'World/Lorebook imported');

    const newIndex = world_names.indexOf(bookName);
    if (newIndex >= 0) {
        $("#world_editor_select").val(newIndex).trigger('change');
    }

    setWorldInfoButtonClass(chid, true);
}

function onWorldInfoChange(_, text) {
    let selectedWorlds;
    if (_ !== '__notSlashCommand__') { // if it's a slash command
        if (text !== undefined) { // and args are provided
            const slashInputSplitText = text.trim().toLowerCase().split(",");

            slashInputSplitText.forEach((worldName) => {
                const wiElement = getWIElement(worldName);
                if (wiElement.length > 0) {
                    wiElement.prop("selected", true);
                    toastr.success(`Activated world: ${wiElement.text()}`);
                } else {
                    toastr.error(`No world found named: ${worldName}`);
                }
            })
        } else { // if no args, unset all worlds
            toastr.success('Deactivated all worlds');
            selected_world_info = [];
            $("#world_info").val("");
        }
    } else { //if it's a pointer selection
        let tempWorldInfo = [];
        let selectedWorlds = $("#world_info").val().map((e) => Number(e)).filter((e) => !isNaN(e));
        if (selectedWorlds.length > 0) {
            selectedWorlds.forEach((worldIndex) => {
                const existingWorldName = world_names[worldIndex];
                if (existingWorldName) {
                    tempWorldInfo.push(existingWorldName);
                } else {
                    const wiElement = getWIElement(existingWorldName);
                    wiElement.prop("selected", false);
                    toastr.error(`The world with ${existingWorldName} is invalid or corrupted.`);
                }
            });
        }
        selected_world_info = tempWorldInfo;
    }

    saveSettingsDebounced();
}

export async function importWorldInfo(file) {
    if (!file) {
        return;
    }

    const formData = new FormData();
    formData.append('avatar', file);

    try {
        let jsonData;

        if (file.name.endsWith('.png')) {
            const buffer = new Uint8Array(await getFileBuffer(file));
            jsonData = extractDataFromPng(buffer, 'naidata');
        } else {
            // File should be a JSON file
            jsonData = await parseJsonFile(file);
        }

        if (jsonData === undefined || jsonData === null) {
            toastr.error(`File is not valid: ${file.name}`);
            return;
        }

        // Convert Novel Lorebook
        if (jsonData.lorebookVersion !== undefined) {
            console.log('Converting Novel Lorebook');
            formData.append('convertedData', JSON.stringify(convertNovelLorebook(jsonData)));
        }

        // Convert Agnai Memory Book
        if (jsonData.kind === 'memory') {
            console.log('Converting Agnai Memory Book');
            formData.append('convertedData', JSON.stringify(convertAgnaiMemoryBook(jsonData)));
        }

        // Convert Risu Lorebook
        if (jsonData.type === 'risu') {
            console.log('Converting Risu Lorebook');
            formData.append('convertedData', JSON.stringify(convertRisuLorebook(jsonData)));
        }
    } catch (error) {
        toastr.error(`Error parsing file: ${error}`);
        return;
    }

    jQuery.ajax({
        type: "POST",
        url: "/importworldinfo",
        data: formData,
        beforeSend: () => { },
        cache: false,
        contentType: false,
        processData: false,
        success: async function (data) {
            if (data.name) {
                await updateWorldInfoList();

                const newIndex = world_names.indexOf(data.name);
                if (newIndex >= 0) {
                    $("#world_editor_select").val(newIndex).trigger('change');
                }

                toastr.info(`World Info "${data.name}" imported successfully!`);
            }
        },
        error: (jqXHR, exception) => { },
    });
}

jQuery(() => {

    $(document).ready(function () {
        registerSlashCommand('world', onWorldInfoChange, [], "– sets active World, or unsets if no args provided", true, true);
    })


    $("#world_info").on('mousedown change', async function (e) {
        // If there's no world names, don't do anything
        if (world_names.length === 0) {
            e.preventDefault();
            return;
        }

        /*
        if (deviceInfo.device.type === 'desktop') {
            let selectScrollTop = null;
            e.preventDefault();
            const option = $(e.target);
            const selectElement = $(this)[0];
            selectScrollTop = selectElement.scrollTop;
            option.prop('selected', !option.prop('selected'));
            await delay(1);
            selectElement.scrollTop = selectScrollTop;
        }
        */

        onWorldInfoChange('__notSlashCommand__');
    });

    //**************************WORLD INFO IMPORT EXPORT*************************//
    $("#world_import_button").on('click', function () {
        $("#world_import_file").trigger('click');
    });

    $("#world_import_file").on("change", async function (e) {
        const file = e.target.files[0];

        await importWorldInfo(file);

        // Will allow to select the same file twice in a row
        $("#form_world_import").trigger("reset");
    });

    $("#world_create_button").on('click', async () => {
        const tempName = getFreeWorldName();
        const finalName = await callPopup("<h3>Create a new World Info?</h3>Enter a name for the new file:", "input", tempName);

        if (finalName) {
            await createNewWorldInfo(finalName);
        }
    });

    $("#world_editor_select").on('change', async () => {
        const selectedIndex = $("#world_editor_select").find(":selected").val();

        if (selectedIndex === "") {
            hideWorldEditor();
        } else {
            const worldName = world_names[selectedIndex];
            showWorldEditor(worldName);
        }
    });

    $(document).on("input", "#world_info_depth", function () {
        world_info_depth = Number($(this).val());
        $("#world_info_depth_counter").text($(this).val());
        saveSettingsDebounced();
    });

    $(document).on("input", "#world_info_budget", function () {
        world_info_budget = Number($(this).val());
        $("#world_info_budget_counter").text($(this).val());
        saveSettingsDebounced();
    });

    $(document).on("input", "#world_info_recursive", function () {
        world_info_recursive = !!$(this).prop('checked');
        saveSettingsDebounced();
    })

    $('#world_info_case_sensitive').on('input', function () {
        world_info_case_sensitive = !!$(this).prop('checked');
        saveSettingsDebounced();
    })

    $('#world_info_match_whole_words').on('input', function () {
        world_info_match_whole_words = !!$(this).prop('checked');
        saveSettingsDebounced();
    });

    $('#world_info_character_strategy').on('change', function () {
        world_info_character_strategy = $(this).val();
        saveSettingsDebounced();
    });

    $('#world_button').on('click', async function () {
        const chid = $('#set_character_world').data('chid');

        if (chid) {
            const worldName = characters[chid]?.data?.extensions?.world;
            const hasEmbed = checkEmbeddedWorld(chid);
            if (worldName && world_names.includes(worldName)) {
                if (!$('#WorldInfo').is(':visible')) {
                    $('#WIDrawerIcon').trigger('click');
                }
                const index = world_names.indexOf(worldName);
                $("#world_editor_select").val(index).trigger('change');
            } else if (hasEmbed) {
                await importEmbeddedWorldInfo();
                saveCharacterDebounced();
            }
            else {
                $('#char-management-dropdown').val($('#set_character_world').val()).trigger('change');
            }
        }
    });

    /*
    $("#world_info").on('mousewheel', function (e) {
        e.preventDefault();
        if ($(this).is(':animated')) {
            return; //dont force multiple scroll animations
        }
        var wheelDelta = e.originalEvent.wheelDelta.toFixed(0);
        var DeltaPosNeg = (wheelDelta >= 0) ? 1 : -1; //determine if scrolling up or down
        var containerHeight = $(this).height().toFixed(0);
        var optionHeight = $(this).find('option').first().height().toFixed(0);
        var visibleOptions = (containerHeight / optionHeight).toFixed(0); //how many options we can see
        var pixelsToScroll = (optionHeight * visibleOptions * DeltaPosNeg).toFixed(0); //scroll a full container height
        var scrollTop = ($(this).scrollTop() - pixelsToScroll).toFixed(0);

        $(this).animate({ scrollTop: scrollTop }, 200);
    });
    */

    // Not needed on mobile
    if (deviceInfo.device.type === 'desktop') {
        $('#world_info').select2({
            width: '100%',
            placeholder: 'No Worlds active. Click here to select.',
            allowClear: true,
            closeOnSelect: false,
        });
    }
})
