/*
 * This file is part of Discord4J.
 *
 * Discord4J is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Discord4J 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Discord4J. If not, see <http://www.gnu.org/licenses/>.
 */
package discord4j.core.event.dispatch;

import discord4j.core.GatewayDiscordClient;
import discord4j.core.event.domain.message.*;
import discord4j.core.object.Embed;
import discord4j.core.object.entity.Member;
import discord4j.core.object.entity.Message;
import discord4j.core.object.reaction.ReactionEmoji;
import discord4j.core.util.ListUtil;
import discord4j.discordjson.json.*;
import discord4j.discordjson.json.gateway.*;
import discord4j.discordjson.possible.Possible;
import discord4j.common.util.Snowflake;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

class MessageDispatchHandlers {

    static Mono<MessageCreateEvent> messageCreate(DispatchContext<MessageCreate> context) {
        GatewayDiscordClient gateway = context.getGateway();
        MessageData message = context.getDispatch().message();
        long messageId = Snowflake.asLong(message.id());
        long channelId = Snowflake.asLong(message.channelId());

        Optional<Member> maybeMember = context.getDispatch().message().guildId().toOptional()
                .map(Snowflake::asLong)
                .flatMap(guildId -> message.member().toOptional()
                        .map(memberData -> new Member(gateway, MemberData.builder()
                                .from(MemberData.builder()
                                        .user(message.author())
                                        .nick(memberData.nick())
                                        .roles(memberData.roles())
                                        .joinedAt(memberData.joinedAt())
                                        .premiumSince(memberData.premiumSince())
                                        .hoistedRole(memberData.hoistedRole())
                                        .deaf(memberData.deaf())
                                        .mute(memberData.mute())
                                        .build())
                                .user(message.author())
                                .build(), guildId)));

        Mono<Void> saveMessage = context.getStateHolder().getMessageStore()
                .save(messageId, message);

        Mono<Void> editLastMessageId = context.getStateHolder().getChannelStore()
                .find(channelId)
                .map(channel -> ChannelData.builder()
                        .from(channel)
                        .lastMessageId(message.id())
                        .build())
                .flatMap(channelBean -> context.getStateHolder().getChannelStore().save(channelId, channelBean));

        return saveMessage
                .and(editLastMessageId)
                .thenReturn(new MessageCreateEvent(gateway, context.getShardInfo(), new Message(gateway, message),
                        context.getDispatch().message().guildId().toOptional()
                                .map(Snowflake::asLong)
                                .orElse(null),
                        maybeMember.orElse(null)));
    }

    static Mono<MessageDeleteEvent> messageDelete(DispatchContext<MessageDelete> context) {
        GatewayDiscordClient gateway = context.getGateway();
        long messageId = Snowflake.asLong(context.getDispatch().id());
        long channelId = Snowflake.asLong(context.getDispatch().channelId());
        Long guildId = context.getDispatch().guildId()
            .toOptional()
            .map(Snowflake::asLong)
            .orElse(null);

        Mono<Void> deleteMessage = context.getStateHolder().getMessageStore().delete(messageId);

        return context.getStateHolder().getMessageStore()
                .find(messageId)
                .flatMap(deleteMessage::thenReturn)
                .map(messageBean -> new MessageDeleteEvent(gateway, context.getShardInfo(), messageId, channelId, guildId,
                        new Message(gateway, messageBean)))
                .defaultIfEmpty(new MessageDeleteEvent(gateway, context.getShardInfo(), messageId, channelId, guildId, null));
    }

    static Mono<MessageBulkDeleteEvent> messageDeleteBulk(DispatchContext<MessageDeleteBulk> context) {
        GatewayDiscordClient gateway = context.getGateway();
        List<Long> messageIds = context.getDispatch().ids().stream()
                .map(Snowflake::asLong)
                .collect(Collectors.toList());
        long channelId = Snowflake.asLong(context.getDispatch().channelId());
        long guildId = Snowflake.asLong(context.getDispatch().guildId().get()); // always present

        Mono<Void> deleteMessages = context.getStateHolder().getMessageStore()
                .delete(Flux.fromIterable(messageIds));

        return Flux.fromIterable(messageIds)
                .flatMap(context.getStateHolder().getMessageStore()::find)
                .map(messageBean -> new Message(gateway, messageBean))
                .collect(Collectors.toSet())
                .flatMap(deleteMessages::thenReturn)
                .map(messages -> new MessageBulkDeleteEvent(gateway, context.getShardInfo(), messageIds, channelId,
                        guildId, messages))
                .defaultIfEmpty(new MessageBulkDeleteEvent(gateway, context.getShardInfo(), messageIds, channelId,
                        guildId, Collections.emptySet()));
    }

