/*
 * Vencord, a modification for Discord's desktop app
 * Copyright (c) 2023 Vendicated and contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

import { Devs } from "@utils/constants";
import definePlugin from "@utils/types";
import { Channel } from "discord-types/general";

import { addContextMenus, removeContextMenus } from "./contextMenus";
import { getPinAt, isPinned, settings, snapshotArray, sortedSnapshot, usePinnedDms } from "./settings";

export default definePlugin({
    name: "PinDMs",
    description: "Allows you to pin private channels to the top of your DM list. To pin/unpin or reorder pins, right click DMs",
    authors: [Devs.Ven, Devs.Strencher],

    settings,

    start: addContextMenus,
    stop: removeContextMenus,

    usePinCount(channelIds: string[]) {
        const pinnedDms = usePinnedDms();
        // See comment on 2nd patch for reasoning
        return channelIds.length ? [pinnedDms.size] : [];
    },

    getChannel(channels: Record<string, Channel>, idx: number) {
        return channels[getPinAt(idx)];
    },

    isPinned,
    getSnapshot: sortedSnapshot,

    getScrollOffset(channelId: string, rowHeight: number, padding: number, preRenderedChildren: number, originalOffset: number) {
        if (!isPinned(channelId))
            return (
                (rowHeight + padding) * 2 // header
                + rowHeight * snapshotArray.length // pins
                + originalOffset // original pin offset minus pins
            );

        return rowHeight * (snapshotArray.indexOf(channelId) + preRenderedChildren) + padding;
    },

    patches: [
        // Patch DM list
        {
            find: ".privateChannelsHeaderContainer,",
            replacement: [
                {
                    // filter Discord's privateChannelIds list to remove pins, and pass
                    // pinCount as prop. This needs to be here so that the entire DM list receives
                    // updates on pin/unpin
                    match: /(?<=\i,{channels:\i,)privateChannelIds:(\i),/,
                    replace: "privateChannelIds:$1.filter(c=>!$self.isPinned(c)),pinCount:$self.usePinCount($1),"
                },
                {
                    // sections is an array of numbers, where each element is a section and
                    // the number is the amount of rows. Add our pinCount in second place
                    // - Section 1: buttons for pages like Friends & Library
                    // - Section 2: our pinned dms
                    // - Section 3: the normal dm list
                    match: /(?<=renderRow:this\.renderRow,)sections:\[\i,/,
                    // For some reason, adding our sections when no private channels are ready yet
                    // makes DMs infinitely load. Thus usePinCount returns either a single element
                    // array with the count, or an empty array. Due to spreading, only in the former
                    // case will an element be added to the outer array
                    // Thanks for the fix, Strencher!
                    replace: "$&...this.props.pinCount??[],"
                },
                {
                    // Patch renderSection (renders the header) to set the text to "Pinned DMs" instead of "Direct Messages"
                    // lookbehind is used to lookup parameter name. We could use arguments[0], but
                    // if children ever is wrapped in an iife, it will break
                    match: /children:(\i\.\i\.Messages.DIRECT_MESSAGES)(?<=renderSection=(\i)=>{.+?)/,
                    replace: "children:$2.section===1?'Pinned DMs':$1"
                },
                {
                    // Patch channel lookup inside renderDM
                    // channel=channels[channelIds[row]];
                    match: /(?<=renderDM=\((\i),(\i)\)=>{.*?this.state,\i=\i\[\i\],\i=)((\i)\[\i\]);/,
                    // section 1 is us, manually get our own channel
                    // section === 1 ? getChannel(channels, row) : channels[channelIds[row]];
                    replace: "$1===1?$self.getChannel($4,$2):$3;"
                },
                {
                    // Fix getRowHeight's check for whether this is the DMs section
                    // section === DMS
                    match: /===\i\.DMS&&0/,
                    // section -1 === DMS
                    replace: "-1$&"
                },
                {
                    // Override scrollToChannel to properly account for pinned channels
                    match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
                    replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
                }
            ]
        },

        // Fix Alt Up/Down navigation
        {
            find: ".Routes.APPLICATION_STORE&&",
            replacement: {
                // channelIds = __OVERLAY__ ? stuff : [...getStaticPaths(),...channelIds)]
                match: /(?<=\i=__OVERLAY__\?\i:\[\.\.\.\i\(\),\.\.\.)\i/,
                // ....concat(pins).concat(toArray(channelIds).filter(c => !isPinned(c)))
                replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
            }
        },
        // fix alt+shift+up/down
        {
            find: ".getFlattenedGuildIds()],",
            replacement: {
                match: /(?<=\i===\i\.ME\?)\i\.\i\.getPrivateChannelIds\(\)/,
                replace: "$self.getSnapshot().concat($&.filter(c=>!$self.isPinned(c)))"
            }
        },
    ]
});
