import { _get, _post, _delete, _patch, toClipboard, toggleLoader, toDateString, SetupCopyButton, addLoader, removeLoader, DateCountdown } from "../modules/common.js";
import { DiscordSearch, DiscordUser, newDiscordSearch } from "../modules/discord.js";
import { reloadProfileNames }  from "../modules/profiles.js";
import { HiddenInputField } from "./ui.js";

declare var window: GlobalWindow;

const INF = "∞";

export const generateCodeLink = (code: string): string => {
    // let codeLink = window.pages.Base + window.pages.Form + "/" + code;
    let codeLink = window.pages.ExternalURI + window.pages.Form + "/" + code;
    return codeLink;
}

class DOMInvite implements Invite {
    updateNotify = (checkbox: HTMLInputElement) => {
        let state = {
            code: this.code,
            notify_expiry: this.notify_expiry,
            notify_creation: this.notify_creation
        };
        let revertChanges: () => void;
        if (checkbox.classList.contains("inv-notify-expiry")) {
            revertChanges = () => { this.notify_expiry = !this.notify_expiry };
        } else {
            revertChanges = () => { this.notify_creation = !this.notify_creation };
        }
        _patch("/invites/edit", state, (req: XMLHttpRequest) => {
            if (req.readyState == 4 && !(req.status == 200 || req.status == 204)) {
                revertChanges();
            }
        });
    }

    delete = () => _delete("/invites", { "code": this.code }, (req: XMLHttpRequest) => {
        if (req.readyState == 4 && (req.status == 200 || req.status == 204)) {
            this.remove();
            const inviteDeletedEvent = new CustomEvent("inviteDeletedEvent", { detail: this.code });
            document.dispatchEvent(inviteDeletedEvent);
        }
    })

    private _userLabel: string = "";
    get user_label(): string { return this._userLabel; }
    set user_label(label: string) {
        this._userLabel = label;
        const labelLabel = this._middle.querySelector(".user-label-label");
        const value = this._middle.querySelector(".user-label");
        if (label) {
            labelLabel.textContent = window.lang.strings("userLabel");
            value.textContent = label;
            value.classList.remove("unfocused");
        } else {
            labelLabel.textContent = "";
            value.textContent = "";
            value.classList.add("unfocused");
        }
    }

    private _label: string = "";
    get label(): string { return this._label; }
    set label(label: string) {
        this._label = label;
        if (label == "") {
            this.code = this.code;
        } else {
            this._labelEditor.value = label;
        }
    }

    private _code: string = "None";
    get code(): string { return this._code; }
    set code(code: string) {
        this._code = code;
        this._codeLink = generateCodeLink(code);
        if (this.label == "") {
            this._labelEditor.value = code.replace(/-/g, '-');
        }
        this._linkEl.href = this._codeLink;
    }
    private _codeLink: string;

    private _validTill: number;
    private _validTillUpdater: ReturnType<typeof setTimeout> = null;
    get valid_till(): number { return this._validTill; }
    set valid_till(v: number) {
        this._validTill = v;
        if (this._validTillUpdater) clearTimeout(this._validTillUpdater);
        this._validTillUpdater = DateCountdown(this._codeArea.querySelector("span.inv-duration"), v);
    }

    private _userExpiryEnabled: boolean;
    get user_expiry(): boolean { return this._userExpiryEnabled; }
    set user_expiry(v: boolean) { this._userExpiryEnabled = v; }
    private _userExpiry = { months: 0, days: 0, hours: 0, minutes: 0 };
    private _userExpiryString: string;
    get user_months(): number { return this._userExpiry.months; }
    get user_days(): number { return this._userExpiry.days; }
    get user_hours(): number { return this._userExpiry.hours; }
    get user_minutes(): number { return this._userExpiry.minutes; }
    set user_months(v: number) {
        this._userExpiry.months = v;
        this._updateUserExpiry();
    }
    set user_days(v: number) {
        this._userExpiry.days = v;
        this._updateUserExpiry();
    }
    set user_hours(v: number) {
        this._userExpiry.hours = v;
        this._updateUserExpiry();
    }
    set user_minutes(v: number) {
        this._userExpiry.minutes = v;
        this._updateUserExpiry();
    }
    set user_expiry_time(v: { months: number, days: number, hours: number, minutes: number }) {
        this._userExpiry = v;
        this._updateUserExpiry()
    }
    private _updateUserExpiry() {
        const expiry = this._middle.querySelector("span.user-expiry") as HTMLSpanElement;
        this._userExpiryString = "";
        if (!(this._userExpiry.months || this._userExpiry.days || this._userExpiry.hours || this._userExpiry.minutes)) {
            expiry.textContent = "";
            expiry.parentElement.classList.add("unfocused");
        } else {
            expiry.textContent = window.lang.strings("userExpiry");
            expiry.parentElement.classList.remove("unfocused");
            const fields = ["months", "days", "hours", "minutes"].map((v) => this._userExpiry[v]);
            const abbrevs = ["mo", "d", "h", "m"];
            for (let i = 0; i < fields.length; i++) {
                if (fields[i]) {
                    this._userExpiryString += ""+fields[i] + abbrevs[i] + " ";
                }
            }
            this._userExpiryString = this._userExpiryString.slice(0, -1);
        }
        this._middle.querySelector("strong.user-expiry-time").textContent = this._userExpiryString;
    }
    