    static Mono<ReactionAddEvent> messageReactionAdd(DispatchContext<MessageReactionAdd> context) {
        GatewayDiscordClient gateway = context.getGateway();
        long userId = Snowflake.asLong(context.getDispatch().userId());
        long channelId = Snowflake.asLong(context.getDispatch().channelId());
        long messageId = Snowflake.asLong(context.getDispatch().messageId());
        Long guildId = context.getDispatch().guildId()
                .toOptional()
                .map(Snowflake::asLong)
                .orElse(null);

        MemberData memberData = context.getDispatch().member().toOptional().orElse(null);

        Mono<Void> addToMessage = context.getStateHolder().getMessageStore()
                .find(messageId)
                .map(oldMessage -> {
                    boolean me = Objects.equals(userId, gateway.getSelfId().asLong());
                    ImmutableMessageData.Builder newMessageBuilder = MessageData.builder().from(oldMessage);

                    if (oldMessage.reactions().isAbsent()) {
                        newMessageBuilder.addReaction(ReactionData.builder()
                                .count(1)
                                .me(me)
                                .emoji(context.getDispatch().emoji())
                                .build());
                    } else {
                        List<ReactionData> reactions = oldMessage.reactions().get();
                        int i;
                        for (i = 0; i < reactions.size(); i++) {
                            ReactionData r = reactions.get(i);
                            // (non-null id && matching id) OR (null id && matching name)
                            boolean emojiHasId = context.getDispatch().emoji().id().isPresent();
                            if ((emojiHasId && context.getDispatch().emoji().id().equals(r.emoji().id()))
                                    || (!emojiHasId && context.getDispatch().emoji().name().equals(r.emoji().name()))) {
                                break;
                            }
                        }

                        if (i < reactions.size()) {
                            // message already has this reaction: bump 1
                            ReactionData oldExisting = reactions.get(i);
                            ReactionData newExisting = ReactionData.builder()
                                    .from(oldExisting)
                                    .me(oldExisting.me() || me)
                                    .count(oldExisting.count() + 1)
                                    .build();
                            newMessageBuilder.reactions(ListUtil.replace(reactions,
                                    oldExisting, newExisting));
                        } else {
                            // message doesn't have this reaction: create
                            ReactionData reaction = ReactionData.builder()
                                    .emoji(context.getDispatch().emoji())
                                    .me(me)
                                    .count(1)
                                    .build();
                            newMessageBuilder.reactions(ListUtil.add(reactions, reaction));
                        }
                    }

                    return newMessageBuilder.build();
                })
                .flatMap(message -> context.getStateHolder().getMessageStore().save(messageId, message));

        Long emojiId = context.getDispatch().emoji().id()
                .map(Snowflake::asLong)
                .orElse(null);
        String emojiName = context.getDispatch().emoji().name()
                .orElse(null);
        boolean emojiAnimated = context.getDispatch().emoji().animated()
                .toOptional()
                .orElse(false);
        ReactionEmoji emoji = ReactionEmoji.of(emojiId, emojiName, emojiAnimated);
        @SuppressWarnings("ConstantConditions")
        Member member = memberData != null ? new Member(gateway, memberData, guildId) : null;

        return addToMessage.thenReturn(new ReactionAddEvent(gateway, context.getShardInfo(), userId, channelId,
                messageId, guildId, emoji, member));
    }

