#include msgbox.jsx
#include comments/api.js
#include comments/form.jsx

const MszCommentsEntryVoteButton = function(args) {
    const { name, title, icon, vote } = args ?? {};

    let element, counter;
    const isCast = () => element?.classList.contains('comments-entry-action-vote-cast') === true;

    element = <button class={`comments-entry-action comments-entry-action-vote-${name}`} onclick={() => { vote(isCast()); }}>
        {icon}
    </button>;

    return {
        get element() { return element; },

        get disabled() { return element.disabled; },
        set disabled(state) {
            element.disabled = state;
        },

        get cast() { return isCast(); },
        set cast(state) {
            element.classList.toggle('comments-entry-action-vote-cast', state);
        },

        get count() { return counter ? parseInt(counter.textContent) : 0; },
        set count(count) {
            if(count > 0) {
                if(!counter)
                    element.appendChild(counter = <span />);

                const formatted = count.toLocaleString();
                counter.textContent = formatted;
                element.title = title.replace('$0', formatted).replace('$s', count === 1 ? '' : 's');
            } else {
                if(counter) {
                    element.removeChild(counter);
                    counter = undefined;
                }

                element.title = title.replace('$0', 'No').replace('$s', 's');
            }
        },
    };
};

const MszCommentsEntryVoteActions = function(args) {
    const { vote } = args ?? {};

    const like = new MszCommentsEntryVoteButton({
        name: 'like',
        title: '$0 like$s',
        icon: <i class="fas fa-chevron-up" />,
        vote: cast => { vote(cast ? 0 : 1); }
    });
    const dislike = new MszCommentsEntryVoteButton({
        name: 'dislike',
        title: '$0 dislike$s',
        icon: <i class="fas fa-chevron-down" />,
        vote: cast => { vote(cast ? 0 : -1); }
    });

    const element = <div class="comments-entry-actions-group comments-entry-actions-group-votes">
        {like}
        {dislike}
    </div>;

    return {
        get element() { return element; },

        get disabled() { return element.classList.contains('comments-entry-actions-group-disabled'); },
        set disabled(state) {
            element.classList.toggle('comments-entry-actions-group-disabled', state);
            like.disabled = dislike.disabled = state;
        },

        updateVotes(votes) {
            like.count = votes?.positive ?? 0;
            like.cast = votes?.vote > 0;
            dislike.count = Math.abs(votes?.negative ?? 0);
            dislike.cast = votes?.vote < 0;
        },
    };
};

const MszCommentsEntryReplyToggleButton = function(args) {
    const { replies, toggle } = args ?? {};

    let icon, counter;
    const element = <button class="comments-entry-action" title="No replies" onclick={() => { toggle(); }}>
        {icon = <i class="fas fa-plus" />}
        {counter = <span />}
    </button>;

    const setVisible = state => {
        element.classList.toggle('hidden', !state);
    };
    const setOpen = state => {
        icon.classList.toggle('fa-plus', !state);
        icon.classList.toggle('fa-minus', state);
    };
    const setCount = count => {
        const formatted = count.toLocaleString();
        counter.textContent = formatted;
        element.title = `${count} ${count === 1 ? 'reply' : 'replies'}`;
        setVisible(count > 0);
    };

    setCount(Array.isArray(replies) ? replies.length : (replies ?? 0));

    return {
        get element() { return element; },

        get visible() { return !element.classList.contains('hidden'); },
        set visible(state) { setVisible(state); },

        get count() { return parseInt(counter.textContent); },
        set count(count) { setCount(count); },

        get open() { return element.classList.contains('fa-plus'); },
        set open(state) { setOpen(state); },
    };
};

const MszCommentsEntryReplyCreateButton = function(args) {
    const { toggle } = args ?? {};

    const element = <button class="comments-entry-action" title="Reply" onclick={() => { toggle(); }}>
        <i class="fas fa-reply" />
        <span>Reply</span>
    </button>;

    return {
        get element() { return element; },

        get visible() { return !element.classList.contains('hidden'); },
        set visible(state) { element.classList.toggle('hidden', !state); },

        get active() { return element.classList.contains('comments-entry-action-reply-active'); },
        set active(state) { element.classList.toggle('comments-entry-action-reply-active', state); },

        click() {
            element.click();
        },
    };
};