    private _noLimit: boolean = false;
    get no_limit(): boolean { return this._noLimit; }
    set no_limit(v: boolean) {
        this._noLimit = v;
        const remaining = this._middle.querySelector("strong.inv-remaining") as HTMLElement;
        if (!this.no_limit) remaining.textContent = ""+this._remainingUses;
        else remaining.textContent = INF;
    }

    private _remainingUses: number = 1;
    get remaining_uses(): number { return this._remainingUses; }
    set remaining_uses(v: number) {
        this._remainingUses = v;
        const remaining = this._middle.querySelector("strong.inv-remaining") as HTMLElement;
        if (!this.no_limit) remaining.textContent = ""+this._remainingUses;
        else remaining.textContent = INF;
    }

    private _send_to: string = "";
    get send_to(): string { return this._send_to };
    set send_to(address: string | null) {
        this._send_to = address;
        const container = this._infoArea.querySelector(".tooltip") as HTMLDivElement;
        const icon = container.querySelector("i");
        const chip = container.querySelector("span.inv-email-chip");
        const tooltip = container.querySelector("span.content") as HTMLSpanElement;
        if (!address) {
            icon.classList.remove("ri-mail-line");
            icon.classList.remove("ri-mail-close-line");
            chip.classList.remove("~neutral");
            chip.classList.remove("~critical");
            chip.classList.remove("button");
            chip.parentElement.classList.remove("h-full");
        } else {
            chip.classList.add("button");
            chip.parentElement.classList.add("h-full");
            if (address.includes(window.lang.strings("failed"))) {
                icon.classList.remove("ri-mail-line");
                icon.classList.add("ri-mail-close-line");
                chip.classList.remove("~neutral");
                chip.classList.add("~critical");
            } else {
                icon.classList.remove("ri-mail-close-line");
                icon.classList.add("ri-mail-line");
                chip.classList.remove("~critical");
                chip.classList.add("~neutral");
            }
        }
        // innerHTML as the newer sent_to re-uses this with HTML.
        tooltip.innerHTML = address;
    }
    private _sendToDialog: SendToDialog; 
    private _sent_to: SentToList;
    get sent_to(): SentToList { return this._sent_to; }
    set sent_to(v: SentToList) {
        this._sent_to = v;
        if (!v || !(v.success || v.failed)) return;
        let text = "";
        if (v.success && v.success.length > 0) {
            text += window.lang.strings("sentTo") + ": " + v.success.join(", ") + " <br>"
        }
        if (v.failed && v.failed.length > 0) {
            text += window.lang.strings("failed") + ": " + v.failed.map((el: SendFailure) => {
                let err: string;
                switch (el.reason) {
                    case "CheckLogs":
                        err = window.lang.notif("errorCheckLogs");
                        break;
                    case "NoUser":
                        err = window.lang.notif("errorNoUser");
                        break;
                    case "MultiUser":
                        err = window.lang.notif("errorMultiUser");
                        break;
                    case "InvalidAddress":
                        err = window.lang.notif("errorInvalidAddress");
                        break;
                    default:
                        err = el.reason;
                        break;
                }
                return el.address + " (" + err + ")";
            }).join(", ");
        }
        if (text.length != 0) this.send_to = text;
    }

    private _usedBy: { [name: string]: number };
    get used_by(): { [name: string]: number } { return this._usedBy; }
    set used_by(uB: { [name: string]: number } | null) {
        this._usedBy = uB;
        if (!uB || Object.keys(uB).length == 0) {
            this._right.classList.add("empty");
            this._userTable.innerHTML = `<p class="content">${window.lang.strings("inviteNoUsersCreated")}</p>`;
            return;
        }
        this._right.classList.remove("empty");
        let innerHTML = `
        <table class="table inv-table table-p-0">
            <thead>
                <tr>
                    <th>${window.lang.strings("name")}</th>
                    <th class="w-2"></th>
                    <th>${window.lang.strings("date")}</th>
                </tr>
            </thead>
            <tbody>
        `;
        for (let username in uB) {
            innerHTML += `
                <tr>
                    <td>${username}</td>
                    <td class="w-2"></td>
                    <td>${toDateString(new Date(uB[username] * 1000))}</td>
                </tr>
            `;
        }
        innerHTML += `
            </tbody>
        </table>
        `;
        this._userTable.innerHTML = innerHTML;
    }

    private _createdUnix: number;
    get created(): number { return this._createdUnix; }
    set created(unix: number) {
        this._createdUnix = unix;
        const el = this._middle.querySelector("strong.inv-created");
        if (unix == 0) {
            el.textContent = window.lang.strings("unknown");
        } else {
            el.textContent = toDateString(new Date(unix*1000));
        }
    }
    
    private _notifyExpiry: boolean = false;
    get notify_expiry(): boolean { return this._notifyExpiry }
    set notify_expiry(state: boolean) {
        this._notifyExpiry = state;
        (this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement).checked = state;
    }

    private _notifyCreation: boolean = false;
    get notify_creation(): boolean { return this._notifyCreation }
    set notify_creation(state: boolean) {
        this._notifyCreation = state;
        (this._left.querySelector("input.inv-notify-creation") as HTMLInputElement).checked = state;
    }

