/**
 * @name MiniChat
 * @description Pop out any chat into a small Always on Top window.
 * @version 0.8.8
 * @invite xp2f3YFKMY
 * @author Snues
 * @authorId 98862725609816064
 * @source https://github.com/Snusene/BetterDiscordPlugins/tree/main/MiniChat
 * @donate https://ko-fi.com/snues
 */

const { React } = BdApi;
const h = React.createElement;

const STYLES = `
#mc-root {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.mc-popout {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  width: 100%;
  overflow: hidden;
}
.mc-popout > * {
  min-width: 0;
  width: 100%;
}
.mc-popout [class*="upperContainer"] {
  -webkit-app-region: drag;
}
.mc-popout [class*="upperContainer"] * {
  -webkit-app-region: no-drag;
}
.mc-popout [class*="upperContainer"] [class*="children"] {
  -webkit-app-region: drag;
}
.mc-popout [class*="upperContainer"] [class*="titleWrapper"] {
  -webkit-app-region: no-drag;
}
.mc-popout [aria-label="Close"],
.mc-popout [aria-label="Minimize"],
.mc-popout [aria-label="Maximize"] {
  -webkit-app-region: no-drag !important;
  pointer-events: auto !important;
}
.mc-popout [class*="toolbar"] [class*="iconWrapper"] {
  width: 28px;
  height: 28px;
}
.mc-popout [class*="toolbar"] [class*="iconWrapper"] svg {
  width: 18px;
  height: 18px;
}
.mc-icon {
  width: var(--space-24);
  height: var(--space-24);
  border-radius: 7px;
  margin-right: var(--space-8);
}
.mc-popout [class*="upperContainer"] [class*="topic__"],
.mc-popout [class*="upperContainer"] [class*="dot__"] {
  display: none !important;
}
.mc-popout form[class*="form_"] {
  zoom: var(--mc-zoom, 1);
}
`;

const svgIcon =
  (...children) =>
  (p) =>
    h(
      "svg",
      {
        width: p.width || 24,
        height: p.height || 24,
        viewBox: "0 0 24 24",
        fill: "currentColor",
        className: p.className,
      },
      ...children,
    );

const MiniIcon = svgIcon(
  h("path", {
    d: "M15 2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 1 1-2 0V4.41l-4.3 4.3a1 1 0 1 1-1.4-1.42L19.58 3H16a1 1 0 0 1-1-1Z",
  }),
  h("path", {
    d: "M5 2a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-6a1 1 0 1 0-2 0v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h6a1 1 0 1 0 0-2H5Z",
  }),
);

const CloseIcon = svgIcon(
  h("path", {
    d: "M17.3 18.7a1 1 0 0 0 1.4-1.4L13.42 12l5.3-5.3a1 1 0 0 0-1.42-1.4L12 10.58l-5.3-5.3a1 1 0 0 0-1.4 1.42L10.58 12l-5.3 5.3a1 1 0 1 0 1.42 1.4L12 13.42l5.3 5.3Z",
  }),
);

const MinIcon = svgIcon(h("path", { d: "M5 11h14v2H5z" }));

const MaxIcon = svgIcon(h("path", { d: "M5 5v14h14V5H5zm12 12H7V7h10v10z" }));

const NON_CHAT = [15, 16];
const mainDoc = document;
let suspendFake = false;
const popupCtx = React.createContext(false);
const headerLike = (m) =>
  typeof m === "function" && m.Icon && m.Title && m.Divider && m.Caret;

class Boundary extends React.Component {
  state = { e: 0 };
  static getDerivedStateFromError() {
    return { e: 1 };
  }
  componentDidCatch() {
    setTimeout(() => this.setState({ e: 0 }), 200);
  }
  render() {
    return this.state.e ? h("div", { id: "mc-root" }) : this.props.children;
  }
}

function syncBdStyles(doc) {
  doc.querySelectorAll("[data-mc-synced]").forEach((el) => el.remove());
  mainDoc.querySelectorAll("bd-head style").forEach((el) => {
    doc.head.appendChild(el.cloneNode(true)).setAttribute("data-mc-synced", "");
    try {
      if (!el.sheet) return;
      for (const rule of el.sheet.cssRules) {
        if (rule.type === CSSRule.IMPORT_RULE && rule.href) {
          const link = doc.createElement("link");
          link.rel = "stylesheet";
          link.href = rule.href;
          link.setAttribute("data-mc-synced", "");
          doc.head.appendChild(link);
        }
      }
    } catch (e) {}
  });
}

function getTheme() {
  const mainBg = mainDoc.querySelector('[class*="bg__"]');
  if (!mainBg) return null;
  const bgParent = mainBg.parentElement;
  const layers = bgParent?.querySelector('[class*="layers"]');
  return {
    app: bgParent?.className || "",
    bg: mainBg.className,
    layers: layers?.className || "",
  };
}

