declare const FRIENDS_URL: string;
declare const BASE_URL: string;
declare const LOCALE: string;

/**
 * Object that we get back from the friend API.
 */
interface JsonFriend {
    name: string,
    id: number,
}

interface YearSummary {
    [index: string]: {[index: number]: number},
}

interface Window {
    fietsboekImageIndex: number,
    fietsboekCurrentImage: HTMLDivElement | null,
}

// Make eslint happy about the Window redefinition
(_: Window) => null;

/**
 * Type alias to make it clear what kind of string we are expecting.
 */
type Selector = string;


/**
 * Gets the value of a single cookie.
 *
 * @param name - Name of the cookie.
 * @return The cookie value, or null.
 */
function getCookie(name: string): string | undefined {
    return document.cookie.split("; ")
        .find((row) => row.startsWith(`${name}=`))
        ?.split("=")[1];
}


/**
 * Builds a URL with the correct application URL as base.
 *
 * Do not add the leading slash, otherwise the resolution will be wrong!
 *
 * @param path - Path to append to the base.
 * @return The correct URL in regards to the application URL.
 */
function makeUrl(name: string): URL {
    return new URL(name, BASE_URL);
}

/**
 * Installs a listener to the given DOM objects.
 *
 * @param selector - The query selector to find the DOM objects.
 * @param event - The event name to listen to.
 * @param handler - The handler function.
 */
function addHandler<K extends keyof GlobalEventHandlersEventMap>(
    selector: Selector,
    event: K,
    handler: (ev: GlobalEventHandlersEventMap[K]) => any,
) {
    document.querySelectorAll(selector).
        forEach((obj) => obj.addEventListener(event, handler as EventListener));
}

/**
 * Handler for when a language is clicked. Sets the cookie to the correct locale.
 *
 * @param event - The triggering event.
 */
function languageClicked(event: MouseEvent) {
    const path = new URL(BASE_URL).pathname;
    const language = (event.target as HTMLElement).getAttribute("data-langcode");
    document.cookie = `fietsboek_locale=${language}; Path=${path}`;
    window.location.reload();
    event.preventDefault();
}

addHandler(".language-choice", "click", languageClicked);

/**
 * Handler for when a tag is clicked. Removes the tag from the tag list.
 *
 * @param event - The triggering event.
 */
function tagClicked(event: MouseEvent) {
    const span = (event.target as HTMLElement).closest('span')!;
    span.parentNode!.removeChild(span);
}

addHandler(".tag-badge", "click", tagClicked);

/**
 * Handler to add a new tag when the button is pressed.
 */
function addTag() {
    const newTag = document.querySelector("#new-tag") as HTMLInputElement;
    if (newTag.value === "") {
        return;
    }
    const node = document.createElement("span");
    node.classList.add("tag-badge");
    node.classList.add("badge");
    node.classList.add("rounded-pill");
    node.classList.add("bg-info");
    node.classList.add("text-dark");
    node.addEventListener("click", tagClicked);
    const text = document.createTextNode(newTag.value);
    node.appendChild(text);
    const icon = document.createElement("i");
    icon.classList.add("bi");
    icon.classList.add("bi-x");
    node.appendChild(icon);
    const input = document.createElement("input");
    input.hidden = true;
    input.name = "tag[]";
    input.value = newTag.value;
    node.appendChild(input);
    document.querySelector("#formTags")?.appendChild(node);
    const space = document.createTextNode(" ");
    document.querySelector("#formTags")?.appendChild(space);
    newTag.value = "";
}

addHandler("#add-tag-btn", "click", addTag);
// Also add a tag when enter is pressed
addHandler("#new-tag", "keypress", (event) => {
    if (event.code == "Enter") {
        event.preventDefault();
        addTag();
    }
});

/**
 * Function to check for password validity.
 *
 * @param main - Selector for the actual entered password input.
 * @param repeat - Selector for the repeated password, must match `main`.
 */
function checkPasswordValidity(main: Selector, repeat: Selector) {
    const mainPassword = document.querySelector(main) as HTMLInputElement;
    const repeatPassword = document.querySelector(repeat) as HTMLInputElement;

    const form = mainPassword.closest('form')!;
    form.classList.remove('was-validated');

    // Check password requirements. The validation errors are not actually
    // displayed, as the HTML template contains pre-filled texts for that.
    if (mainPassword.value.length != 0 && mainPassword.value.length < 8) {
        mainPassword.setCustomValidity('Too short');
    } else {
        mainPassword.setCustomValidity('');
    }

    if (mainPassword.value != repeatPassword.value) {
        repeatPassword.setCustomValidity('Needs to match');
    } else {
        repeatPassword.setCustomValidity('');
    }
}

