import { msgBus } from "../events/MsgBus.js";
import { $msg, $pres, Strophe } from "strophe.js";
import { loadTextProductInWindow, hideTextWindow } from "../ui/text-window.js";
import {
    iembotFilter,
    printGrid,
    showHtmlVersion,
} from "../utils/grid-utilities.js";
import { fromLonLat } from "ol/proj";
import { getMap } from "../map/MapPanel.js";
import { LiveConfig } from "../config.js";
import { getPreference, setPreference } from "../utils/prefs.js";
import { DataTip } from "../ui/data-tip.js";
import { Application } from "../app-state.js";

function createOutgoingMessageStanza(panel, text, fgcolor, bgcolor) {
    const messageId =
        "msg-" + Date.now() + "-" + Math.random().toString(36).slice(2, 8);
    const parser = new DOMParser();
    const doc = parser.parseFromString(
        Application.replaceURLWithHTMLLinks(text),
        "text/html",
    );
    const nodes = Array.from(doc.body.childNodes);

    let msg = $msg({
        to: panel.barejid,
        type: panel.chatType,
        id: messageId,
    })
        .c("active", {
            xmlns: "http://jabber.org/protocol/chatstates",
        })
        .up()
        .c("body")
        .t(text)
        .up();

    if (panel.chatType === "chat") {
        msg = msg
            .c("request", {
                xmlns: "urn:xmpp:receipts",
            })
            .up();
    }

    msg = msg
        .c("html", {
            xmlns: "http://jabber.org/protocol/xhtml-im",
        })
        .c("body", {
            xmlns: "http://www.w3.org/1999/xhtml",
        })
        .c("p")
        .c("span", {
            style: "color:#" + fgcolor + ";background:#" + bgcolor + ";",
        });

    for (let i = 0; i < nodes.length; i++) {
        msg = msg.cnode(nodes[i]);
        if (i < nodes.length) {
            msg = msg.up();
        }
    }

    return { msg, messageId };
}

function retryFailedDirectMessages(panel, store) {
    if (
        !panel ||
        panel.chatType !== "chat" ||
        !store ||
        !Application.XMPPConn ||
        !Application.XMPPConn.authenticated
    ) {
        return;
    }

    const fgcolor = getPreference("fgcolor", "000000");
    const bgcolor = getPreference("bgcolor", "FFFFFF");
    let retryCount = 0;

    store.each(function (record) {
        if (record.get("delivery_status") !== "failed") {
            return;
        }
        const rawMessage = record.get("raw_message");
        if (!rawMessage) {
            return;
        }
        const { msg, messageId } = createOutgoingMessageStanza(
            panel,
            rawMessage,
            fgcolor,
            bgcolor,
        );
        Application.XMPPConn.send(msg);
        record.set("stanza_id", messageId);
        record.set("delivery_status", "sent");
        retryCount += 1;
    });

    if (retryCount > 0) {
        Application.log("Retried failed direct messages: " + retryCount);
    }
}

const MUCChatPanel = Ext.extend(Ext.Panel, {
    hideMode: "offsets",
    closable: true,
    layout: "border",
    chatType: "groupchat",
    barejid: null,
    handle: null,
    anonymous: null,
    joinedChat: false /* Was I successful at getting logged into room */,

    initComponent () {
        // Create the child components with references immediately
        const gridPanel = new ChatGridPanel({
            region: "center",
        });
        const textEntry = new ChatTextEntry({
            region: "south",
            height: 50,
            split: true,
        });

        // Store references immediately so they're available for message handling
        this.gp = gridPanel;
        this.te = textEntry;

        this.items = [gridPanel, textEntry];

        if (this.initialConfig.chatType !== "allchats") {
            const roomUsers = new MUCRoomUsers({
                region: "east",
                width: 175,
                collapsible: true,
                split: true,
            });
            this.roomusers = roomUsers;
            this.items.push(roomUsers);
            this.iconCls = "tabno";
        } else {
            this.items[1].emptyText = "Type message here";
        }
        const iconfig = {
            listeners: {
                activate (self) {
                    // In ExtJS 6, just update the icon class when tab becomes active
                    if (self.iconCls) {
                        self.setIconCls("tabno");
                    }
                },
                deactivate () {
                    // Icon class can be updated on deactivate if needed
                    // Currently no action needed
                },
            },
        };
        Ext.apply(this, Ext.apply(this.initialConfig, iconfig));

        MUCChatPanel.superclass.initComponent.apply(this, arguments);
        this.buildItems();
    },
    clearRoom () {
        this.gp.getStore().removeAll();
        this.roomusers.getRootNode().removeAll();
    },
    getJidByHandle (handle) {
        const node = this.roomusers.getRootNode().findChild("text", handle);
        if (node === null) {
            return null;
        }
        if (
            Strophe.getDomainFromJid(node.data.jid) === LiveConfig.XMPPMUCHOST
        ) {
            return null;
        }
        return node.data.jid;
    },
    buildItems () {
        // References are already set in initComponent, now just configure them
        if (this.chatType === "allchats") {
            //this.gp.toolbars[0].items.items[4].setText("Sounds Off");
            const toolbar = this.gp.down("toolbar");
            if (toolbar && toolbar.items && toolbar.items.items[4]) {
                toolbar.items.items[4].disable();
            }
            this.gp.soundOn = false;
            if (this.te.items && this.te.items.items[1]) {
                this.te.items.items[1].setText("To Room?");
            }
            return;
        }

        if (this.roomusers) {
            // In ExtJS 6, configure sorting on the tree's store instead of using TreeSorter
            const store = this.roomusers.getStore();
            if (store) {
                store.sort({
                    property: "text",
                    direction: "ASC",
                });
            }
        }

        /* Disable text box for anonymous rooms */
        if (this.anonymous) {
            this.te.disable();
        }

        let pref = "muc::" + Strophe.getNodeFromJid(this.barejid) + "::mute";
        if (getPreference(pref, false)) {
            /* hacky */
            const toolbar = this.gp.down("toolbar");
            if (toolbar && toolbar.items && toolbar.items.items[4]) {
                toolbar.items.items[4].toggle(true, true);
                toolbar.items.items[4].setText("Sounds Muted");
            }
            this.gp.soundOn = false;
        }

        pref =
            "muc::" + Strophe.getNodeFromJid(this.barejid) + "::iembothidden";
        if (getPreference(pref, false)) {
            /* hacky */
            const toolbar = this.gp.down("toolbar");
            if (toolbar && toolbar.items && toolbar.items.items[3]) {
                toolbar.items.items[3].toggle(true, true);
                toolbar.items.items[3].setText("IEMBot Hidden");
            }
            this.gp.store.filterBy(iembotFilter);
        }
    },
});