function Popout({ modules, channelId, headerNode, guildIconUrl, onClose }) {
  const {
    Header,
    Bar,
    SplitView,
    Native,
    ChatInputTypes,
    ChannelStore,
    GuildStore,
  } = modules;
  const ref = React.useRef(null);
  const [tc, setTc] = React.useState(getTheme);

  React.useEffect(() => {
    const doc = ref.current?.ownerDocument;
    if (!doc || doc === mainDoc || doc.querySelector("#mc-popout-style"))
      return;

    const mainMount = mainDoc.getElementById("app-mount");
    const popMount = doc.getElementById("app-mount");
    const root = doc.getElementById("mc-root");
    const axStore = BdApi.Webpack.Stores.AccessibilityStore;

    const sync = () => {
      syncBdStyles(doc);
      doc.documentElement.className = mainDoc.documentElement.className;
      doc.documentElement.style.cssText = mainDoc.documentElement.style.cssText;
      if (mainMount && popMount) popMount.className = mainMount.className;
      if (root && axStore) {
        const z = axStore.zoom / 100;
        root.style.zoom = z;
        root.style.setProperty("--mc-zoom", String(1 / z));
      }
      setTc(getTheme());
    };
    sync();

    const s = doc.createElement("style");
    s.id = "mc-popout-style";
    s.textContent = STYLES;
    doc.head.appendChild(s);

    let timer = null;
    const debounced = () => {
      clearTimeout(timer);
      timer = setTimeout(sync, 100);
    };
    const observer = new MutationObserver(debounced);
    const bdHead = mainDoc.querySelector("bd-head");
    if (bdHead)
      observer.observe(bdHead, {
        childList: true,
        subtree: true,
        characterData: true,
      });
    observer.observe(mainDoc.documentElement, {
      attributes: true,
      attributeFilter: ["style", "class"],
    });
    if (axStore) axStore.addChangeListener(debounced);

    return () => {
      clearTimeout(timer);
      observer.disconnect();
      if (axStore) axStore.removeChangeListener(debounced);
    };
  }, []);

  const wk = "DISCORD_MC_" + channelId;
  const ch = ChannelStore.getChannel(channelId);
  const guild = ch?.guild_id ? GuildStore.getGuild(ch.guild_id) : null;
  const chatInputType = ChatInputTypes?.SIDEBAR;
  const winBtn = (icon, label, onClick) =>
    h(Bar.Icon, { icon, onClick, tooltip: label, "aria-label": label });

  return h(
    popupCtx.Provider,
    { value: true },
    h(
      Boundary,
      null,
      h(
        "div",
        { id: "mc-root", className: tc?.app || "", ref },
        tc ? h("div", { className: tc.bg }) : null,
        h(
          "div",
          { className: (tc?.layers || "") + " mc-popout" },
          ch && Header
            ? h(Header, {
                guildId: ch.guild_id,
                channelId: ch.id,
                channelType: ch.type,
                hideSearch: true,
                toolbar: h(
                  React.Fragment,
                  null,
                  winBtn(MinIcon, "Minimize", () => Native?.minimize(wk)),
                  winBtn(MaxIcon, "Maximize", () => Native?.maximize(wk)),
                  winBtn(CloseIcon, "Close", onClose),
                ),
                children: h(
                  React.Fragment,
                  null,
                  guildIconUrl
                    ? h("img", {
                        className: "mc-icon",
                        src: guildIconUrl,
                        alt: "",
                      })
                    : null,
                  h("div", {
                    style: { display: "contents" },
                    ref: (el) =>
                      el && headerNode && el.replaceChildren(headerNode),
                  }),
                ),
              })
            : null,
          ch && SplitView
            ? h(SplitView, { channel: ch, guild, chatInputType })
            : null,
        ),
      ),
    ),
  );
}