    private _profile: string;
    get profile(): string { return this._profile; }
    set profile(profile: string) { this.loadProfiles(profile); }
    loadProfiles = (selected?: string) => {
        const select = this._left.querySelector("select") as HTMLSelectElement;
        let noProfile = false;
        if (selected === "") {
            noProfile = true; 
        } else {
            selected = selected || select.value;
        }
        let innerHTML = `<option value="noProfile" ${noProfile ? "selected" : ""}>${window.lang.strings("inviteNoProfile")}</option>`;
        for (let profile of window.availableProfiles) {
            innerHTML += `<option value="${profile}" ${((profile == selected) && !noProfile) ? "selected" : ""}>${profile}</option>`;
        }
        select.innerHTML = innerHTML;
        this._profile = selected;
    };
    updateProfile = () => {
        const select = this._left.querySelector("select") as HTMLSelectElement;
        const previous = this.profile;
        let profile = select.value;
        let state = {code: this.code};
        if (profile != "noProfile") { state["profile"] = profile };
        _patch("/invites/edit", state, (req: XMLHttpRequest) => {
            if (req.readyState == 4) {
                if (!(req.status == 200 || req.status == 204)) {
                    select.value = previous || "noProfile";
                } else {
                    this._profile = profile;
                }
            }
        });
    }

    private _setLabel = () => {
        const newLabel = this._labelEditor.value.trim();
        const old = this.label;
        this.label = newLabel;
        let state = {
            code: this.code,
            label: newLabel
        };
        _patch("/invites/edit", state, (req: XMLHttpRequest) => {
            if (req.readyState != 4) return;
            if (req.status != 200 && req.status != 204) {
                this.label = old;
            }
        });
    }

    private _container: HTMLDivElement;

    private _header: HTMLDivElement;
    private _codeArea: HTMLDivElement;
    private _infoArea: HTMLDivElement;

    private _details: HTMLDivElement;
    private _left: HTMLDivElement;
    private _middle: HTMLDivElement;
    private _right: HTMLDivElement;
    private _userTable: HTMLDivElement;

    private _linkContainer: HTMLElement;
    private _linkEl: HTMLAnchorElement;
    private _labelEditor: HiddenInputField;

    private _detailsToggle: HTMLInputElement;

    private _gap: number;
    get gap(): number { return this._gap; }
    set gap(v: number) {
        // Do it this way to ensure the class is included by tailwind
        let gapClass: string;
        switch (v) {
            case 1:
                gapClass = "gap-1";
                break;
            case 2:
                gapClass = "gap-2";
                break;
            case 3:
                gapClass = "gap-3";
                break;
            case 4:
                gapClass = "gap-4";
                break;
            default:
                gapClass = "gap-"+v;
                break;
        }
        this._container.classList.remove("gap-"+this._gap);
        this._container.classList.add(gapClass);
        this._gap = v;
    }

    // whether the details card is expanded.
    get expanded(): boolean {
        return this._detailsToggle.checked;
    }
    set expanded(state: boolean) {
        this._detailsToggle.checked = state;
        if (state) {
            this._detailsToggle.previousElementSibling.classList.add("rotated");
            this._detailsToggle.previousElementSibling.classList.remove("not-rotated");
            
            const mainTransitionStart = () => {
                this._details.removeEventListener("transitionend", mainTransitionStart);
                this._details.style.transitionDuration = "";
                this._details.addEventListener("transitionend", mainTransitionEnd);
                this._details.style.opacity = "100%";
                this._details.style.maxHeight = "calc(" + (1*this._details.scrollHeight)+"px" + " + " + (0.125 * 8 * this.gap) + "rem)"; // Compensate for the margin and padding (ugly)
                this._details.style.marginTop = "0";
                this._details.style.marginBottom = "0";
                this._details.style.paddingTop = "";
                this._details.style.paddingBottom = "";
            };
            const mainTransitionEnd = () => {
                this._details.removeEventListener("transitionend", mainTransitionEnd);
                this._details.style.maxHeight = "9999px";
            };
            this._details.classList.remove("unfocused");
            this._details.classList.add("focused");
            this._details.style.transitionDuration = "1ms";
            // Add negative y margin to cancel out "gap-x" when we unhide (and are initially height: 0)
            // perhaps not great assuming --spacing == 0.25rem
            this._details.style.marginTop = (-0.125 * this.gap)+"rem";
            this._details.style.marginBottom = (-0.125 * this.gap)+"rem";
            this._details.style.paddingTop = "0";
            this._details.style.paddingBottom = "0";
            mainTransitionStart();
        } else {
            this._detailsToggle.previousElementSibling.classList.remove("rotated");
            this._detailsToggle.previousElementSibling.classList.add("not-rotated");
            const mainTransitionEnd = () => {
                this._details.removeEventListener("transitionend", mainTransitionEnd);
                this._details.style.paddingTop = "";
                this._details.style.paddingBottom = "";
                this._details.style.marginTop = "0";
                this._details.style.marginBottom = "0";
                this._details.classList.add("unfocused");
                this._details.classList.remove("focused");
            };
            const mainTransitionStart = () => {
                this._details.removeEventListener("transitionend", mainTransitionStart);
                this._details.style.transitionDuration = "";
                this._details.addEventListener("transitionend", mainTransitionEnd);
                this._details.style.paddingTop = "0";
                this._details.style.paddingBottom = "0";
                this._details.style.maxHeight = "0";
                this._details.style.opacity = "0";
                // Add negative y margin to cancel out "gap-x" when we finish hiding (and end up height:0)
                // perhaps not great assuming --spacing == 0.25rem
                this._details.style.marginTop = (-0.125 * this.gap)+"rem";
                this._details.style.marginBottom = (-0.125 * this.gap)+"rem";
            };
            this._details.style.transitionDuration = "1ms";
            this._details.addEventListener("transitionend", mainTransitionStart);
            this._details.style.maxHeight = (1*this._details.scrollHeight)+"px";
        }
    }