// This function is used via a HTML onchange= handler, so make eslint happy
checkPasswordValidity;

/**
 * Function to check for name validity.
 *
 * @param name - Selector name that should be checked.
 */
function checkNameValidity(name: Selector) {
    const nameField = document.querySelector(name) as HTMLInputElement;
    if (nameField.value.length == 0) {
        nameField.setCustomValidity('Needs a name');
    }
}

// This function is used via a HTML onchange= handler, so make eslint happy
checkNameValidity;

/**
 * Check whether the given friend is already tagged.
 *
 * Note that this uses the "HTML list", so it uses the tags that the user is
 * currently editing - not the ones from the database!
 *
 * @param friendId - ID of the friend to check.
 * @return Whether the friend is in the list of tagged people.
 */
function friendIsTagged(friendId: number): boolean {
    return Array.from(document.querySelectorAll("[name='tagged-friend[]']"))
        .map((obj) => obj as HTMLInputElement)
        .filter((obj) => !obj.disabled)
        .map((obj) => obj.value)
        .includes(friendId.toString());
}

/**
 * Hit the endpoint to search for friends. This populates the friend selector
 * when tagging friends.
 */
function searchFriends() {
    const searchPattern = (document.querySelector("#friendSearchQuery") as HTMLInputElement).
        value.toLowerCase();
    const friendSearch = document.querySelector("#friendSearch")!;
    friendSearch.innerHTML = "";
    fetch(FRIENDS_URL)
        .then((response) => response.json())
        .then((response: [JsonFriend]) => {
            const blueprint =
                (document.querySelector("#friendSearchBlueprint") as HTMLTemplateElement).content;

            // Only show friends with a matching name
            let friends = response.filter(
                (obj) => obj.name.toLowerCase().indexOf(searchPattern) != -1
            );

            // Only show friends that are not yet existing
            friends = friends.filter(
                (obj) => !friendIsTagged(obj.id)
            );

            friends.forEach((friend) => {
                const copy = blueprint.cloneNode(true) as DocumentFragment;
                (copy.querySelector(".friend-name") as HTMLSpanElement).textContent = friend.name;
                copy.querySelector("button")?.addEventListener("click", (event: MouseEvent) => {
                    const button = (event.target as HTMLElement).closest("button")!;
                    button.parentNode!.parentNode!.removeChild(button.parentNode!);

                    const added =
                        (document.querySelector("#friendAddedBlueprint")! as HTMLTemplateElement)
                        .content
                        .cloneNode(true) as DocumentFragment;
                    (added.querySelector(".friend-name") as HTMLSpanElement).
                        textContent = friend.name;
                    added.querySelector("input")!.value = friend.id.toString();
                    added.querySelector("input")!.removeAttribute("disabled");
                    added.querySelector("button")!.addEventListener("click", removeFriendClicked);
                    document.querySelector('#taggedFriends')!.appendChild(added);
                });
                friendSearch.appendChild(copy);
            });
        });
}

addHandler("#add-friend-btn", "click", () => searchFriends());
// Also trigger the search on Enter keypress
addHandler("#friendSearchQuery", "keypress", (event) => {
    if (event.code == "Enter") {
        event.preventDefault();
        searchFriends();
    }
});

/**
 * Handler for when a "Remove friend" button is clicked.
 *
 * @param event - The triggering event.
 */
function removeFriendClicked(event: MouseEvent) {
    const button = (event.target as HTMLElement).closest("button")!;
    button.parentNode!.parentNode!.removeChild(button.parentNode!);
}

addHandler(".remove-friend-button", "click", removeFriendClicked);

/**
 * Handler for when the image input is changed.
 *
 * This handler splits the multiple images up into single input fields, such
 * that each one can be removed individually. It also adds preview images, and
 * adds the button to delete and edit the image's description.
 *
 * @param event - The triggering event.
 */
function imageSelectorChanged(event: Event) {
    const target = event.target as HTMLInputElement;
    for (const file of Array.from(target.files ?? [])) {
        window.fietsboekImageIndex++;

        const input = document.createElement("input");
        input.type = "file";
        input.hidden = true;
        input.name = `image[${window.fietsboekImageIndex}]`;

        const transfer = new DataTransfer();
        transfer.items.add(file);
        input.files = transfer.files;

        const preview =
            (document.querySelector("#trackImagePreviewBlueprint") as HTMLTemplateElement).
            content.
            cloneNode(true) as DocumentFragment;
        preview.querySelector("img")!.src = URL.createObjectURL(file);
        preview.querySelector("button.delete-image")!.
            addEventListener("click", deleteImageButtonClicked as EventListener);
        preview.querySelector("button.edit-image-description")!.
            addEventListener("click", editImageDescriptionClicked as EventListener);
        (preview.querySelector("input.image-description-input") as HTMLInputElement).
            name = `image-description[${window.fietsboekImageIndex}]`;
        preview.querySelector("div")!.appendChild(input);

        document.querySelector("#trackImageList")!.appendChild(preview);
    }

    target.value = "";
}