    static Mono<ReactionRemoveEvent> messageReactionRemove(DispatchContext<MessageReactionRemove> context) {
        GatewayDiscordClient gateway = context.getGateway();
        long userId = Snowflake.asLong(context.getDispatch().userId());
        long channelId = Snowflake.asLong(context.getDispatch().channelId());
        long messageId = Snowflake.asLong(context.getDispatch().messageId());
        Long guildId = context.getDispatch().guildId()
                .toOptional()
                .map(Snowflake::asLong)
                .orElse(null);

        Mono<Void> removeFromMessage = context.getStateHolder().getMessageStore()
                .find(messageId)
                .filter(message -> !message.reactions().isAbsent())
                .map(oldMessage -> {
                    boolean me = Objects.equals(userId, gateway.getSelfId().asLong());
                    ImmutableMessageData.Builder newMessageBuilder = MessageData.builder().from(oldMessage);

                    List<ReactionData> reactions = oldMessage.reactions().get();
                    int i;
                    // filter covers getReactions() null case
                    for (i = 0; i < reactions.size(); i++) {
                        ReactionData r = reactions.get(i);
                        // (non-null id && matching id) OR (null id && matching name)
                        boolean emojiHasId = context.getDispatch().emoji().id().isPresent();
                        if ((emojiHasId && context.getDispatch().emoji().id().equals(r.emoji().id()))
                                || (!emojiHasId && context.getDispatch().emoji().name().equals(r.emoji().name()))) {
                            break;
                        }
                    }

                    if (i < reactions.size()) {
                        ReactionData existing = reactions.get(i);
                        if (existing.count() - 1 == 0) {
                            newMessageBuilder.reactions(ListUtil.remove(reactions,
                                    reaction -> reaction.equals(existing)));
                        } else {
                            ReactionData newExisting = ReactionData.builder()
                                    .from(existing)
                                    .count(existing.count() - 1)
                                    .me(!me && existing.me())
                                    .build();
                            newMessageBuilder.reactions(ListUtil.replace(reactions, existing, newExisting));
                        }
                    }
                    return newMessageBuilder.build();
                })
                .flatMap(message -> context.getStateHolder().getMessageStore().save(messageId, message));

        Long emojiId = context.getDispatch().emoji().id()
                .map(Snowflake::asLong)
                .orElse(null);
        String emojiName = context.getDispatch().emoji().name()
                .orElse(null);
        boolean emojiAnimated = context.getDispatch().emoji().animated()
                .toOptional()
                .orElse(false);
        ReactionEmoji emoji = ReactionEmoji.of(emojiId, emojiName, emojiAnimated);
        return removeFromMessage.thenReturn(new ReactionRemoveEvent(gateway, context.getShardInfo(), userId,
                channelId, messageId, guildId, emoji));
    }

    static Mono<ReactionRemoveEmojiEvent> messageReactionRemoveEmoji(DispatchContext<MessageReactionRemoveEmoji> context) {
        GatewayDiscordClient gateway = context.getGateway();
        long channelId = Snowflake.asLong(context.getDispatch().channelId());
        long messageId = Snowflake.asLong(context.getDispatch().messageId());
        Long guildId = context.getDispatch().guildId()
                .toOptional()
                .map(Snowflake::asLong)
                .orElse(null);

        Mono<Void> removeFromMessage = context.getStateHolder().getMessageStore()
                .find(messageId)
                .filter(message -> !message.reactions().isAbsent())
                .map(oldMessage -> {
                    ImmutableMessageData.Builder newMessageBuilder = MessageData.builder().from(oldMessage);

                    List<ReactionData> reactions = oldMessage.reactions().get();
                    int i;
                    // filter covers getReactions() null case
                    for (i = 0; i < reactions.size(); i++) {
                        ReactionData r = reactions.get(i);
                        // (non-null id && matching id) OR (null id && matching name)
                        boolean emojiHasId = context.getDispatch().emoji().id().isPresent();
                        if ((emojiHasId && context.getDispatch().emoji().id().equals(r.emoji().id()))
                                || (!emojiHasId && context.getDispatch().emoji().name().equals(r.emoji().name()))) {
                            break;
                        }
                    }

                    if (i < reactions.size()) {
                        ReactionData existing = reactions.get(i);
                        newMessageBuilder.reactions(ListUtil.remove(reactions,
                                reaction -> reaction.equals(existing)));
                    }
                    return newMessageBuilder.build();
                })
                .flatMap(message -> context.getStateHolder().getMessageStore().save(messageId, message));

        Long emojiId = context.getDispatch().emoji().id()
                .map(Snowflake::asLong)
                .orElse(null);
        String emojiName = context.getDispatch().emoji().name()
                .orElse(null);
        boolean emojiAnimated = context.getDispatch().emoji().animated()
                .toOptional()
                .orElse(false);
        ReactionEmoji emoji = ReactionEmoji.of(emojiId, emojiName, emojiAnimated);
        return removeFromMessage.thenReturn(new ReactionRemoveEmojiEvent(gateway, context.getShardInfo(), channelId,
                messageId, guildId, emoji));
    }