    setExpandedWithoutAnimation(state: boolean) {
        this._detailsToggle.checked = state;
        if (state) {
            this._detailsToggle.previousElementSibling.classList.add("rotated");
            this._detailsToggle.previousElementSibling.classList.remove("not-rotated");
            
            this._details.classList.remove("unfocused");
            this._details.classList.add("focused");
            this._details.style.maxHeight = "9999px";
            this._details.style.opacity = "100%";
        } else {
            this._detailsToggle.previousElementSibling.classList.remove("rotated");
            this._detailsToggle.previousElementSibling.classList.add("not-rotated");
            this._details.classList.add("unfocused");
            this._details.classList.remove("focused");
            this._details.style.maxHeight = "0";
            this._details.style.opacity = "0";
        }
    }

    focus = () => this._container.scrollIntoView({ behavior: "smooth", block: "center" });

    constructor(invite: Invite) {
        // first create the invite structure, then use our setter methods to fill in the data.
        this._container = document.createElement('div') as HTMLDivElement;
        this._container.classList.add("inv", "overflow-visible", "flex", "flex-col");
       
        // Stores gap-x so we can cancel it out for transitions
        this.gap = 2;

        this._header = document.createElement('div') as HTMLDivElement;
        this._container.appendChild(this._header);
        this._header.classList.add("card", "dark:~d_neutral", "@low", "inv-header", "flex", "flex-row", "justify-between", "overflow-visible", "gap-2", "z-[1]");

        this._codeArea = document.createElement('div') as HTMLDivElement;
        this._header.appendChild(this._codeArea);
        this._codeArea.classList.add("flex", "flex-row", "flex-wrap", "justify-between", "w-full", "items-center", "gap-2", "truncate");
        this._codeArea.innerHTML = `
        <div class="flex items-center gap-x-4 gap-y-2 truncate">
            <a class="invite-link-container text-black dark:text-white font-mono bg-inherit truncate"></a>
            <button class="invite-copy-button"></button>
        </div>
        <span>${window.lang.var("strings", "inviteExpiresInTime", "<span class=\"inv-duration\"></span>")}</span>
        `;

        this._linkContainer = this._codeArea.getElementsByClassName("invite-link-container")[0] as HTMLElement;
        this._labelEditor = new HiddenInputField({
            container: this._linkContainer,
            buttonOnLeft: false,
            customContainerHTML: `<a class="hidden-input-content invite-link text-black dark:text-white font-mono bg-inherit truncate"></a>`,
            clickAwayShouldSave: true,
            onSet: this._setLabel
        });
        this._linkEl = this._linkContainer.getElementsByClassName("invite-link")[0] as HTMLAnchorElement;

        const copyButton = this._codeArea.getElementsByClassName("invite-copy-button")[0] as HTMLButtonElement;
        SetupCopyButton(copyButton, () => { return this._codeLink; }); 

        this._infoArea = document.createElement('div') as HTMLDivElement;
        this._header.appendChild(this._infoArea);
        this._infoArea.classList.add("inv-infoarea", "flex", "flex-row", "items-center", "gap-2");
        this._infoArea.innerHTML = `
        <div class="tooltip below darker" tabindex="0">
            <span class="inv-email-chip h-full"><i></i></span>
            <span class="content sm p-1"></span>
        </div>
        <span class="button ~critical @low inv-delete h-full">${window.lang.strings("delete")}</span>
        <label>
            <i class="icon px-2.5 py-2 ri-arrow-down-s-line text-xl not-rotated"></i>
            <input class="inv-toggle-details unfocused" type="checkbox">
        </label>
        `;
        
        (this._infoArea.querySelector(".inv-delete") as HTMLSpanElement).onclick = this.delete;

        this._detailsToggle = (this._infoArea.querySelector("input.inv-toggle-details") as HTMLInputElement);
        this._detailsToggle.onclick = () => {
            this.expanded = this.expanded;
        };
        const toggleDetails = (event: Event) => { 
            if (event.target == this._header || event.target == this._codeArea || event.target == this._infoArea) {
                this.expanded = !this.expanded; 
            }
        };
        this._header.onclick = toggleDetails;


        this._details = document.createElement('div') as HTMLDivElement;
        this._container.appendChild(this._details);
        this._details.classList.add("card", "~neutral", "@low", "inv-details", "transition-all", "unfocused");
        this._details.style.maxHeight = "0";
        this._details.style.opacity = "0";
        const detailsInner = document.createElement('div') as HTMLDivElement;
        this._details.appendChild(detailsInner);
        detailsInner.classList.add("inv-row", "flex", "flex-row", "flex-wrap", "justify-between", "gap-4");

        this._left = document.createElement('div') as HTMLDivElement;
        this._left.classList.add("flex", "flex-row", "flex-wrap", "gap-4", "min-w-full", "sm:min-w-fit", "whitespace-nowrap");
        detailsInner.appendChild(this._left);
        const leftLeft = document.createElement("div") as HTMLDivElement;
        this._left.appendChild(leftLeft);
        leftLeft.classList.add("inv-profilearea", "min-w-full", "sm:min-w-fit", "flex", "flex-col", "gap-4");
        let innerHTML = `
        <label class="flex flex-col gap-2">
            <p class="label supra">${window.lang.strings("profile")}</p>
            <div class="select ~neutral @low inv-profileselect min-w-full inline-block">
                <select>
                    <option value="noProfile" selected>${window.lang.strings("inviteNoProfile")}</option>
                </select>
            </div>
        </label>
        `;
        if (window.notificationsEnabled) {
            innerHTML += `
            <div class="flex flex-col gap-2">
                <p class="label supra">${window.lang.strings("notifyEvent")}</p>
                <label class="switch block">
                    <input class="inv-notify-expiry" type="checkbox">
                    <span>${window.lang.strings("notifyInviteExpiry")}</span>
                </label>
                <label class="switch block">
                    <input class="inv-notify-creation" type="checkbox">
                    <span>${window.lang.strings("notifyUserCreation")}</span>
                </label>
            </div>
            `;
        }
        leftLeft.innerHTML = innerHTML;
        (this._left.querySelector("select") as HTMLSelectElement).onchange = this.updateProfile;
        
        if (window.notificationsEnabled) {
            const notifyExpiry = this._left.querySelector("input.inv-notify-expiry") as HTMLInputElement;
            notifyExpiry.onchange = () => { this._notifyExpiry = notifyExpiry.checked; this.updateNotify(notifyExpiry); };

            const notifyCreation = this._left.querySelector("input.inv-notify-creation") as HTMLInputElement;
            notifyCreation.onchange = () => { this._notifyCreation = notifyCreation.checked; this.updateNotify(notifyCreation); };
        }

        this._middle = document.createElement('div') as HTMLDivElement;
        this._left.appendChild(this._middle);
        this._middle.classList.add("flex", "flex-col", "grow", "gap-4");
        this._middle.innerHTML = `
        <p class="label flex items-center gap-2 supra">${window.lang.strings("inviteDateCreated")} <strong class="inv-created"></strong></p>
        <p class="label flex items-center gap-2 supra">${window.lang.strings("inviteRemainingUses")} <strong class="inv-remaining"></strong></p>
        <p class="label flex items-center gap-2 supra"><span class="user-expiry"></span> <strong class="user-expiry-time"></strong></p>
        <p class="flex items-center gap-2"><span class="user-label-label label supra"></span> <span class="user-label chip ~blue unfocused"></span></p>
        <div class="invite-send-to-dialog"></div>
        `;

        this._right = document.createElement('div') as HTMLDivElement;
        detailsInner.appendChild(this._right);
        this._right.classList.add("card", "~neutral", "@low", "inv-created-users", "min-w-full", "sm:min-w-fit", "whitespace-nowrap");
        this._right.innerHTML = `<span class="label supra table-header">${window.lang.strings("inviteUsersCreated")}</span>`;
        this._userTable = document.createElement('div') as HTMLDivElement;
        this._userTable.classList.add("text-sm", "mt-1", );
        this._right.appendChild(this._userTable);

        this.setExpandedWithoutAnimation(false);
        this.update(invite);

        document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);
        document.addEventListener("timefmt-change", () => {
            this.created = this.created;
            this.used_by = this.used_by;
        });
    }

    update = (invite: Invite) => {
        this.code = invite.code;
        this.valid_till = invite.valid_till;
        if (invite.user_expiry) {
            this.user_expiry = invite.user_expiry;
            this.user_expiry_time = {
                months: invite.user_months,
                days: invite.user_days,
                hours: invite.user_hours,
                minutes: invite.user_minutes
            };
        }
        this.created = invite.created;
        this.profile = invite.profile;
        this.used_by = invite.used_by;
        this.no_limit = invite.no_limit ? invite.no_limit : false;
        this.remaining_uses = invite.remaining_uses;
        this.send_to = invite.send_to;
        this.sent_to = invite.sent_to;
        if (window.notificationsEnabled) {
            this.notify_creation = invite.notify_creation;
            this.notify_expiry = invite.notify_expiry;
        }
        if (invite.label) {
            this.label = invite.label;
        }
        if (invite.user_label) {
            this.user_label = invite.user_label;
        }
        this._sendToDialog = new SendToDialog(this._middle.getElementsByClassName("invite-send-to-dialog")[0] as HTMLElement, invite, () => {
            const needsUpdatingEvent = new CustomEvent("inviteNeedsUpdating", { detail: this.code });
            document.dispatchEvent(needsUpdatingEvent);
        });
    }

    asElement = (): HTMLDivElement => { return this._container; }

    remove = () => { this._container.remove(); }
}