addHandler("#imageSelector", "change", imageSelectorChanged);

/**
 * Handler to remove a picture from a track.
 *
 * @param event - The triggering event.
 */
function deleteImageButtonClicked(event: MouseEvent) {
    const preview = (event.target as HTMLElement).closest("div.track-image-preview")!;
    /* If this was a image yet to be uploaded, simply remove it */
    const input = preview.querySelector("input[type=file]");
    if (input) {
        preview.parentNode!.removeChild(preview);
        return;
    }

    /* Otherwise, we need to remove it but also insert a "delete-image" input */
    const deleter = preview.querySelector("input.image-deleter-input")!;
    deleter.removeAttribute("disabled");
    preview.removeChild(deleter);
    preview.parentNode!.appendChild(deleter);
    preview.parentNode!.removeChild(preview);
}

addHandler("button.delete-image", "click", deleteImageButtonClicked);

/**
 * Handler to show the image description editor.
 *
 * @param event - The triggering event.
 */
function editImageDescriptionClicked(event: MouseEvent) {
    window.fietsboekCurrentImage = (event.target as HTMLElement).closest("div")!;

    const imageInput = (
        <HTMLInputElement>
        window.fietsboekCurrentImage.querySelector("input.image-description-input")
    );
    const currentDescription = imageInput.value;
    const modalDom = document.getElementById("imageDescriptionModal")!;
    modalDom.querySelector("textarea")!.value = currentDescription;

    const modal = bootstrap.Modal.getOrCreateInstance(modalDom, {});
    modal.show();
}

addHandler("button.edit-image-description", "click", editImageDescriptionClicked);

/**
 * Handler to save the image description of the currently edited image.
 *
 * @param event - The triggering event.
 */
function saveImageDescriptionClicked(_event: MouseEvent) {
    const modalDom = document.getElementById("imageDescriptionModal")!;
    const wantedDescription = modalDom.querySelector("textarea")!.value;
    (window.fietsboekCurrentImage!.
        querySelector("input.image-description-input") as HTMLInputElement).
        value = wantedDescription;
    window.fietsboekCurrentImage!.
        querySelector("img")!.title = wantedDescription;

    const modal = bootstrap.Modal.getOrCreateInstance(modalDom, {});
    modal.hide();

    window.fietsboekCurrentImage = null;
}

addHandler("#imageDescriptionModal button.btn-success", "click", saveImageDescriptionClicked);

/**
 * Handler to toggle (collapse/expand) the yearly/monthly summary.
 *
 * @param event - The triggering event.
 */
function toggleSummary(event: MouseEvent) {
    const chevron = event.target as HTMLElement;
    const containing = chevron.closest("a")!;
    const summary = containing.nextElementSibling!;
    bootstrap.Collapse.getOrCreateInstance(summary).toggle();
    if (chevron.classList.contains("bi-chevron-down")) {
        chevron.classList.remove("bi-chevron-down");
        chevron.classList.add("bi-chevron-right");
    } else {
        chevron.classList.remove("bi-chevron-right");
        chevron.classList.add("bi-chevron-down");
    }
}

addHandler(".summary-toggler", "click", toggleSummary);

/*
 * Handler to enable the "Download archive button" ...
 */
addHandler("#archiveDownloadButton", "click", () => {
    const checked = document.querySelectorAll(".archive-checkbox:checked");
    const url = makeUrl("track/archive");
    checked.forEach((c) => {
        url.searchParams.append("track_id[]", (c as HTMLInputElement).value);
    });
    window.location.assign(url);
});
/*
 * ... and the listeners on the checkboxes to disable and enable the button.
 */
addHandler(".archive-checkbox", "change", () => {
    const checked = document.querySelectorAll(".archive-checkbox:checked");
    const downloadButton = document.querySelector("#archiveDownloadButton") as HTMLButtonElement;
    downloadButton.disabled = (checked.length == 0);
});

/**
 * Handler to clear the input when a .button-clear-input is pressed.
 *
 * The button must be in an input-group with the input.
 *
 * @param event - The triggering event.
 */
