/*
 * This file is part of JuniperBot.
 *
 * JuniperBot 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.

 * JuniperBot 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 JuniperBot. If not, see <http://www.gnu.org/licenses/>.
 */
package ru.juniperbot.common.worker.event.service;

import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.GenericEvent;
import net.dv8tion.jda.api.events.guild.GenericGuildEvent;
import net.dv8tion.jda.api.events.guild.member.GenericGuildMemberEvent;
import net.dv8tion.jda.api.requests.RestAction;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NamedThreadLocal;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import ru.juniperbot.common.configuration.CommonProperties;
import ru.juniperbot.common.service.ConfigService;
import ru.juniperbot.common.service.TransactionHandler;
import ru.juniperbot.common.utils.LocaleUtils;

import java.awt.*;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.Supplier;

@Slf4j
@Service
public class ContextServiceImpl implements ContextService {

    private static class ContextHolder {
        private Locale locale;
        private Color color;
        private Long guildId;
        private String userId;
    }

    private static final String MDC_GUILD = "guildId";

    private static final String MDC_USER = "userId";

    private final ThreadLocal<Locale> localeHolder = new NamedThreadLocal<>("ContextServiceImpl.Locale");

    private final ThreadLocal<Color> colorHolder = new NamedThreadLocal<>("ContextServiceImpl.Color");

    private final ThreadLocal<Long> guildHolder = new NamedThreadLocal<>("ContextServiceImpl.GuildIds");

    @Getter
    private Color accentColor;

    @Autowired
    private CommonProperties commonProperties;

    @Autowired
    private ConfigService configService;

    @Autowired
    private SourceResolverService resolverService;

    @Autowired
    private TransactionHandler transactionHandler;

    @Override
    public Locale getLocale() {
        Locale locale = localeHolder.get();
        if (locale == null) {
            Long guildId = guildHolder.get();
            if (guildId != null) {
                setLocale(configService.getLocale(guildId));
                locale = localeHolder.get();
            }
        }
        return locale != null ? locale : LocaleUtils.getDefaultLocale();
    }

    @Override
    public Color getColor() {
        Color color = colorHolder.get();
        if (color == null) {
            Long guildId = guildHolder.get();
            if (guildId != null) {
                setColor(Color.decode(configService.getColor(guildId)));
                color = colorHolder.get();
            }
        }
        return color != null ? color : getDefaultColor();
    }

    @Override
    public Color getDefaultColor() {
        if (accentColor == null) {
            String defaultAccentColor = commonProperties.getDiscord().getDefaultAccentColor();
            accentColor = StringUtils.isNotEmpty(defaultAccentColor) ? Color.decode(defaultAccentColor) : null;
        }
        return accentColor;
    }

    @Override
    public Locale getLocale(String localeName) {
        return LocaleUtils.getOrDefault(localeName);
    }

    @Override
    public Locale getLocale(Guild guild) {
        return getLocale(guild.getIdLong());
    }

    @Override
    public Locale getLocale(long guildId) {
        return LocaleUtils.getOrDefault(configService.getLocale(guildId));
    }

    @Override
    public void setLocale(Locale locale) {
        if (locale == null) {
            localeHolder.remove();
        } else {
            localeHolder.set(locale);
        }
    }

    @Override
    public void setColor(Color color) {
        if (color == null) {
            colorHolder.remove();
        } else {
            colorHolder.set(color);
        }
    }

    public void setGuildId(Long guildId) {
        if (guildId == null) {
            guildHolder.remove();
        } else {
            guildHolder.set(guildId);
        }
    }

    @Override
    public void initContext(GenericEvent event) {
        Guild guild = null;
        User user = null;

        if (event instanceof GenericGuildMemberEvent) {
            GenericGuildMemberEvent memberEvent = (GenericGuildMemberEvent) event;
            guild = memberEvent.getMember().getGuild();
            user = memberEvent.getMember().getUser();
        }

        if (guild == null && event instanceof GenericGuildEvent) {
            guild = ((GenericGuildEvent) event).getGuild();
        }

        if (guild == null || user == null) {
            Member member = resolverService.getMember(event);
            if (member != null) {
                guild = member.getGuild();
                user = member.getUser();
            }
        }
        if (guild == null) {
            guild = resolverService.getGuild(event);
        }
        if (user == null) {
            user = resolverService.getUser(event);
        }
        if (guild != null) {
            initContext(guild);
        }
        if (user != null) {
            initContext(user);
        }
    }

    @Override
    public void initContext(Guild guild) {
        if (guild != null) {
            initContext(guild.getIdLong());
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T withContext(Long guildId, Supplier<T> action) {
        Object[] holder = new Object[1];
        withContext(guildId, () -> {
            holder[0] = action.get();
        });
        return (T) holder[0];
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T withContext(Guild guild, Supplier<T> action) {
        Object[] holder = new Object[1];
        withContext(guild, () -> {
            holder[0] = action.get();
        });
        return (T) holder[0];
    }

    @Override
    public void withContext(Long guildId, Runnable action) {
        withContext(guildId, this::initContext, action);
    }

    @Override
    public void withContext(Guild guild, Runnable action) {
        withContext(guild, this::initContext, action);
    }

    private <T> void withContext(T value, Consumer<T> init, Runnable action) {
        if (value == null) {
            action.run();
            return;
        }
        ContextHolder currentContext = getContext();
        resetContext();
        init.accept(value);
        try {
            action.run();
        } finally {
            setContext(currentContext);
        }
    }

    @Override
    @Async
    public void withContextAsync(Guild guild, Runnable action) {
        transactionHandler.runInTransaction(() -> withContext(guild, action));
    }

    @Override
    public <T> void queue(Guild guild, RestAction<T> action, Consumer<T> success) {
        action.queue(e -> withContext(guild, () -> success.accept(e)));
    }

    @Override
    public <T> void queue(Long guildId, RestAction<T> action, Consumer<T> success) {
        action.queue(e -> withContext(guildId, () -> success.accept(e)));
    }

    @Override
    public void initContext(User user) {
        if (user != null) {
            MDC.put(MDC_USER, user.getId());
        }
    }

    @Override
    public void initContext(long guildId) {
        MDC.put(MDC_GUILD, String.valueOf(guildId));
        guildHolder.set(guildId);
    }

    @Override
    public void resetContext() {
        MDC.remove(MDC_GUILD);
        MDC.remove(MDC_USER);
        guildHolder.remove();
        localeHolder.remove();
        colorHolder.remove();
    }

    public ContextHolder getContext() {
        ContextHolder holder = new ContextHolder();
        holder.guildId = guildHolder.get();
        holder.locale = localeHolder.get();
        holder.color = colorHolder.get();
        holder.userId = MDC.get(MDC_USER);
        return holder;
    }

    public void setContext(ContextHolder holder) {
        setLocale(holder.locale);
        setGuildId(holder.guildId);
        MDC.put(MDC_GUILD, String.valueOf(holder.guildId));
        MDC.put(MDC_USER, holder.userId);
    }

    private void setLocale(String tag) {
        setLocale(LocaleUtils.get(tag));
    }
}