const ChatPanel = Ext.extend(Ext.Panel, {
    hideMode: "offsets",
    closable: true,
    layout: "border",
    iconCls: "tabno",
    chatType: "chat",
    barejid: null,
    handle: null,
    anonymous: null,

    initComponent () {
        // Create components and store references immediately
        const gridPanel = new ChatGridPanel({
            region: "center",
        });
        const textEntry = new ChatTextEntry({
            region: "south",
            height: 50,
            split: true,
        });

        this.gp = gridPanel;
        this.te = textEntry;
        this.items = [gridPanel, textEntry];

        const iconfig = {
            listeners: {
                activate (self) {
                    self.setIconCls("tabno");
                },
                deactivate (self) {
                    self.setIconCls("tabno");
                },
                beforedestroy (self) {
                    if (self.te.chatstate) {
                        self.te.chatstate.cancel();
                    }
                    Application.XMPPConn.send(
                        $msg({
                            to: self.barejid,
                            type: self.chatType,
                        }).c("gone", {
                            xmlns: "http://jabber.org/protocol/chatstates",
                        }),
                    );
                },
            },
        };
        Ext.apply(this, Ext.apply(this.initialConfig, iconfig));

        ChatPanel.superclass.initComponent.apply(this, arguments);
        this.buildItems();
    },
    getJidByHandle () {
        return this.barejid;
    },
    buildItems () {
        // References already set in initComponent
        const toolbar = this.gp.getDockedItems
            ? this.gp.getDockedItems("toolbar[dock=top]")[0]
            : this.gp.getTopToolbar
              ? this.gp.getTopToolbar()
              : null;
        if (!toolbar || !toolbar.items || !toolbar.items.each) {
            return;
        }

        const labelsToRemove = new Set(["Hide IEMBot", "Mute Sounds"]);
        const itemsToRemove = [];
        toolbar.items.each((item) => {
            if (item && labelsToRemove.has(item.text)) {
                itemsToRemove.push(item);
            }
        });

        itemsToRemove.forEach((item) => {
            toolbar.remove(item, true);
        });

        const retryFailedBtn = toolbar.getComponent("retryFailedBtn");
        if (retryFailedBtn) {
            retryFailedBtn.show();
        }

        if (toolbar.doLayout) {
            toolbar.doLayout();
        }
    },
});

const MUCRoomUsers = Ext.extend(Ext.tree.TreePanel, {
    bodyStyle: { "margin-left": "-15px" },
    title: "0 people in room",
    rootVisible: false,
    lines: false,
    autoScroll: true,
    plugins: new DataTip({
        tpl: "<div>JID: {jid}<br />Affiliation: {affiliation}<br />Role: {role}</div>",
        constrainPosition: true,
    }),
    listeners: {
        click (n) {
            if (!n.data.jid) {
                return;
            }
            const username = Strophe.getNodeFromJid(n.data.jid);
            /* Can't speak with ourself */
            if (username === Application.USERNAME) {
                return;
            }
            /* Now, we either talk with private or private via MUC */
            let jid = n.data.jid;
            if (
                Strophe.getDomainFromJid(n.data.jid) ===
                LiveConfig.XMPPHOST
            ) {
                jid = Strophe.getBareJidFromJid(n.data.jid);
            }
            Application.log("Wish to start chat with:" + jid);
            let cp = ChatPanel.getChat(jid);
            if (!cp) {
                cp = ChatPanel.addChat(jid);
            }
            ChatPanel.setActiveTab(cp);
        },
    },
    initComponent () {
        this.root = {
            text: "Room Users",
            expanded: true,
        };

        MUCRoomUsers.superclass.initComponent.apply(this, arguments);

        // Robustly update the title with the number of people in the room
        const updateTitle = () => {
            const root = this.getRootNode && this.getRootNode();
            let count = 0;
            if (root && root.childNodes) {
                // Only count visible, non-placeholder nodes
                count = root.childNodes.filter(function (n) {
                    return n && n.data && n.data.jid;
                }).length;
            }
            this.setTitle(`${count} people in room`);
        };

        // Listen for node and store events
        const rootNode = this.getRootNode && this.getRootNode();
        if (rootNode) {
            rootNode.on("append", updateTitle, this);
            rootNode.on("remove", updateTitle, this);
        }
        if (this.getStore && this.getStore()) {
            this.getStore().on("add", updateTitle, this);
            this.getStore().on("remove", updateTitle, this);
        }
        updateTitle();
    },
});