const MszCommentsEntryReplyActions = function(args) {
    const { replies, toggleReplies, toggleForm } = args ?? {};

    const toggle = new MszCommentsEntryReplyToggleButton({ replies, toggle: toggleReplies });
    const button = toggleForm ? new MszCommentsEntryReplyCreateButton({ toggle: toggleForm }) : undefined;

    const element = <div class="comments-entry-actions-group comments-entry-actions-group-replies">
        {toggle}
        {button}
    </div>;

    const setVisible = state => {
        element.classList.toggle('hidden', !state);
    };

    return {
        get element() { return element; },

        get visible() { return !element.classList.contains('hidden'); },
        set visible(state) { setVisible(state); },

        get toggle() { return toggle; },
        get button() { return button; },

        updateVisible() {
            setVisible(toggle.visible || button?.visible === true);
        },
    };
};

const MszCommentsEntryGeneralButton = function(args) {
    const { icon, title, action } = args ?? {};
    const element = <button class="comments-entry-action" title={title} onclick={() => { action(); }}>{icon}</button>;

    return {
        get element() { return element; },

        get visible() { return !element.classList.contains('hidden'); },
        set visible(state) { element.classList.toggle('hidden', !state); },

        get disabled() { return element.disabled; },
        set disabled(state) { element.disabled = state; },
    };
};