export class DOMInviteList implements InviteList {
    private _list: HTMLDivElement;
    private _empty: boolean;
    // since invite reload sends profiles, this event it broadcast so the createInvite object can load them.

    invites: { [code: string]: DOMInvite };

    focusInvite = (inviteCode: string, errorMsg: string = window.lang.notif("errorInviteNoLongerExists")) => {
        for (let code of Object.keys(this.invites)) {
            this.invites[code].setExpandedWithoutAnimation(code == inviteCode);
        }
        if (inviteCode in this.invites) this.invites[inviteCode].focus();
        else window.notifications.customError("inviteDoesntExistError", errorMsg);
    };

    public static readonly _inviteURLEvent = "invite-url";
    registerURLListener = () => document.addEventListener(DOMInviteList._inviteURLEvent, (event: CustomEvent) => {
        this.focusInvite(event.detail);
    })

    isInviteURL = () => {
        const urlParams = new URLSearchParams(window.location.search);
        const inviteCode = urlParams.get("invite");
        return Boolean(inviteCode);
    }

    loadInviteURL = () => {
        const urlParams = new URLSearchParams(window.location.search);
        const inviteCode = urlParams.get("invite");
        this.focusInvite(inviteCode, window.lang.notif("errorInviteNotFound"));
    }

    constructor() {
        this._list = document.getElementById('invites') as HTMLDivElement;
        this.empty = true;
        this.invites = {};
        // FIXME: Do this better, take advantage of getting the code in e.detail
        document.addEventListener("inviteNeedsUpdating", () => { this.reload(); }, false);

        document.addEventListener("newInviteEvent", () => { this.reload(); }, false);
        document.addEventListener("inviteDeletedEvent", (event: CustomEvent) => {
            const code = event.detail;
            const length = Object.keys(this.invites).length - 1; // store prior as Object.keys is undefined when there are no keys
            delete this.invites[code];
            if (length == 0) {
                this.empty = true;
            }
        }, false);

        this.registerURLListener();
    }