Application.colors = [
    "000000", //black
    "666666", //
    "D51460", //
    "FF0000", //
    "993333", //
    "FF9900", //
    "005500", //
    "009900", //
    "00DD00", //
    "0066CC", //
    "3399FF", //
    "0000FF", //
    "6666FF", //
];
Application.colorpointer = 0;

Application.UserColorStore = new Ext.data.Store({
    fields: ["user", "color"],
});

Application.getUserColor = function (user) {
    const idx = Application.UserColorStore.find("user", user);
    if (idx === -1) {
        const c = Application.colors[Application.colorpointer];
        Application.UserColorStore.add(
            new Ext.data.Record({ user, color: c }),
        );
        Application.colorpointer++;
        if (Application.colorpointer > 12) {
            Application.colorpointer = 0;
        }
        return c;
    } else {
        return Application.UserColorStore.getAt(idx).get("color");
    }
};

const ChatTextEntry = Ext.extend(Ext.Panel, {
    layout: "hbox",
    layoutConfig: {
        align: "stretch",
    },
    border: false,
    chatstate: null,
    initComponent () {
        this.items = [
            {
                xtype: "textarea",
                flex: 1,
                cls: "message-entry-box",
                autoCreate: {
                    tag: "textarea",
                    style: 'rows:10;cols:72;wrap:"hard";',
                    autocomplete: "off",
                },
                style: {
                    background: "#" + getPreference("bgcolor", "FFFFFF"),
                    color: "#" + getPreference("fgcolor", "000000"),
                },
                enableKeyEvents: true,
                listeners: {
                    keyup (elTxt, e) {
                        // e.HOME, e.END, e.PAGE_UP, e.PAGE_DOWN,
                        // e.TAB, e.ESC, arrow keys: e.LEFT,
                        // e.RIGHT, e.UP,
                        // e.DOWN
                        if (e.getKey() === e.ENTER && !e.shiftKey) {
                            if (this.chatstate) {
                                this.chatstate.cancel();
                            }
                            this.chatstate = null;
                            this.ownerCt.getComponent(1).handler();
                            return true;
                        }
                        /* Chat States! */
                        if (this.ownerCt.ownerCt.chatType === "chat") {
                            if (!this.chatstate) {
                                this.chatstate = new Ext.util.DelayedTask(
                                    function () {
                                        if (
                                            !this.ownerCt ||
                                            !this.ownerCt.ownerCt
                                        ) {
                                            return;
                                        }
                                        Application.XMPPConn.send(
                                            $msg({
                                                to: this.ownerCt.ownerCt
                                                    .barejid,
                                                type: this.ownerCt.ownerCt
                                                    .chatType,
                                            }).c("paused", {
                                                xmlns: "http://jabber.org/protocol/chatstates",
                                            }),
                                        );
                                        this.chatstate = null;
                                    },
                                    this,
                                );
                                Application.XMPPConn.send(
                                    $msg({
                                        to: this.ownerCt.ownerCt.barejid,
                                        type: this.ownerCt.ownerCt.chatType,
                                    }).c("composing", {
                                        xmlns: "http://jabber.org/protocol/chatstates",
                                    }),
                                );
                            }
                            /* Wait 5 seconds before pausing */
                            this.chatstate.delay(5000);
                        }
                    },
                    render: {
                        delay: 500,
                        fn () {
                            this.focus();
                        },
                    },
                },
            },
            {
                xtype: "button",
                text: "Send",
                width: 60,
                popup: null,
                handler () {
                    const txt = this.ownerCt.getComponent(0);
                    let text = txt.getValue().trim();
                    if (text.length === 0) {
                        txt.focus();
                        return false;
                    }
                    const bgcolor = getPreference("bgcolor", "FFFFFF");
                    const fgcolor = getPreference("fgcolor", "000000");

                    /* allchat */
                    if (this.ownerCt.ownerCt.chatType === "allchats") {
                        txt.emptyText = "";
                        txt.setValue("");
                        new Application.AllChatMessageWindow({
                            message: Application.replaceURLWithHTMLLinks(text),
                        }).show();
                    } else {
                        const chatPanel = this.ownerCt.ownerCt;
                        const chatType = chatPanel.chatType;
                        const { msg, messageId } = createOutgoingMessageStanza(
                            chatPanel,
                            text,
                            fgcolor,
                            bgcolor,
                        );
                        Application.XMPPConn.send(msg);
                        txt.setValue("");
                        txt.focus();

                        // Since we don't get our messages back via XMPP
                        // we need to manually add to the store
                        if (chatType === "chat") {
                            text =
                                "<span " +
                                "style='color:#" +
                                fgcolor +
                                ";background:#" +
                                bgcolor +
                                ";'>" +
                                text +
                                "</span>";
                            this.ownerCt.ownerCt.gp.getStore().add({
                                ts: new Date(),
                                author: Application.USERNAME,
                                message:
                                    Application.replaceURLWithHTMLLinks(text),
                                stanza_id: messageId,
                                delivery_status: "sent",
                                raw_message: text,
                            });
                        }
                    }
                },
            },
        ];
        ChatTextEntry.superclass.initComponent.apply(this, arguments);
    },
});

