package moe.kyokobot.bot.i18n;

import com.google.common.base.Charsets;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.util.ULocale;
import com.mewna.catnip.entity.guild.Guild;
import com.mewna.catnip.entity.guild.Member;
import com.mewna.catnip.entity.user.User;
import moe.kyokobot.shared.entity.GuildData;
import moe.kyokobot.shared.entity.UserData;
import moe.kyokobot.shared.i18n.Language;
import org.apache.commons.codec.language.bm.Lang;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class I18n {
    private static final Logger logger = LoggerFactory.getLogger(I18n.class);
    private Map<Language, MessageMap> languages;

    @SuppressWarnings("squid:S2629")
    public void loadMessages() {
        logger.debug("Loading messages...");
        languages = new EnumMap<>(Language.class);

        for (Language l : Language.values()) {
            if (l != Language.DEFAULT) try {
                File f = new File("./messages/" + l.getShortName() + "/messages.properties");

                URL url;
                if (f.exists()) {
                    url = f.toURI().toURL();
                } else {
                    url = getClass().getResource("/" + l.getShortName() + "/messages.properties");
                    if (url == null) {
                        logger.warn("Messages file for language {} does not exist.", l.name());
                        continue;
                    }
                }

                logger.debug("Loaded messages for language {} from {}", l.getLocalized(), url);
                loadMessages(l, url.openStream());
            } catch (Exception e) {
                logger.error("Error while loading language!", e);
            }
        }
    }

    /**
     * Loads messages for given language message map from InputStream.
     *
     * @param language    The language we want to load messages for.
     * @param inputStream The input stream which contains message file.
     */
    public void loadMessages(Language language, InputStream inputStream) throws IOException {
        Properties p = new Properties();
        p.load(new InputStreamReader(inputStream, Charsets.UTF_8));
        loadMessages(language, p);
    }

    /**
     * Loads messages for given language message map from Properties object.
     *
     * @param language   The language we want to load messages for.
     * @param properties Properties instance with messages.
     */
    public void loadMessages(Language language, Properties properties) {
        var messageMap = languages.computeIfAbsent(language, MessageMap::new);
        properties.forEach((k, v) -> messageMap.registerMessage((String) k, (String) v));
    }

    /**
     * @param language The language you want to get message map of.
     * @return Immutable message map of specified language.
     */
    public Map<String, String> getMessageMap(@NotNull Language language) {
        return languages.get(language).immutableMap();
    }

    /**
     * @param language Preferred language for messages.
     * @param key      Translation key.
     * @return Translated message, English fallback or provided key if missing.
     */
    @NotNull
    public String get(@NotNull Language language, @NotNull String key) {
        if (!languages.containsKey(Objects.requireNonNull(language))) {
            language = Language.ENGLISH;
        }

        var map = languages.get(language);
        var message = map.message(key);
        if (message == null) {
            message = languages.get(Language.ENGLISH).message(key);
        }

        return Objects.requireNonNullElse(message, key);
    }

    /**
     * @param language Preferred language for messages.
     * @param key      Translation key.
     * @return MessageFormat object for specified translation key or null if it doesn't exist.
     */
    @Nullable
    public MessageFormat getICU(@NotNull Language language, @NotNull String key) {
        if (!languages.containsKey(Objects.requireNonNull(language))) {
            language = Language.ENGLISH;
        }

        var map = languages.get(language);
        var message = map.getICU(key);
        if (message == null) {
            message = languages.get(Language.ENGLISH).getICU(key);
        }

        return message;
    }

    /**
     * Gets language of specified guild.
     *
     * @param guild The guild which we want to get language of.
     * @return The language of specified guild.
     */
    public Language getLanguage(@NotNull Guild guild) {
        try {
            var l = GuildData.get(guild.idAsLong()).language();
            return l == Language.DEFAULT ? Language.ENGLISH : l;
        } catch (Exception e) {
            logger.error("Error while getting guild language!", e);
            return Language.ENGLISH;
        }
    }

    /**
     * Gets language of specified member.
     *
     * @param member The member which we want to get language of.
     * @return The language of specified member.
     */
    public Language getLanguage(Member member) {
        try {
            Language l = UserData.get(member.idAsLong()).language();
            return l == Language.DEFAULT ? getLanguage(member.guild()) : l;
        } catch (Exception e) {
            logger.error("An error occurred while trying to get user language!", e);
            return Language.ENGLISH;
        }
    }

    /**
     * Gets language of specified user.
     *
     * @param user The user which we want to get language of.
     * @return The language of specified user.
     */
    public Language getLanguage(User user) {
        try {
            Language l = UserData.get(user.idAsLong()).language();
            return l == Language.DEFAULT ? Language.ENGLISH : l;
        } catch (Exception e) {
            logger.error("Error while getting user language!", e);
            return Language.ENGLISH;
        }
    }

    class MessageMap {
        private final Language language;
        private final Map<Language, ULocale> uLocaleMap;
        private final Map<String, String> messages;
        private final Map<String, MessageFormat> icuCache;

        MessageMap(Language language) {
            this.language = language;
            this.uLocaleMap = new EnumMap<>(Language.class);
            this.messages = new HashMap<>();
            this.icuCache = new ConcurrentHashMap<>();
        }

        void registerMessage(String key, String value) {
            messages.put(key, value);
        }

        String message(String key) {
            return messages.get(key);
        }

        MessageFormat getICU(String key) {
            return messages.containsKey(key)
                    ? icuCache.computeIfAbsent(key, t -> new MessageFormat(messages.get(key), getULocale(language)))
                    : null;
        }

        Map<String, String> immutableMap() {
            return Collections.unmodifiableMap(messages);
        }

        private ULocale getULocale(Language language) {
            return uLocaleMap.computeIfAbsent(language, l -> ULocale.forLocale(l.getLocale()));
        }
    }
}