const MszCommentsEntryGeneralActions = function(args) {
    let deleteButton, restoreButton, nukeButton, pinButton, unpinButton;
    const element = <div class="comments-entry-actions-group">
        {deleteButton = args.delete ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-trash" />, title: 'Delete', action: args.delete }) : null}
        {restoreButton = args.restore ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-trash-restore" />, title: 'Restore', action: args.restore }) : null}
        {nukeButton = args.nuke ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-radiation-alt" />, title: 'Permanently delete', action: args.nuke }) : null}
        {pinButton = args.pin ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-thumbtack" />, title: 'Pin', action: args.pin }) : null}
        {unpinButton = args.unpin ? new MszCommentsEntryGeneralButton({ icon: <i class="fas fa-screwdriver" />, title: 'Unpin', action: args.unpin }) : null}
    </div>;

    return {
        get element() { return element; },

        get visible() { return !element.classList.contains('hidden'); },
        set visible(state) { element.classList.toggle('hidden', !state); },

        get disabled() { return element.classList.contains('comments-entry-actions-group-disabled'); },
        set disabled(state) {
            element.classList.toggle('comments-entry-actions-group-disabled', state);
            if(deleteButton)
                deleteButton.disabled = state;
            if(restoreButton)
                restoreButton.disabled = state;
            if(nukeButton)
                nukeButton.disabled = state;
            if(pinButton)
                pinButton.disabled = state;
            if(unpinButton)
                unpinButton.disabled = state;
        },

        get deleteButton() { return deleteButton; },
        get restoreButton() { return restoreButton; },
        get nukeButton() { return nukeButton; },

        get deleteVisible() { return deleteButton?.visible === true; },
        set deleteVisible(state) {
            if(deleteButton)
                deleteButton.visible = state;
            if(restoreButton)
                restoreButton.visible = !state;
            if(nukeButton)
                nukeButton.visible = !state;
        },

        get pinButton() { return pinButton; },
        get unpinButton() { return unpinButton; },

        get pinVisible() { return pinButton?.visible === true; },
        set pinVisible(state) {
            if(pinButton)
                pinButton.visible = state;
            if(unpinButton)
                unpinButton.visible = !state;
        },
    };
};

const MszCommentsEntryActions = function() {
    const element = <div class="comments-entry-actions hidden" />;

    const hideIfNoChildren = () => {
        element.classList.toggle('hidden', element.childElementCount < 1);
    };
    hideIfNoChildren();

    return {
        get element() { return element; },

        appendGroup(group) {
            element.appendChild(group.element);
            hideIfNoChildren();
        },
    };
};

const MszCommentsEntry = function(catInfo, userInfo, postInfo, listing, root) {
    const actions = new MszCommentsEntryActions;

    const voteActions = new MszCommentsEntryVoteActions({
        vote: async vote => {
            if(voteActions.disabled)
                return;

            voteActions.disabled = true;
            try {
                voteActions.updateVotes(vote === 0
                    ? await MszCommentsApi.deleteVote(postInfo.id)
                    : await MszCommentsApi.createVote(postInfo.id, vote));
            } catch(ex) {
                console.error(ex);
            } finally {
                enableVoteActionsMaybe();
            }
        }
    });
    actions.appendGroup(voteActions);

    const enableVoteActionsMaybe = () => {
        voteActions.disabled = !userInfo.can_vote || !!postInfo.deleted || !!catInfo.locked;
    };

    enableVoteActionsMaybe();
    voteActions.updateVotes(postInfo);

    const repliesIsArray = Array.isArray(postInfo.replies);
    const replies = new MszCommentsListing({ hidden: !repliesIsArray });
    if(repliesIsArray)
        replies.addPosts(catInfo, userInfo, postInfo.replies);

    const repliesElem = <div class="comments-entry-replies">
        {replies}
    </div>;

    let replyForm;
    let repliesLoaded = replies.loaded;
    const replyActions = new MszCommentsEntryReplyActions({
        replies: postInfo.replies,
        toggleReplies: async () => {
            replyActions.toggle.open = replies.visible = !replies.visible;
            if(!repliesLoaded) {
                repliesLoaded = true;
                try {
                    replies.addPosts(catInfo, userInfo, await MszCommentsApi.getPostReplies(postInfo.id));
                } catch(ex) {
                    console.error(ex);
                    replyActions.toggle.open = false;
                    repliesLoaded = false;
                }
            }
        },
        toggleForm: userInfo.can_create ? () => {
            if(replyForm) {
                replyActions.button.active = false;
                repliesElem.removeChild(replyForm.element);
                replyForm = null;
            } else {
                replyActions.button.active = true;
                replyForm = new MszCommentsForm({
                    userInfo, catInfo, postInfo,
                    listing: replies,
                    repliesToggle: replyActions.toggle,
                    replyToggle: replyActions.button,
                });
                $insertBefore(replies.element, replyForm.element);
                replyForm.focus();
            }
        } : null,
    });
    actions.appendGroup(replyActions);

    const enableReplyButtonMaybe = () => {
        if(replyActions.button)
            replyActions.button.visible = !catInfo.locked && !postInfo.deleted;
        replyActions.updateVisible();
    };

    replyActions.toggle.open = replies.visible;
    enableReplyButtonMaybe();

    const generalActions = new MszCommentsEntryGeneralActions({
        delete: postInfo.can_delete ? async () => {
            generalActions.disabled = true;
            try {
                if(!await MszShowConfirmBox(`Are you sure you want to delete comment #${postInfo.id}?`, 'Deleting a comment'))
                    return;

                postInfo.deleted = new Date;
                await MszCommentsApi.deletePost(postInfo.id);
                if(generalActions.restoreButton) {
                    setOptionalTime(deletedElem, new Date, 'commentDeleted');
                    generalActions.deleteVisible = false;
                    enableVoteActionsMaybe();
                    enableReplyButtonMaybe();
                    listing.reorder();
                } else
                    nukeThePost();
            } catch(ex) {
                delete postInfo.deleted;
                console.error(ex);
            } finally {
                generalActions.disabled = false;
            }
        } : null,
        restore: postInfo.can_delete_any ? async () => {
            generalActions.disabled = true;
            const deleted = postInfo.deleted;
            try {
                delete postInfo.deleted;
                await MszCommentsApi.restorePost(postInfo.id);
                setOptionalTime(deletedElem, null, 'commentDeleted');
                generalActions.deleteVisible = true;
                enableVoteActionsMaybe();
                enableReplyButtonMaybe();
                listing.reorder();
            } catch(ex) {
                postInfo.deleted = deleted;
                console.error(ex);
            } finally {
                generalActions.disabled = false;
            }
        } : null,
        nuke: postInfo.can_delete_any ? async () => {
            generalActions.disabled = true;
            try {
                await MszCommentsApi.nukePost(postInfo.id);
                nukeThePost();
            } catch(ex) {
                console.error(ex);
            } finally {
                generalActions.disabled = false;
            }
        } : null,
        pin: root && userInfo.can_pin ? async () => {
            generalActions.disabled = true;
            try {
                if(!await MszShowConfirmBox(`Are you sure you want to pin comment #${postInfo.id}?`, 'Pinning a comment'))
                    return;

                const result = await MszCommentsApi.updatePost(postInfo.id, { pin: 'on' });
                generalActions.pinVisible = !result.pinned;
                setOptionalTime(pinnedElem, result.pinned, 'commentPinned');
                listing.reorder();
            } catch(ex) {
                console.error(ex);
            } finally {
                generalActions.disabled = false;
            }
        } : null,
        unpin: root && userInfo.can_pin ? async () => {
            generalActions.disabled = true;
            try {
                const result = await MszCommentsApi.updatePost(postInfo.id, { pin: '' });
                generalActions.pinVisible = !result.pinned;
                setOptionalTime(pinnedElem, result.pinned, 'commentPinned');
                listing.reorder();
            } catch(ex) {
                console.error(ex);
            } finally {
                generalActions.disabled = false;
            }
        } : null,
    });
    actions.appendGroup(generalActions);

    generalActions.deleteVisible = !postInfo.deleted;
    generalActions.pinVisible = !postInfo.pinned;

    const userAvatarElem = <img alt="" width="40" height="40" class="avatar" />;
    const userNameElem = <div class="comments-entry-user" />;

    const createdTimeElem = <a href={`#comment-${postInfo.id}`} class="comments-entry-time-link" />;
    const editedElem = <div class="comments-entry-time comments-entry-time-edited">
        <div class="comments-entry-time-icon"><i class="fas fa-pencil-alt" /></div>
    </div>;
    const pinnedElem = <div class="comments-entry-time comments-entry-time-pinned">
        <div class="comments-entry-time-icon"><i class="fas fa-thumbtack" /></div>
    </div>;
    const deletedElem = <div class="comments-entry-time comments-entry-time-deleted">
        <div class="comments-entry-time-icon"><i class="fas fa-trash" /></div>
    </div>;

    const bodyElem = <div class="comments-entry-body" />;
    const setBody = body => { bodyElem.textContent = body ?? '[deleted]'; };
    setBody(postInfo?.body);

    const element = <div id={`comment-${postInfo.id}`} data-comment={postInfo.id} class={{ 'comments-entry': true, 'comments-entry-root': root }}>
        <div class="comments-entry-main">
            <div class="comments-entry-avatar">
                {userAvatarElem}
            </div>
            <div class="comments-entry-wrap">
                <div class="comments-entry-meta">
                    <div class="comments-entry-user">
                        {userNameElem}
                    </div>
                    <div class="comments-entry-time">
                        <div class="comments-entry-time-icon">&mdash;</div>
                        {createdTimeElem}
                    </div>
                    {editedElem}
                    {pinnedElem}
                    {deletedElem}
                </div>
                {bodyElem}
                {actions}
            </div>
        </div>
        {repliesElem}
    </div>;

    const setUserInfo = userInfo => {
        $removeChildren(userNameElem);
        if(userInfo) {
            if(typeof userInfo.colour === 'string')
                element.style.setProperty('--user-colour', userInfo.colour);
            userAvatarElem.src = userInfo.avatar;
            userNameElem.appendChild(<a class="comments-entry-user-link" href={userInfo.profile} style="color: var(--user-colour);">{userInfo.name}</a>);
        } else {
            element.style.removeProperty('--user-colour');
            userAvatarElem.src = '/images/no-avatar.png';
            userNameElem.appendChild(<span class="comments-entry-user-dead">Deleted user</span>);
        }
    };
    setUserInfo(postInfo.user);

    const setCreatedTime = date => {
        if(typeof date === 'string')
            date = new Date(date);

        $removeChildren(createdTimeElem);
        element.dataset.commentCreated = date.getTime();
        const time = <time class="comments-entry-time-text" datetime={date.toISOString()} title={date.toString()}>{MszSakuya.formatTimeAgo(date)}</time>;
        createdTimeElem.appendChild(time);
        MszSakuya.trackElement(time);
    };
    setCreatedTime(postInfo.created);

    const updateOrderValue = () => {
        let order = parseInt(element.dataset.commentCreated ?? 0);

        if(element.dataset.commentDeleted !== undefined)
            order -= parseInt(element.dataset.commentDeleted);
        else if(element.dataset.commentPinned !== undefined)
            order += parseInt(element.dataset.commentPinned);

        element.dataset.commentOrder = order;
    };

    const setOptionalTime = (elem, date, name, reorder=true, textIfTrue=null) => {
        if(typeof date === 'string')
            date = new Date(date);

        while(!(elem.lastChild instanceof HTMLDivElement))
            elem.removeChild(elem.lastChild);

        if(date) {
            if(date instanceof Date) {
                if(name)
                    element.dataset[name] = date.getTime();

                const timeElem = <time class="comments-entry-time-text" datetime={date.toISOString()} title={date.toString()}>{MszSakuya.formatTimeAgo(date)}</time>
                elem.appendChild(timeElem);
                MszSakuya.trackElement(timeElem);
            } else {
                // this is kiiiind of a hack but commentCreated isn't updated through this function so who cares lol !
                if(name)
                    element.dataset[name] = element.dataset.commentCreated;

                if(typeof textIfTrue === 'string')
                    elem.appendChild(<span>{textIfTrue}</span>);
            }

            elem.classList.remove('hidden');
        } else {
            if(name)
                delete element.dataset[name];
            elem.classList.add('hidden');
        }

        if(reorder)
            updateOrderValue();
    };

    setOptionalTime(editedElem, postInfo.edited, 'commentEdited', false);
    setOptionalTime(pinnedElem, postInfo.pinned, 'commentPinned', false);
    setOptionalTime(deletedElem, postInfo.deleted, 'commentDeleted', false, 'deleted');
    updateOrderValue();

    const nukeThePost = () => {
        if(replies.count < 1 && replyActions.toggle.count < 1)
            listing.element.removeChild(element);
        else {
            generalActions.visible = false;
            generalActions.disabled = true;
            enableVoteActionsMaybe();
            enableReplyButtonMaybe();
            setUserInfo(null);
            setBody(null);
            setOptionalTime(deletedElem, true, 'commentDeleted', true, 'deleted');
            listing.reorder();
        }
    };

    return {
        get element() { return element; },

        updateLocked() {
            enableVoteActionsMaybe();
            enableReplyButtonMaybe();
            replies.updateLocked();
        },
    };
};

const MszCommentsListing = function(options) {
    let { hidden, root } = options ?? {};

    let loading; // intentionally left as undefined here so the === null is still false
    const entries = new Map;
    const element = <div class={{ 'comments-listing': true, 'comments-listing-root': root, 'hidden': hidden }} />;

    const pub = {
        get element() { return element; },
        get count() { return loading === null ? element.childElementCount : 0; },

        get visible() { return !element.classList.contains('hidden'); },
        set visible(state) { element.classList.toggle('hidden', !state); },

        get loaded() { return loading === null; },

        reset() {
            entries.clear();
            $removeChildren(element);
            loading = new MszLoading;
            element.appendChild(loading.element);
        },
        removeLoading() {
            loading?.element.remove();
            loading = null;
        },

        reorder() {
            // this feels yucky but it works
            const items = Array.from(element.children).sort((a, b) => parseInt(b.dataset.commentOrder) - parseInt(a.dataset.commentOrder));
            for(const item of items)
                element.appendChild(item);
        },
        updateLocked() {
            for(const [, value] of entries)
                value.updateLocked();
        },

        addPost(catInfo, userInfo, postInfo) {
            const existing = element.querySelector(`[data-comment="${postInfo.id}"]`);
            if(existing)
                element.removeChild(existing);

            const entry = new MszCommentsEntry(catInfo ?? {}, userInfo ?? {}, postInfo, pub, root);
            entries.set(postInfo.id, entry);
            element.appendChild(entry.element);
        },
        addPosts(catInfo, userInfo, posts) {
            try {
                if(!Array.isArray(posts))
                    throw 'posts must be an array';
                catInfo ??= {};
                userInfo ??= {};
                for(const postInfo of posts)
                    pub.addPost(catInfo, userInfo, postInfo);
            } finally {
                pub.removeLoading();
            }
        },
    };

    return pub;
};