Application.msgFormatter = new Ext.XTemplate(
    '<div class="chat-message-row {[this.getMessageRowClass(values)]}">',
    '<div class="chat-message-header {[this.getAuthorClass(values.jid)]}">',
    '<span class="chat-message-ts">',
    '<tpl if="this.isNotToday(ts)">',
    '{ts:date("d M")} ',
    "</tpl>",
    '{ts:date("g:i A")}',
    "</span>",
    '<span class="chat-message-sep">·</span>',
    '<tpl if="values.room != null">',
    '<span class="chat-message-room">[{room}]</span>',
    '<span class="chat-message-sep">·</span>',
    "</tpl>",
    '<span class="chat-message-author" style="{[this.getAuthorStyle(values.author, values.jid)]}">{author}</span>',
    '<tpl if="values.me === values.author && values.delivery_status">',
    '<span class="chat-message-delivery {[this.getDeliveryClass(values.delivery_status)]}">{[this.getDeliveryLabel(values.delivery_status)]}</span>',
    '</tpl>',
    '{[this.getGeoBadge(values)]}',
    "</div>",
    '<div class="chat-message-body">{message}</div>',
    "</div>",
    {
        isNotToday (ts) {
            // Use Ext.Date.format for date formatting
            const today = new Date();
            return Ext.Date.format(today, "md") !== Ext.Date.format(ts, "md");
        },
        getMessageRowClass (values) {
            if (values.me === values.author) {
                return "chat-message-own";
            }
            return "chat-message-peer";
        },
        getAuthorClass (jid) {
            //console.log("node: "+Strophe.getNodeFromJid(jid) );
            if (jid === null || typeof jid === "undefined") {
                return "author-default";
            }
            if (Strophe.getNodeFromJid(jid) === "iembot") {
                return "author-iembot";
            }
            if (Strophe.getNodeFromJid(jid).match(/^nws/)) {
                return "author-nws";
            }

            return "author-chatpartner";
        },
        getAuthorStyle (author, jid) {
            if (jid) {
                const node = Strophe.getNodeFromJid(jid);
                if (node === "iembot" || (node && node.match(/^nws/))) {
                    return "";
                }
            }
            if (
                !Application.getUserColor ||
                typeof Application.getUserColor !== "function"
            ) {
                return "";
            }
            const userColor = Application.getUserColor(author) || "000000";
            return `color: #${userColor};`;
        },
        getDeliveryLabel (deliveryStatus) {
            if (deliveryStatus === "delivered") {
                return "✓✓";
            }
            if (deliveryStatus === "sent") {
                return "✓";
            }
            if (deliveryStatus === "failed") {
                return "⚠";
            }
            return "";
        },
        getDeliveryClass (deliveryStatus) {
            if (deliveryStatus === "failed") {
                return "chat-message-delivery-failed";
            }
            return "chat-message-delivery-ok";
        },
        getGeoBadge (values) {
            const rawLat = values.geo_lat;
            const rawLon = values.geo_long;
            if (
                rawLat === null ||
                typeof rawLat === "undefined" ||
                rawLat === "" ||
                rawLon === null ||
                typeof rawLon === "undefined" ||
                rawLon === ""
            ) {
                return "";
            }

            const lat = Number(rawLat);
            const lon = Number(rawLon);
            if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
                return "";
            }
            if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
                return "";
            }
            return (
                `<button class="chat-map-zoom-btn" type="button" ` +
                `data-lat="${lat}" data-long="${lon}" ` +
                `title="Zoom map to ${lat.toFixed(3)}, ${lon.toFixed(3)}">` +
                "Zoom Map" +
                "</button>"
            );
        },
    },
);