module.exports = class MiniChat {
  constructor(meta) {
    this.api = new BdApi(meta.name);
    this.modules = null;
    this.popouts = new Map();
  }

  start() {
    this.modules = MiniChat.getModules();
    this.settings = { alwaysOnTop: true, ...this.api.Data.load("settings") };
    this.patchInput();
    this.patchSplitView();
    this.patchToolbar();
    this.startAck();
  }

  stop() {
    this.restoreInput();
    for (const cid of [...this.popouts.keys()]) this.close(cid);
    this._vcAbort?.abort();
    this.stopAck();
    this.api.Patcher.unpatchAll();
    this.modules = this.settings = null;
  }

  startAck() {
    const { AckActions } = this.modules;
    if (!AckActions) return;
    this._dispatcher = BdApi.Webpack.Stores.UserStore._dispatcher;
    this._onMessage = (e) => {
      if (!this.popouts.has(e.channelId)) return;
      AckActions.ack(e.channelId, undefined, true, true, e.message?.id);
      this._dispatcher.dispatch({
        type: "CHANNEL_LOCAL_ACK",
        channelId: e.channelId,
      });
    };
    this._dispatcher.subscribe("MESSAGE_CREATE", this._onMessage);
  }

  stopAck() {
    this._dispatcher?.unsubscribe("MESSAGE_CREATE", this._onMessage);
    this._dispatcher = this._onMessage = null;
  }

  patchInput() {
    const { ChatInputTypes } = this.modules;
    if (!ChatInputTypes?.SIDEBAR) return;
    const sb = ChatInputTypes.SIDEBAR;
    this._origInput = {
      gifs: { ...sb.gifs },
      stickers: { ...sb.stickers },
    };
    sb.gifs.button = sb.stickers.button = sb.stickers.autoSuggest = true;
  }

  restoreInput() {
    const { ChatInputTypes } = this.modules || {};
    if (!ChatInputTypes?.SIDEBAR || !this._origInput) return;
    const sb = ChatInputTypes.SIDEBAR;
    Object.assign(sb.gifs, this._origInput.gifs);
    Object.assign(sb.stickers, this._origInput.stickers);
    this._origInput = null;
  }

  patchSplitView() {
    const { GuildStore } = this.modules;
    if (!GuildStore) return;
    const defaults = {
      name: "DM",
      roles: {},
      emojis: [],
      stickers: [],
      features: new Set(),
    };
    const fake = new Proxy(defaults, { get: (t, p) => (p in t ? t[p] : null) });
    this.api.Patcher.instead(GuildStore, "getGuild", (_, [id], original) => {
      const ret = original(id);
      if (ret == null && id === null && this.popouts.size && !suspendFake)
        return fake;
      return ret;
    });
    const [threadMod, threadKey] = BdApi.Webpack.getWithKey(
      BdApi.Webpack.Filters.byStrings("Thread must have a parent ID"),
    );
    if (threadMod) {
      this.api.Patcher.instead(
        threadMod,
        threadKey,
        (_, [channel, opts], original) => {
          if (channel?.parent_id == null) return;
          return original(channel, opts);
        },
      );
    }
    const dispatcher = BdApi.Webpack.Stores.UserStore._dispatcher;
    this.api.Patcher.before(dispatcher, "dispatch", (_, [event]) => {
      if (event?.type === "CONNECTION_OPEN") {
        suspendFake = true;
        queueMicrotask(() => {
          suspendFake = false;
        });
      }
    });
    const { UserGuildSettingsStore: ugss } = this.modules;
    if (ugss) {
      for (const [fn, fb] of [
        ["getChannelOverrides", {}],
        ["getMessageNotifications", 0],
        ["isMuted", false],
        ["getGuildFlags", 0],
      ]) {
        this.api.Patcher.instead(ugss, fn, (_, a, orig) => {
          try {
            return orig(...a);
          } catch {
            return fb;
          }
        });
      }
    }
  }

  static getModules() {
    const {
      Webpack,
      Webpack: { Filters },
    } = BdApi;
    return {
      ...Webpack.getBulkKeyed({
        SplitView: {
          filter: (e) => {
            if (e?.$$typeof?.toString() !== "Symbol(react.memo)") return false;
            const s = e.type?.toString?.() || "";
            return (
              s.includes("chatInputType") && s.includes("filterAfterTimestamp")
            );
          },
        },
        Header: {
          filter: (m) =>
            headerLike(m) && m.toString().includes("isAuthenticated"),
        },
        Bar: {
          filter: (m) =>
            headerLike(m) && !m.toString().includes("isAuthenticated"),
        },
        ChatInputTypes: {
          filter: Filters.byKeys("FORM", "SIDEBAR"),
          searchExports: true,
        },
        PopoutActions: {
          filter: Filters.byKeys("open", "close", "setAlwaysOnTop"),
        },
        AckActions: {
          filter: Filters.byKeys("ack"),
        },
        PopoutWindow: {
          filter: (e) => {
            const s = (e?.render ?? e)?.toString?.() ?? "";
            return s.includes("guestWindow") && s.includes("windowKey");
          },
          searchExports: true,
        },
        IconUtils: {
          filter: Filters.byKeys("getGuildIconURL"),
        },
        Native: {
          filter: (m) =>
            m?.setAlwaysOnTop
              ?.toString?.()
              ?.includes?.("window.setAlwaysOnTop"),
        },
      }),
      PopoutWindowStore: Webpack.getStore("PopoutWindowStore"),
      UserGuildSettingsStore: Webpack.getStore("UserGuildSettingsStore"),
      GuildStore: Webpack.Stores.GuildStore,
      ChannelStore: Webpack.Stores.ChannelStore,
    };
  }

  getSettingsPanel() {
    return BdApi.UI.buildSettingsPanel({
      settings: [
        {
          type: "switch",
          id: "alwaysOnTop",
          name: "Always On Top",
          note: "Keep popout windows above all others",
          value: this.settings.alwaysOnTop,
        },
      ],
      onChange: (_, id, val) => {
        this.settings[id] = val;
        this.api.Data.save("settings", this.settings);
      },
    });
  }

  guildIcon(channelId) {
    const { ChannelStore, GuildStore, IconUtils } = this.modules;
    const gid = ChannelStore.getChannel(channelId)?.getGuildId();
    const guild = gid && GuildStore.getGuild(gid);
    if (!guild?.icon || !IconUtils) return null;
    return IconUtils.getGuildIconURL({
      id: guild.id,
      icon: guild.icon,
      size: 48,
    });
  }

  patchToolbar() {
    const [mod, key] = BdApi.Webpack.getWithKey(
      (m) => headerLike(m) && m.toString().includes("isAuthenticated"),
    );
    if (!mod) return;
    const Bar = mod[key];
    this.api.Patcher.before(mod, key, (_, [props]) => {
      if (popupCtx._currentValue) return;
      if (!props?.toolbar) return;
      const ch = this.modules.ChannelStore.getChannel(props.channelId);
      if (!ch || NON_CHAT.includes(ch.type)) return;
      props.toolbar = h(
        React.Fragment,
        null,
        h(Bar.Icon, {
          icon: MiniIcon,
          onClick: () => this.open(ch.id),
          tooltip: "MiniChat",
          "aria-label": "MiniChat",
        }),
        props.toolbar,
      );
    });
    const sidebar = BdApi.Webpack.Filters.byStrings("CallChatSidebar");
    this._vcAbort = new AbortController();
    BdApi.Webpack.waitForModule(sidebar, {
      searchExports: true, raw: true, signal: this._vcAbort.signal,
    }).then((raw) => {
      const vc = raw?.exports;
      const vcKey = vc && Object.keys(vc).find((k) => sidebar(vc[k]));
      if (!vcKey) return;
      this.api.Patcher.after(vc, vcKey, (_, [props], ret) => {
        const bar = BdApi.Utils.findInTree(ret, (n) => n?.toolbar, {
          walkable: ["props", "children"],
        });
        if (!bar) return;
        const icon = h(Bar.Icon, {
          icon: MiniIcon, tooltip: "MiniChat", "aria-label": "MiniChat",
          onClick: () => this.open(props.channel.id),
        });
        bar.toolbar = h(React.Fragment, null, icon, bar.toolbar);
      });
    });
    const header = mainDoc.querySelector('[class*="upperContainer__"]');
    const inst = header && BdApi.ReactUtils.getOwnerInstance(header);
    if (inst) inst.forceUpdate();
  }

  close(channelId) {
    const wk = this.popouts.get(channelId);
    if (!wk) return;
    this.modules.PopoutActions.close(wk);
    this.popouts.delete(channelId);
  }

  ensure() {
    if (this.modules.SplitView && this.modules.PopoutWindow) return;
    const fresh = MiniChat.getModules();
    for (const k in fresh) this.modules[k] ??= fresh[k];
  }

  open(channelId) {
    this.ensure();
    const { PopoutActions, PopoutWindow, PopoutWindowStore, ChannelStore } =
      this.modules;
    const wk = "DISCORD_MC_" + channelId;
    if (PopoutWindowStore.getWindowOpen(wk)) {
      this.close(channelId);
      return;
    }
    this.popouts.delete(channelId);
    if (!PopoutWindow) return;

    const channel = ChannelStore.getChannel(channelId);
    const name = channel ? channel.name || "DM" : "Chat";
    const headerNode = mainDoc
      .querySelector('[class*="upperContainer__"] [class*="children__"]')
      ?.cloneNode(true);
    const guildIconUrl = this.guildIcon(channelId);
    const onClose = () => this.close(channelId);
    PopoutActions.open(
      wk,
      () =>
        h(
          PopoutWindow,
          { windowKey: wk, withTitleBar: false, title: name, channelId },
          h(Popout, {
            modules: this.modules,
            channelId,
            headerNode,
            guildIconUrl,
            onClose,
          }),
        ),
      { width: 500, height: 450 },
    );
    this.popouts.set(channelId, wk);

    let tries = 0;
    const setup = () => {
      if (!this.popouts.has(channelId) || tries++ > 20) return;
      const w = PopoutWindowStore.getWindow(wk);
      if (!w) return setTimeout(setup, 100);
      w.resizeTo(500, 450);
      if (this.settings?.alwaysOnTop)
        this.modules.Native?.setAlwaysOnTop(wk, true);
    };
    setTimeout(setup, 300);
  }
};