    get empty(): boolean { return this._empty; }
    set empty(state: boolean) {
        this._empty = state;
        if (state) {
            this.invites = {};
            this._list.classList.add("empty");
            this._list.innerHTML = `
            <div class="inv inv-empty">
                <div class="card dark:~d_neutral @low inv-header">
                    <div class="justify-start">
                        <span class="text-black dark:text-white font-mono bg-inherit">${window.lang.strings("inviteNoInvites")}</span>
                    </div>
                </div>
            </div>
            `;
        } else {
            this._list.classList.remove("empty");
            if (this._list.querySelector(".inv-empty")) {
                this._list.textContent = '';
            }
        }
    }

    add = (invite: Invite) => {
        let domInv = new DOMInvite(invite);
        this.invites[invite.code] = domInv;
        if (this.empty) { this.empty = false; }
        this._list.appendChild(domInv.asElement());
    }

    reload = (callback?: () => void) => reloadProfileNames(() => _get("/invites", null, (req: XMLHttpRequest) => {
        if (req.readyState == 4) {
            let data = req.response;
            if (data["invites"] === undefined || data["invites"] == null || data["invites"].length == 0) {
                this.empty = true;
                return;
            }
            // get a list of all current inv codes on dom
            // every time we find a match in resp, delete from list
            // at end delete all remaining in list from dom
            let invitesOnDOM: { [code: string]: boolean } = {};
            for (let code in this.invites) { invitesOnDOM[code] = true; }
            for (let invite of (data["invites"] as Array<Invite>)) {
                if (invite.code in this.invites) {
                    this.invites[invite.code].update(invite);
                    delete invitesOnDOM[invite.code];
                } else {
                    this.add(invite);
                }
            }
            for (let code in invitesOnDOM) {
                this.invites[code].remove();
                delete this.invites[code];
            }

            if (callback) callback();
        }
    }));
}

export const inviteURLEvent = (id: string) => { return new CustomEvent(DOMInviteList._inviteURLEvent, {"detail": id}) };

export class createInvite {
    private _sendTo: SendToDialog;
    private _userExpiryToggle = document.getElementById("create-user-expiry-enabled") as HTMLInputElement;
    private _uses = document.getElementById('create-uses') as HTMLInputElement;
    private _infUses = document.getElementById("create-inf-uses") as HTMLInputElement;
    private _infUsesWarning = document.getElementById('create-inf-uses-warning') as HTMLParagraphElement;
    private _createButton = document.getElementById("create-submit") as HTMLSpanElement;
    private _profile = document.getElementById("create-profile") as HTMLSelectElement;
    private _label = document.getElementById("create-label") as HTMLInputElement;
    private _userLabel = document.getElementById("create-user-label") as HTMLInputElement;

    private _months = document.getElementById("create-months") as HTMLSelectElement;
    private _days = document.getElementById("create-days") as HTMLSelectElement;
    private _hours = document.getElementById("create-hours") as HTMLSelectElement;
    private _minutes = document.getElementById("create-minutes") as HTMLSelectElement;
    private _userMonths = document.getElementById("user-months") as HTMLSelectElement;
    private _userDays = document.getElementById("user-days") as HTMLSelectElement;
    private _userHours = document.getElementById("user-hours") as HTMLSelectElement;
    private _userMinutes = document.getElementById("user-minutes") as HTMLSelectElement;

    private _invDurationButton = document.getElementById('radio-inv-duration') as HTMLInputElement;
    private _userExpiryButton = document.getElementById('radio-user-expiry') as HTMLInputElement;
    private _invDuration = document.getElementById('inv-duration');
    private _userExpiry = document.getElementById('user-expiry');

    private _sendToDiscord: (passData: string) => void;

    // Broadcast when new invite created
    private _newInviteEvent = new CustomEvent("newInviteEvent");
    private _firstLoad = true;

    private _count: number = 30;
    private _populateNumbers = () => {
        const fieldIDs = ["months", "days", "hours", "minutes"];
        const prefixes = ["create-", "user-"];
        for (let i = 0; i < fieldIDs.length; i++) {
            for (let j = 0; j < prefixes.length; j++) { 
                const field = document.getElementById(prefixes[j] + fieldIDs[i]);
                field.textContent = '';
                for (let n = 0; n <= this._count; n++) {
                   const opt = document.createElement("option") as HTMLOptionElement;
                   opt.textContent = ""+n;
                   opt.value = ""+n;
                   field.appendChild(opt);
                }
            }
        }
    }