const ChatGridPanel = Ext.extend(Ext.grid.GridPanel, {
    cls: "chat-grid-panel",
    region: "center",
    soundOn: true,
    iembotHide: false,
    stripeRows: true,
    autoScroll: true,
    stickToBottom: true,
    pendingAutoScroll: false,
    unreadCount: 0,
    autoScrollThresholdPx: 60,

    tbar: [
        {
            text: "Clear Room Log",
            cls: "x-btn-text-icon",
            icon: "icons/close.png",
            handler: (btn) => {
                btn.ownerCt.ownerCt.getStore().removeAll();
            },
        },
        {
            text: "Print Log",
            icon: "icons/print.png",
            cls: "x-btn-text-icon",
            handler: (btn) => {
                printGrid(btn.ownerCt.ownerCt);
            },
        },
        {
            text: "View As HTML",
            handler: (btn) => {
                showHtmlVersion(btn.ownerCt.ownerCt);
            },
            icon: "icons/text.png",
            cls: "x-btn-text-icon",
        },
        {
            text: "Hide IEMBot",
            enableToggle: true,
            toggleHandler (btn, toggled) {
                const pref =
                    "muc::" +
                    Strophe.getNodeFromJid(
                        btn.ownerCt.ownerCt.ownerCt.barejid,
                    ) +
                    "::iembothidden";
                const store = btn.ownerCt.ownerCt.getStore();
                btn.ownerCt.ownerCt.iembotHide = toggled;
                if (toggled) {
                    setPreference(pref, "true");
                    store.filterBy(iembotFilter);
                    btn.setText("IEMBot Hidden");
                } else {
                    Application.removePreference(pref);
                    store.clearFilter(false);
                    btn.setText("Hide IEMBot");
                }
            },
        },
        {
            text: "Mute Sounds",
            enableToggle: true,
            toggleHandler: (btn, toggled) => {
                const pref =
                    "muc::" +
                    Strophe.getNodeFromJid(
                        btn.ownerCt.ownerCt.ownerCt.barejid,
                    ) +
                    "::mute";
                if (toggled) {
                    setPreference(pref, "true");
                    btn.ownerCt.ownerCt.soundOn = false;
                    btn.setText("Sounds Muted");
                } else {
                    Application.removePreference(pref);
                    btn.ownerCt.ownerCt.soundOn = true;
                    btn.setText("Mute Sounds");
                }
            },
        },
        {
            itemId: "retryFailedBtn",
            text: "Retry Failed",
            hidden: true,
            handler (btn) {
                const grid = btn.ownerCt && btn.ownerCt.ownerCt;
                const panel = grid && grid.ownerCt;
                const store = grid && grid.getStore ? grid.getStore() : null;
                retryFailedDirectMessages(panel, store);
            },
        },
        {
            text: "Move Tab Left",
            handler (btn) {
                const grid = btn.ownerCt && btn.ownerCt.ownerCt;
                const chatPanel = grid && grid.ownerCt;
                const tabPanel = chatPanel && chatPanel.ownerCt;
                if (tabPanel && tabPanel.moveActiveTabBy) {
                    Ext.defer(function () {
                        tabPanel.moveActiveTabBy(-1, btn);
                    }, 1);
                }
            },
        },
        {
            text: "Move Tab Right",
            handler (btn) {
                const grid = btn.ownerCt && btn.ownerCt.ownerCt;
                const chatPanel = grid && grid.ownerCt;
                const tabPanel = chatPanel && chatPanel.ownerCt;
                if (tabPanel && tabPanel.moveActiveTabBy) {
                    Ext.defer(function () {
                        tabPanel.moveActiveTabBy(1, btn);
                    }, 1);
                }
            },
        },
        {
            icon: "icons/font-less.png",
            handler () {
                const size = parseInt(getPreference("font-size", 14)) - 2;
                setPreference("font-size", size);
                //var cssfmt = String.format('normal {0}px/{1}px arial', size, size +2);
                const cssfmt = `normal ${size}px arial`;
                Ext.util.CSS.updateRule(".chat-message-cell", "font", cssfmt);
                Ext.util.CSS.updateRule(".message-entry-box", "font", cssfmt);
            },
        },
        {
            icon: "icons/font-more.png",
            handler () {
                const size = parseInt(getPreference("font-size", 14)) + 2;
                setPreference("font-size", size);
                const cssfmt = `normal ${size}px arial`;
                Ext.util.CSS.updateRule(".chat-message-cell", "font", cssfmt);
                Ext.util.CSS.updateRule(".message-entry-box", "font", cssfmt);
            },
        },
        "->",
        {
            itemId: "jumpToLatestBtn",
            text: "Jump to latest",
            hidden: true,
            handler (btn) {
                const grid = btn.ownerCt && btn.ownerCt.ownerCt;
                if (!grid) {
                    return;
                }
                grid.jumpToLatest();
            },
        },
    ],
    jumpToLatest () {
        this.stickToBottom = true;
        this.unreadCount = 0;
        this.updateJumpToLatestButton();
        this.queueScrollToLatest();
    },
    getToolbar () {
        if (this.getDockedItems) {
            return this.getDockedItems("toolbar[dock=top]")[0] || null;
        }
        if (this.getTopToolbar) {
            return this.getTopToolbar();
        }
        return null;
    },
    getJumpToLatestButton () {
        const toolbar = this.getToolbar();
        return toolbar && toolbar.getComponent
            ? toolbar.getComponent("jumpToLatestBtn")
            : null;
    },
    createJumpToLatestBadge () {
        if (this.jumpToLatestBadgeEl || !this.getEl || !this.getEl()) {
            return;
        }
        const badgeDom = Ext.DomHelper.append(
            this.getEl(),
            {
                tag: "button",
                type: "button",
                cls: "chat-jump-latest-badge",
                style: "display:none;",
                html: "Jump to latest",
            },
            true,
        );
        if (!badgeDom) {
            return;
        }
        badgeDom.on("click", (event) => {
            event.stopEvent();
            this.jumpToLatest();
        });
        this.jumpToLatestBadgeEl = badgeDom;
    },
    getScrollElement () {
        const gridView = this.getView && this.getView();
        if (!gridView) {
            return null;
        }
        if (gridView.scroller && gridView.scroller.dom) {
            return gridView.scroller.dom;
        }
        const gridViewEl =
            gridView && gridView.getEl ? gridView.getEl() : gridView && gridView.el;
        return gridViewEl && gridViewEl.dom ? gridViewEl.dom : null;
    },
    isNearBottom () {
        const dom = this.getScrollElement();
        if (!dom) {
            return true;
        }
        const distanceFromBottom = dom.scrollHeight - dom.scrollTop - dom.clientHeight;
        return distanceFromBottom <= this.autoScrollThresholdPx;
    },
    updateBottomState () {
        const nearBottom = this.isNearBottom();
        this.stickToBottom = nearBottom;
        if (nearBottom && this.unreadCount !== 0) {
            this.unreadCount = 0;
            this.updateJumpToLatestButton();
        }
    },
    updateJumpToLatestButton () {
        const button = this.getJumpToLatestButton();
        const badge = this.jumpToLatestBadgeEl;
        const label =
            this.unreadCount > 0
                ? `${this.unreadCount} new message${this.unreadCount === 1 ? "" : "s"} \u2193`
                : "Jump to latest";

        if (this.unreadCount > 0) {
            if (button) {
                button.hide();
                button.setText(label);
            }
            if (badge) {
                badge.update(label);
                badge.show();
            }
        } else {
            if (button) {
                button.hide();
                button.setText("Jump to latest");
            }
            if (badge) {
                badge.hide();
                badge.update("Jump to latest");
            }
        }
    },
    handleIncomingRecords (records) {
        this.updateBottomState();
        let newVisibleMessageCount = 0;
        for (let i = 0; i < records.length; i++) {
            if (!records[i].get("xdelay")) {
                newVisibleMessageCount += 1;
            }
        }
        if (newVisibleMessageCount === 0) {
            return;
        }
        if (this.stickToBottom) {
            this.unreadCount = 0;
            this.updateJumpToLatestButton();
            this.queueScrollToLatest();
            return;
        }
        this.unreadCount += newVisibleMessageCount;
        this.updateJumpToLatestButton();
    },
    queueScrollToLatest () {
        this.pendingAutoScroll = true;
        Ext.defer(
            function () {
                this.flushPendingAutoScroll();
            },
            1,
            this,
        );
    },
    flushPendingAutoScroll () {
        if (!this.pendingAutoScroll || this.destroyed) {
            return;
        }

        const gridStore = this.getStore && this.getStore();
        if (!gridStore || gridStore.getCount() === 0) {
            this.pendingAutoScroll = false;
            return;
        }

        const gridView = this.getView && this.getView();
        const scrollDom = this.getScrollElement();
        const ownerPanel = this.ownerCt;
        const visible =
            (!this.isVisible || this.isVisible(true) !== false) &&
            (!ownerPanel ||
                !ownerPanel.isVisible ||
                ownerPanel.isVisible(true) !== false);

        if (
            !this.rendered ||
            !gridView ||
            !gridView.rendered ||
            !scrollDom ||
            !visible
        ) {
            return;
        }

        const lastIndex = gridStore.getCount() - 1;
        const lastRecord = gridStore.getAt && gridStore.getAt(lastIndex);

        if (lastRecord && typeof this.ensureVisible === "function") {
            this.ensureVisible(lastRecord, {
                animate: false,
                focus: false,
                highlight: false,
                select: false,
            });
        } else {
            const row = gridView.getRow && gridView.getRow(lastIndex);
            if (row && typeof row.scrollIntoView === "function") {
                row.scrollIntoView(false);
            }
            scrollDom.scrollTop = scrollDom.scrollHeight;
        }
        this.pendingAutoScroll = false;
        this.stickToBottom = true;
        this.unreadCount = 0;
        this.updateJumpToLatestButton();
    },
    initComponent () {
        this.columns = [
            {
                header: "Author",
                sortable: true,
                dataIndex: "author",
                hidden: true,
            },
            {
                // Removed id: "message" to avoid duplicate component id errors
                header: "Message",
                sortable: true,
                dataIndex: "ts",
                tdCls: "chat-message-cell",
                cellWrap: true,
                flex: 1,
                renderer (_value, _p, record) {
                    const parentPanel = this.ownerCt;
                    const data = {
                        author: record.get("author"),
                        message: record.get("message"),
                        ts: record.get("ts"),
                        room: record.get("room"),
                        jid: record.get("jid"),
                        delivery_status: record.get("delivery_status"),
                        geo_lat: record.get("geo_lat"),
                        geo_long: record.get("geo_long"),
                        me: parentPanel ? parentPanel.handle : null,
                    };
                    const html = Application.msgFormatter.apply(data);
                    return html;
                },
            },
        ];
        this.store = new Ext.data.ArrayStore({
            sortInfo: {
                field: "ts",
                direction: "ASC",
            },
            fields: [
                {
                    name: "ts",
                    type: "date",
                },
                "author",
                "message",
                "product_id",
                "jid",
                "room",
                {
                    name: "xdelay",
                    type: "boolean",
                },
                "stanza_id",
                "delivery_status",
                "raw_message",
                "geo_lat",
                "geo_long",
            ],
        });
        this.store.on(
            "add",
            function (_store, records) {
                this.handleIncomingRecords(records);
            },
            this,
        );
        this.store.on(
            "add",
            function (_store, records) {
                if (!this.soundOn) {
                    return true;
                }
                let nonIEMBot = false;
                let nothingNew = true;
                for (let i = 0; i < records.length; i++) {
                    /* No events for delayed messages */
                    if (records[i].get("xdelay")) {
                        continue;
                    }
                    /* No events if I talked! */
                    if (records[i].get("author") === this.ownerCt.handle) {
                        continue;
                    }
                    if (records[i].get("author") !== "iembot") {
                        nonIEMBot = true;
                    }
                    const messageText = records[i].get("message");
                    const handle = this.ownerCt && this.ownerCt.handle;
                    if (
                        typeof messageText === "string" &&
                        messageText.match(/tornado/i)
                    ) {
                        msgBus.fire("soundevent", "tornado");
                    }
                    // TODO: figure out how to make this case insensitive
                    if (
                        typeof messageText === "string" &&
                        handle &&
                        messageText.match(handle)
                    ) {
                        msgBus.fire("soundevent", "myhandle");
                    }
                    nothingNew = false;
                } //end of for()
                if (nothingNew) {
                    return true;
                }
                if (nonIEMBot) {
                    // Make this tab show the new icon for the new message
                    this.ownerCt.setIconCls("new-tab");
                    msgBus.fire("soundevent", "new_message");
                } else {
                    // If iembot is muted, lets stop the events
                    if (!this.iembotHide) {
                        this.ownerCt.setIconCls("new-tab");
                        msgBus.fire("soundevent", "iembot");
                    }
                }
            },
            this,
        );
        this.store.on(
            "datachanged",
            function () {
                if (this.stickToBottom) {
                    this.queueScrollToLatest();
                }
            },
            this,
        );
        const iconfig = {
            selModel: {
                type: "rowmodel",
                listeners: {
                    select (_sm, record) {
                        if (record.data.product_id) {
                            loadTextProductInWindow(record.data.product_id);
                        }
                    },
                },
            },
            listeners: {
                render (p) {
                    const view = p.getView();
                    p.createJumpToLatestBadge();
                    p.updateJumpToLatestButton();
                    p.queueScrollToLatest();
                    view.on(
                        "refresh",
                        function () {
                            p.flushPendingAutoScroll();
                        },
                        p,
                    );
                    const scrollEl = p.getScrollElement();
                    if (scrollEl) {
                        Ext.get(scrollEl).on("scroll", function () {
                            p.updateBottomState();
                        });
                    }
                    const viewEl = view.getEl();
                    if (p.ownerCt && p.ownerCt.on) {
                        p.ownerCt.on(
                            "activate",
                            function () {
                                p.flushPendingAutoScroll();
                            },
                            p,
                        );
                    }
                    if (viewEl) {
                        viewEl.on("mousedown", function (e, t) {
                            if (t.tagName === "A") {
                                e.stopEvent();
                                t.target = "_blank";
                                t.rel = "noopener noreferrer";
                            }
                        });
                    }
                    p.body.on({
                        click (e, t) {
                            const lat = parseFloat(t.getAttribute("data-lat"));
                            const lon = parseFloat(t.getAttribute("data-long"));
                            if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
                                e.stopEvent();
                                return;
                            }

                            const mapPanel = Ext.getCmp("map");
                            const map = mapPanel && mapPanel.map ? mapPanel.map : getMap();
                            if (map && map.getView) {
                                const mapView = map.getView();
                                const center = fromLonLat([lon, lat]);
                                const currentZoom = mapView.getZoom ? mapView.getZoom() : 5;
                                const targetZoom = Math.max(currentZoom || 5, 8);
                                if (mapView.animate) {
                                    mapView.animate({
                                        center,
                                        zoom: targetZoom,
                                        duration: 250,
                                    });
                                } else {
                                    mapView.setCenter(center);
                                    if (mapView.setZoom) {
                                        mapView.setZoom(targetZoom);
                                    }
                                }
                            }
                            e.stopEvent();
                            hideTextWindow();
                        },
                        delegate: ".chat-map-zoom-btn",
                    });
                    p.body.on({
                        mousedown (e, t) {
                            // try to
                            // intercept the
                            // easy
                            // way
                            //console.log("Daryl2:"+ t.tagName);
                            t.target = "_blank";
                            t.rel = "noopener noreferrer";
                            hideTextWindow();
                        },
                        click (e, t) {
                            if (String(t.target).toLowerCase() !== "_blank") {
                                e.stopEvent();
                                open(
                                    t.href,
                                    "_blank",
                                    "noopener,noreferrer",
                                );
                            }
                            hideTextWindow();
                        },
                        delegate: "a",
                    });
                },
            },
        };
        Ext.apply(this, iconfig);

        ChatGridPanel.superclass.initComponent.apply(this, arguments);

        /*
         * this.view = new Ext.grid.GridView({ cellTpl: new Ext.Template( '<td class="x-grid3-col x-grid3-cell x-grid3-td-{id} {css}" style="{style}" tabIndex="0" {cellAttr}>', '<div
         * class="x-grid3-cell-inner x-grid3-col-{id}" unselectable="on"
         * {attr}>{value}</div>', '</td>' ) });
         */
    },
});