    static Mono<ReactionRemoveAllEvent> messageReactionRemoveAll(DispatchContext<MessageReactionRemoveAll> context) {
        GatewayDiscordClient gateway = context.getGateway();
        long channelId = Snowflake.asLong(context.getDispatch().channelId());
        long messageId = Snowflake.asLong(context.getDispatch().messageId());
        Long guildId = context.getDispatch().guildId()
                .toOptional()
                .map(Snowflake::asLong)
                .orElse(null);

        Mono<Void> removeAllFromMessage = context.getStateHolder().getMessageStore()
                .find(messageId)
                .map(message -> MessageData.builder()
                        .from(message)
                        .reactions(Possible.absent())
                        .build())
                .flatMap(message -> context.getStateHolder().getMessageStore().save(messageId, message));

        return removeAllFromMessage.thenReturn(new ReactionRemoveAllEvent(gateway, context.getShardInfo(), channelId,
                messageId, guildId));
    }

    static Mono<MessageUpdateEvent> messageUpdate(DispatchContext<MessageUpdate> context) {
        GatewayDiscordClient gateway = context.getGateway();
        PartialMessageData messageData = context.getDispatch().message();

        long channelId = Snowflake.asLong(messageData.channelId());
        long messageId = Snowflake.asLong(messageData.id());
        Long guildId = messageData.guildId()
                .toOptional()
                .map(Snowflake::asLong)
                .orElse(null);

        String currentContent = messageData.content().toOptional().orElse(null);
        List<Embed> embedList = messageData.embeds()
                .stream()
                .map(embedData -> new Embed(gateway, embedData))
                .collect(Collectors.toList());

        Mono<MessageUpdateEvent> update = context.getStateHolder().getMessageStore()
                .find(messageId)
                .flatMap(oldMessageData -> {
                    // updating the content and embed of the bean in the store
                    Message oldMessage = new Message(gateway, oldMessageData);

                    boolean contentChanged = !messageData.content().isAbsent() &&
                            !Objects.equals(oldMessageData.content(), messageData.content().get());
                    boolean embedsChanged = !Objects.equals(oldMessageData.embeds(), messageData.embeds());

                    MessageData newMessageData = MessageData.builder()
                            .from(oldMessageData)
                            .content(messageData.content().toOptional()
                                    .orElse(oldMessageData.content()))
                            .embeds(messageData.embeds())
                            .mentions(messageData.mentions())
                            .mentionRoles(messageData.mentionRoles())
                            .mentionEveryone(messageData.mentionEveryone().toOptional()
                                    .orElse(oldMessageData.mentionEveryone()))
                            .editedTimestamp(messageData.editedTimestamp())
                            .build();

                    MessageUpdateEvent event = new MessageUpdateEvent(gateway, context.getShardInfo(), messageId,
                            channelId, guildId, oldMessage, contentChanged, currentContent, embedsChanged, embedList);

                    return context.getStateHolder().getMessageStore()
                            .save(messageId, newMessageData)
                            .thenReturn(event);
                });

        MessageUpdateEvent event = new MessageUpdateEvent(gateway, context.getShardInfo(), messageId, channelId,
                guildId, null, !messageData.content().isAbsent(),
                currentContent, !messageData.embeds().isEmpty(), embedList);

        return update.defaultIfEmpty(event);
    }

    static <T> Possible<T> newPossibleIfPresent(Possible<T> oldPossible, Possible<T> newPossible) {
        return newPossible.isAbsent() ? oldPossible : newPossible;
    }
}