    get label(): string { return this._label.value; }
    set label(label: string) { this._label.value = label; }

    get user_label(): string { return this._userLabel.value; }
    set user_label(label: string) { this._userLabel.value = label; }

    get infiniteUses(): boolean {
        return this._infUses.checked;
    }
    set infiniteUses(state: boolean) {
        this._infUses.checked = state;
        this._uses.disabled = state;
        if (state) {
            this._infUses.parentElement.classList.remove("~neutral");
            this._infUses.parentElement.classList.add("~urge");
            this._infUsesWarning.classList.remove("unfocused");
        } else {
            this._infUses.parentElement.classList.remove("~urge");
            this._infUses.parentElement.classList.add("~neutral");
            this._infUsesWarning.classList.add("unfocused");
        }
    }
    
    get uses(): number { return this._uses.valueAsNumber; }
    set uses(n: number) { this._uses.valueAsNumber = n; }

    private _checkDurationValidity = () => {
        if (this.months + this.days + this.hours + this.minutes == 0) {
            this._createButton.setAttribute("disabled", "");
            this._createButton.onclick = null;
        } else {
            this._createButton.removeAttribute("disabled");
            this._createButton.onclick = this.create;
        }
    }

    get months(): number {
        return +this._months.value;
    }
    set months(n: number) {
        this._months.value = ""+n;
        this._checkDurationValidity();
    }
    get days(): number {
        return +this._days.value;
    }
    set days(n: number) {
        this._days.value = ""+n;
        this._checkDurationValidity();
    }
    get hours(): number {
        return +this._hours.value;
    }
    set hours(n: number) {
        this._hours.value = ""+n;
        this._checkDurationValidity();
    }
    get minutes(): number {
        return +this._minutes.value;
    }
    set minutes(n: number) {
        this._minutes.value = ""+n;
        this._checkDurationValidity();
    }
    get userExpiry(): boolean {
        return this._userExpiryToggle.checked;
    }
    set userExpiry(enabled: boolean) {
        this._userExpiryToggle.checked = enabled;
        const parent = this._userExpiryToggle.parentElement;
        if (enabled) {
            parent.classList.add("~urge");
            parent.classList.remove("~neutral");
        } else {
            parent.classList.add("~neutral");
            parent.classList.remove("~urge");
        }
        this._userMonths.disabled = !enabled;
        this._userDays.disabled = !enabled;
        this._userHours.disabled = !enabled;
        this._userMinutes.disabled = !enabled;
    }
    get userMonths(): number {
        return +this._userMonths.value;
    }
    set userMonths(n: number) {
        this._userMonths.value = ""+n;
    }
    get userDays(): number {
        return +this._userDays.value;
    }
    set userDays(n: number) {
        this._userDays.value = ""+n;
    }
    get userHours(): number {
        return +this._userHours.value;
    }
    set userHours(n: number) {
        this._userHours.value = ""+n;
    }
    get userMinutes(): number {
        return +this._userMinutes.value;
    }
    set userMinutes(n: number) {
        this._userMinutes.value = ""+n;
    }

    get sendTo(): string {
        if (!(this._sendTo)) return "";
        if (this._sendTo.addresses.length > 1) console.error("FIXME: SendToDialog has collected more than one address, make them usable or fix it!");
        if (this._sendTo.addresses.length > 0) return this._sendTo.addresses[0];
        else return "";
    }
    set sendTo(address: string) { if (!(this._sendTo)) return; this._sendTo.addresses = [address]; }

    get profile(): string { 
        const val = this._profile.value;
        if (val == "noProfile") {
            return "";
        }
        return val;
    }
    set profile(p: string) {
        if (p == "") { p = "noProfile"; }
        this._profile.value = p;
    }

    loadProfiles = () => {
        let innerHTML = `<option value="noProfile">${window.lang.strings("inviteNoProfile")}</option>`;
        for (let profile of window.availableProfiles) {
            innerHTML += `<option value="${profile}">${profile}</option>`;
        }
        let selected = this.profile;
        this._profile.innerHTML = innerHTML;
        if (this._firstLoad) {
            this.profile = window.availableProfiles[0] || "";
            this._firstLoad = false;
        } else {
            this.profile = selected;
        }
    }

    create = () => {
        toggleLoader(this._createButton);
        let userExpiry = this.userExpiry;
        if (this.userMonths == 0 && this.userDays == 0 && this.userHours == 0 && this.userMinutes == 0) {
            userExpiry = false;
        }
        let send = {
            "months": this.months,
            "days": this.days,
            "hours": this.hours,
            "minutes": this.minutes,
            "user-expiry": userExpiry,
            "user-months": this.userMonths,
            "user-days": this.userDays,
            "user-hours": this.userHours,
            "user-minutes": this.userMinutes,
            "multiple-uses": (this.uses > 1 || this.infiniteUses),
            "no-limit": this.infiniteUses,
            "remaining-uses": this.uses,
            "send-to": this.sendTo,
            "profile": this.profile,
            "label": this.label,
            "user_label": this.user_label
        };
        _post("/invites", send, (req: XMLHttpRequest) => {
            if (req.readyState == 4) {
                if (req.status == 200 || req.status == 204) {
                    document.dispatchEvent(this._newInviteEvent);
                }
                toggleLoader(this._createButton);
            }
        });
    }