function clearInputButtonClicked(event: MouseEvent) {
    const target = event.target as HTMLElement;
    target.closest(".input-group")!.querySelectorAll("input").forEach((i) => i.value = "");
    target.closest(".input-group")!.querySelectorAll("select").forEach((i) => i.value = "");
}

addHandler(".button-clear-input", "click", clearInputButtonClicked);


/**
 * Handler to change the sorting of the home page.
 *
 * This basically sets the cookie to signal that the home page should be
 * returned reversed, and then reloads the page.
 *
 * @param event - The triggering event.
 */
function changeHomeSorting(_event: MouseEvent) {
    const currentSorting = getCookie("home_sorting") ?? "asc";
    const newSorting = currentSorting == "asc" ? "desc" : "asc";
    document.cookie = `home_sorting=${newSorting}; SameSite=Lax`;
    window.location.reload();
}

addHandler("#changeHomeSorting", "click", changeHomeSorting);


/**
 * Handler to toggle the favourite status of a track.
 *
 * This is applied to .favourite-star elements and expects the track ID in
 * data-track-id.
 *
 * @param event - The triggering event.
 */
function toggleTrackFavourite(event: MouseEvent) {
    const target = event.target as HTMLElement;
    const trackId = target.getAttribute("data-track-id");
    if (trackId === null) {
        return;
    }
    const url = makeUrl("me/toggle-favourite");
    const formData = new URLSearchParams();
    formData.append("track-id", trackId);
    formData.append("csrf_token", getCookie("csrf_token") ?? "");
    fetch(url, {
        "method": "POST",
        "body": formData,
    }).then(response => response.json().then(data => {
        const isNowFavourite = data["favourite"];
        if (isNowFavourite) {
            target.classList.replace("bi-star", "bi-star-fill");
        } else {
            target.classList.replace("bi-star-fill", "bi-star");
        }
    }));
}

addHandler(".favourite-star", "click", toggleTrackFavourite);

/**
 * Returns an array of localized month names (Jan-Dec).
 */
function getLocalizedMonthNames(): string[] {
    const monthNames: string[] = [];
    const date = new Date(2000, 0);
    for (let i = 0; i < 12; i++) {
        monthNames.push(date.toLocaleString(LOCALE, {month: 'long'}));
        date.setMonth(i + 1);
    }
    return monthNames;
}

/**
 * Load and plot the user's summary.
 */
function loadProfileStats() {
    const monthNames = getLocalizedMonthNames();

    const url = makeUrl("me/summary.json");
    fetch(url)
        .then(response => response.json())
        .then((response: YearSummary) => {
            const datasets = [];
            for (const [year, months] of Object.entries(response)) {
                const data = [];
                for (let i = 1; i <= 12; ++i) {
                    data.push((i in months) ? (months[i] / 1000) : 0);
                }
                datasets.push({
                    data: data,
                    label: year,
                });
            }

            new Chart(
                "graph-month-summary",
                {
                    type: "bar",
                    data: {
                        datasets: datasets,
                        labels: monthNames,
                    },
                },
            );
        });
}

/* Used via in-page scripts, so make eslint happy */
loadProfileStats;

document.addEventListener('DOMContentLoaded', function() {
    window.fietsboekImageIndex = 0;

    /* Enable tooltips */
    const tooltipTriggerList = [].slice.call(
        document.querySelectorAll('[data-bs-toggle="tooltip"]')
    );
    tooltipTriggerList.map((tooltipTriggerEl) => {
        return new bootstrap.Tooltip(tooltipTriggerEl, { sanitize: false });
    });

    /* Enable Bootstrap form validation */
    const forms = document.querySelectorAll('.needs-validation');
    Array.from(forms).forEach((form) => {
        form.addEventListener('submit', (event) => {
            if (!(form as HTMLFormElement).checkValidity()) {
                event.preventDefault();
                event.stopPropagation();
            }

            form.classList.add('was-validated');
        }, false);
    });

    /* Format all datetimes to the local timezone */
    document.querySelectorAll(".fietsboek-local-datetime").forEach((obj) => {
        const timestamp = parseFloat(obj.attributes.getNamedItem("data-utc-timestamp")!.value);
        const date = new Date(timestamp * 1000);
        // TypeScript complains about this, but according to MDN it is fine, at
        // least in "somewhat modern" browsers
        const intl = new Intl.DateTimeFormat(LOCALE, {
            dateStyle: "medium",
            timeStyle: "medium",
        } as any);
        obj.innerHTML = intl.format(date);
    });
});