/*
 * Handles all of the ROSTER related activities
 */

/*
 * Handle presence from a buddy. This gets into complicated UI land, but we'll
 * try to handle it all
 */
function onBuddyPresence(msg) {
    // Okay, we know who we got the presence from
    const jid = msg.getAttribute("from");
    const barejid = Strophe.getBareJidFromJid(jid);
    const resource = Strophe.getResourceFromJid(jid);
    const username = Strophe.getNodeFromJid(jid);
    let status = "Available";
    /* IMPORTANT: If we find our own username, then we need to set a global
     * flag to prevent auto-login from working
     */
    if (
        username === Application.USERNAME &&
        resource !== Application.XMPPRESOURCE &&
        resource.match(/^WeatherIM/)
    ) {
        if (msg.getAttribute("type") === "unavailable") {
            Application.log(
                "Self presence: [" +
                    username +
                    "] [" +
                    resource +
                    "] unavailable",
            );
        } else {
            Application.log(
                "Self presence: [" +
                    username +
                    "] [" +
                    resource +
                    "] available",
            );
        }
    }
    /* Check for status */
    if (msg.getElementsByTagName("status").length > 0) {
        status = msg.getElementsByTagName("status")[0].textContent;
    }
    /* Check for subscription request */
    if (msg.getAttribute("type") === "subscribe") {
        Ext.Msg.show({
            title: "New Buddy Request",
            msg:
                "User " +
                username +
                " wishes to add you as a buddy. Is this okay?",
            buttons: Ext.Msg.YESNO,
            fn (btn) {
                if (btn === "yes") {
                    /* <presence to='fire-daryl.e.herzmann@localhost' type='subscribed'/> */
                    const stanza = $pres({ to: jid, type: "subscribed" });
                    Application.XMPPConn.send(stanza.tree());
                    Application.buildAddBuddy(username, username, "Buddies");
                } else {
                    const stanza = $pres({ to: jid, type: "unsubscribed" });
                    Application.XMPPConn.send(stanza.tree());
                }
            },
            icon: Ext.MessageBox.QUESTION,
        });

        return;
    }

    // Go look for our barejid
    const buddiesTree = Ext.getCmp("buddies");
    if (buddiesTree && buddiesTree.getRootNode()) {
        buddiesTree.getRootNode().eachChild(function (node) {
            node.eachChild(function (leaf) {
                if (leaf.data.barejid === barejid) {
                    const res = leaf.data.resources.get(resource);
                    if (!res) {
                        leaf.data.resources.add(resource, {
                            status,
                            resource,
                        });
                    }
                    if (!msg.getAttribute("type")) {
                        if (msg.getElementsByTagName("show").length > 0) {
                            leaf.set("iconCls", "buddy-away");
                        } else {
                            leaf.set("iconCls", "buddy-online");
                        }
                        leaf.data.presence = "online";
                        leaf.set("hidden", false);
                        //console.log("Replace"+ resource +" Status:"+ status);
                        leaf.data.resources.replace(resource, {
                            status,
                            resource,
                        });
                    } else if (msg.getAttribute("type") === "unavailable") {
                        if (leaf.data.resources.length === 1) {
                            leaf.data.presence = "offline";
                            leaf.set("iconCls", "buddy-offline");
                            leaf.set("hidden", true);
                        }
                        leaf.data.resources.remove(resource);
                    }
                }
            });
        });
    }
}