    constructor() {
        this._populateNumbers();
        this.months = 0;
        this.days = 0;
        this.hours = 0;
        this.minutes = 30;
        this._infUses.onchange = () => { this.infiniteUses = this.infiniteUses; };
        this.infiniteUses = false;
        this.userExpiry = false;
        this._userExpiryToggle.onchange = () => { this.userExpiry = this._userExpiryToggle.checked; }
        this._userMonths.disabled = true;
        this._userDays.disabled = true;
        this._userHours.disabled = true;
        this._userMinutes.disabled = true;
        this._createButton.onclick = this.create;
        this.sendTo = "";
        this.uses = 1;
        this.label = "";

        const checkDuration = () => {
            const invSpan = this._invDurationButton.nextElementSibling as HTMLSpanElement;
            const userSpan = this._userExpiryButton.nextElementSibling as HTMLSpanElement;
            if (this._invDurationButton.checked) {
                this._invDuration.classList.remove("unfocused");
                this._userExpiry.classList.add("unfocused");
                invSpan.classList.add("@high");
                invSpan.classList.remove("@low");
                userSpan.classList.add("@low");
                userSpan.classList.remove("@high");
            } else if (this._userExpiryButton.checked) {
                this._userExpiry.classList.remove("unfocused");
                this._invDuration.classList.add("unfocused");
                invSpan.classList.add("@low");
                invSpan.classList.remove("@high");
                userSpan.classList.add("@high");
                userSpan.classList.remove("@low");
            }
        };

        this._userExpiryButton.checked = false;
        this._invDurationButton.checked = true;
        this._userExpiryButton.onchange = checkDuration;
        this._invDurationButton.onchange = checkDuration;

        this._days.onchange = this._checkDurationValidity;
        this._months.onchange = this._checkDurationValidity;
        this._hours.onchange = this._checkDurationValidity;
        this._minutes.onchange = this._checkDurationValidity;
        document.addEventListener("profileLoadEvent", () => { this.loadProfiles(); }, false);

        const sendToContainer = document.getElementById("create-send-to-container");
        if (window.emailEnabled || window.discordEnabled) {
            this._sendTo = new SendToDialog(sendToContainer);
        } else { 
            sendToContainer.classList.add("unfocused");
        }
    }
}

class SendToDialog {
    private _container: HTMLElement;
    private _input: HTMLInputElement;
    private _submit?: HTMLButtonElement;
    // FIXME: Make an interface for multiple addresses
    // private _addresses: string[] = [];
    get addresses(): string[] {
        if (this._input.value) return [this._input.value];
        return [];
    }
    set addresses(v: string[]) {
        if (v.length > 0) this._input.value = v[0];
        // this._addresses = v;
    };

    private _search?: HTMLButtonElement;
    private _discordSearch?: DiscordSearch;


    constructor(container: HTMLElement, invite?: Invite, onSuccess?: () => void) {
        this._container = container;
        this._container.classList.add("flex", "flex-col", "gap-2");
        this._container.innerHTML = `
            <label class="label supra">${window.lang.strings("inviteSendToEmail")}</label>
            <div class="flex flex-row gap-2">
                <input class="input ~neutral @low send-to-dialog-input" type="email" placeholder="example@example.com">
                <button class="button ~urge @low send-to-dialog-search unfocused" title="${window.lang.strings("search")}">
                    <i class="icon ri-search-2-line"></i>
                </button>
                <button class="button ~urge @low send-to-dialog-submit unfocused" title="${window.lang.strings("submit")}">
                    <i class="icon ri-send-plane-2-line"></i>
                </button>
            </div>
        `;
        this._input = this._container.getElementsByClassName("send-to-dialog-input")[0] as HTMLInputElement;
        if (window.discordEnabled) {
            this._input.type = "text";
            this._input.placeholder = "example@example.com | user#1234";
            this._search = this._container.getElementsByClassName("send-to-dialog-search")[0] as HTMLButtonElement;
            this._search.classList.remove("unfocused");
            this._discordSearch = newDiscordSearch(
                window.lang.strings("findDiscordUser"),
                window.lang.strings("searchDiscordUser"),
                window.lang.strings("select"),
                (user: DiscordUser) => {
                    this.addresses = [user.name];
                    // this.addresses.push(user.name);

                    window.modals.discord.close();
                }
            );
            // FIXME: Check why we're passing an empty string rather than the input value
            this._search.onclick = () => this._discordSearch("");
        }

        if (invite) {
            if (this._search) {
                this._search.classList.add("~neutral");
                this._search.classList.remove("~urge");
            }
            this._submit = this._container.getElementsByClassName("send-to-dialog-submit")[0] as HTMLButtonElement;
            this._submit.classList.remove("unfocused");
            this._submit.onclick = () => {
                const icon = this._submit.children[0] as HTMLElement;
                addLoader(icon, true);
                if (this.addresses.length == 0) return;
                _post("/invites/send", {"invite": invite.code, "send-to": this.addresses[0]}, (req: XMLHttpRequest) => {
                    if (req.readyState != 4) return;
                    removeLoader(icon, true)
                    if (req.status != 200 && req.status != 204) {
                        window.notifications.customError("errorSendInvite", window.lang.notif("errorFailureCheckLogs"));
                        return;
                    }
                    window.notifications.customSuccess("sendInvite", window.lang.strings("sent"));
                    if (onSuccess) onSuccess();
                    this.addresses = [];
                });
            };
            this._input.addEventListener("keypress", (e: KeyboardEvent) => {
                if (e.key === "Enter") {
                    e.preventDefault();
                    this._submit.click();
                }
            })
        }
    }
}