export { onBuddyPresence };

Application.ChatTabPanel = Ext.extend(Ext.TabPanel, {
    activeTab: 0,
    deferredRender: false,
    split: true,
    enableTabScroll: true,
    initComponent () {
        const iconfig = {};

        Ext.apply(this, Ext.apply(this.initialConfig, iconfig));

        Application.ChatTabPanel.superclass.initComponent.apply(
            this,
            arguments,
        );
        this.buildItems();
    },
    moveActiveTabBy (delta, sourceBtn) {
        if (!delta) {
            return;
        }

        if (sourceBtn) {
            if (typeof sourceBtn.blur === "function") {
                sourceBtn.blur();
            }
            if (
                sourceBtn.el &&
                sourceBtn.el.dom &&
                typeof sourceBtn.el.dom.blur === "function"
            ) {
                sourceBtn.el.dom.blur();
            }
        }

        const activeTab = this.getActiveTab ? this.getActiveTab() : null;
        if (!activeTab || activeTab.closable === false) {
            return;
        }

        const total = this.items && this.items.getCount ? this.items.getCount() : 0;
        if (total < 2) {
            return;
        }

        const currentIndex = this.items.indexOf(activeTab);
        if (currentIndex < 0) {
            return;
        }

        const targetIndex = currentIndex + delta;
        if (targetIndex < 0 || targetIndex >= total) {
            return;
        }

        const parkingTab = this.items.getAt(targetIndex);
        if (parkingTab && parkingTab !== activeTab) {
            try {
                this.setActiveTab(parkingTab);
            } catch {
                // Keep moving even if focus park fails
            }
        }

        Ext.defer(() => {
            if (this.destroyed || activeTab.destroyed) {
                return;
            }

            const movedTab = this.remove(activeTab, false);
            if (!movedTab) {
                return;
            }
            this.insert(targetIndex, movedTab);

            Ext.defer(() => {
                if (!this.destroyed && movedTab && !movedTab.destroyed) {
                    this.setActiveTab(movedTab);
                }
            }, 10);
        }, 1);
    },
    buildItems () {
        const helpElement = document.getElementById("help");
        const helpHtml = helpElement ? helpElement.innerHTML : "";
        this.add({
            html: helpHtml,
            title: "Help",
            style: {
                margin: "5px",
            },
            autoScroll: true,
        });
        this.add(
            new MUCChatPanel({
                title: "All Chats",
                closable: false,
                chatType: "allchats",
                barejid: `__allchats__@${LiveConfig.XMPPMUCHOST}`,
                id: "__allchats__",
            }),
        );
    },
    addMUC (barejid, handle, anonymous) {
        const mcp = new MUCChatPanel({
            title: Strophe.getNodeFromJid(barejid),
            barejid,
            handle,
            anonymous,
        });
        return this.add(mcp);
    },
    getMUC (barejid) {
        // In ExtJS 6, use query to find components by property
        const items = this.query("[barejid=" + barejid + "]");
        return items.length > 0 ? items[0] : null;
    },
    removeMUC (barejid) {
        const mcp = this.getMUC(barejid);
        if (!mcp || mcp.destroyed) {
            return;
        }
        if (mcp.closable === false) {
            return;
        }
        // Defer removal to next event loop to avoid ExtJS DOM race conditions
        Ext.defer(() => {
            if (mcp.close && !mcp.destroyed) {
                mcp.close();
            } else if (!mcp.destroyed) {
                this.remove(mcp, true);
            }
        }, 1);
    },
    addChat (jid) {
        let title = Strophe.getNodeFromJid(jid);
        if (Strophe.getDomainFromJid(jid) === LiveConfig.XMPPMUCHOST) {
            title = Strophe.getResourceFromJid(jid);
        }
        const cp = new ChatPanel({
            title,
            handle: Application.USERNAME,
            barejid: jid,
        });
        return this.add(cp);
    },
    getChat (jid) {
        // In ExtJS 6, use query to find components by property
        const items = this.query("[barejid=" + jid + "]");
        return items.length > 0 ? items[0] : null;
    },
    removeChat (jid) {
        const cp = this.getChat(jid);
        if (!cp || cp.destroyed) {
            return;
        }
        // Defer removal to next event loop to avoid ExtJS DOM race conditions
        Ext.defer(() => {
            if (cp.close && !cp.destroyed) {
                cp.close();
            } else if (!cp.destroyed) {
                this.remove(cp, true);
            }
        }, 1);
    },
});
