package com.earth2me.essentials;

import com.earth2me.essentials.utils.AdventureUtil;
import net.ess3.api.IEssentials;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.regex.Pattern;

public class I18n implements net.ess3.api.II18n {
    private static final String MESSAGES = "messages";
    private static final Pattern NODOUBLEMARK = Pattern.compile("''");
    private static final ExecutorService BUNDLE_LOADER_EXECUTOR = Executors.newFixedThreadPool(2);
    private static final ResourceBundle NULL_BUNDLE = new ResourceBundle() {
        @SuppressWarnings("NullableProblems")
        public Enumeration<String> getKeys() {
            return null;
        }

        protected Object handleGetObject(final @NotNull String key) {
            return null;
        }
    };
    private static I18n instance;
    private final transient Locale defaultLocale = Locale.getDefault();
    private final transient ResourceBundle defaultBundle;
    private final transient IEssentials ess;
    private transient Locale currentLocale = defaultLocale;
    private final transient Map<Locale, ResourceBundle> loadedBundles = new ConcurrentHashMap<>();
    private final transient List<Locale> loadingBundles = new ArrayList<>();
    private transient ResourceBundle localeBundle;
    private final transient Map<Locale, Map<String, MessageFormat>> messageFormatCache = new HashMap<>();

    public I18n(final IEssentials ess) {
        this.ess = ess;
        defaultBundle = ResourceBundle.getBundle(MESSAGES, Locale.ENGLISH, new UTF8PropertiesControl());
        localeBundle = defaultBundle;
    }

    /**
     * Translates a message using the server's configured locale.
     * @param tlKey The translation key.
     * @param objects Translation parameters, if applicable. Note: by default, these will not be parsed for MiniMessage.
     * @return The translated message.
     * @see AdventureUtil#parsed(String)
     */
    public static String tlLiteral(final String tlKey, final Object... objects) {
        if (instance == null) {
            return "";
        }

        return tlLocale(instance.currentLocale, tlKey, objects);
    }

    /**
     * Translates a message using the provided locale.
     * @param locale The locale to translate the key to.
     * @param tlKey The translation key.
     * @param objects Translation parameters, if applicable. Note: by default, these will not be parsed for MiniMessage.
     * @return The translated message.
     * @see AdventureUtil#parsed(String)
     */
    public static String tlLocale(final Locale locale, final String tlKey, final Object... objects) {
        if (instance == null) {
            return "";
        }
        if (objects.length == 0) {
            return NODOUBLEMARK.matcher(instance.translate(locale, tlKey)).replaceAll("'");
        } else {
            return instance.format(tlKey, objects);
        }
    }

    public static String capitalCase(final String input) {
        return input == null || input.isEmpty() ? input : input.toUpperCase(Locale.ENGLISH).charAt(0) + input.toLowerCase(Locale.ENGLISH).substring(1);
    }

    public void onEnable() {
        instance = this;
    }

    public void onDisable() {
        instance = null;
    }

    @Override
    public Locale getCurrentLocale() {
        return currentLocale;
    }

    /**
     * Returns the {@link ResourceBundle} for the given {@link Locale}, if loaded. If a bundle is requested which is
     * not loaded, it will be loaded asynchronously and the default bundle will be returned in the meantime.
     */
    private ResourceBundle getBundle(final Locale locale) {
        if (loadedBundles.containsKey(locale)) {
            return loadedBundles.get(locale);
        } else {
            synchronized (loadingBundles) {
                if (!loadingBundles.contains(locale)) {
                    loadingBundles.add(locale);
                    BUNDLE_LOADER_EXECUTOR.submit(() -> {
                        ResourceBundle bundle;
                        try {
                            bundle = ResourceBundle.getBundle(MESSAGES, locale, new FileResClassLoader(I18n.class.getClassLoader(), ess), new UTF8PropertiesControl());
                        } catch (MissingResourceException ex) {
                            try {
                                bundle = ResourceBundle.getBundle(MESSAGES, locale, new UTF8PropertiesControl());
                            } catch (MissingResourceException ex2) {
                                bundle = NULL_BUNDLE;
                            }
                        }

                        loadedBundles.put(locale, bundle);
                        synchronized (loadingBundles) {
                            loadingBundles.remove(locale);
                        }
                    });
                }
            }
            return defaultBundle;
        }
    }

    private String translate(final Locale locale, final String string) {
        try {
            try {
                return getBundle(locale).getString(string);
            } catch (final MissingResourceException ex) {
                return localeBundle.getString(string);
            }
        } catch (final MissingResourceException ex) {
            if (ess != null && ess.getSettings().isDebug()) {
                ess.getLogger().log(Level.WARNING, String.format("Missing translation key \"%s\" in translation file %s", ex.getKey(), localeBundle.getLocale().toString()), ex);
            }
            return defaultBundle.getString(string);
        }
    }

    private String format(final String string, final Object... objects) {
        return format(currentLocale, string, objects);
    }

    private String format(final Locale locale, final String string, final Object... objects) {
        String format = translate(locale, string);

        MessageFormat messageFormat = messageFormatCache.computeIfAbsent(locale, l -> new HashMap<>()).get(format);
        if (messageFormat == null) {
            try {
                messageFormat = new MessageFormat(format);
            } catch (final IllegalArgumentException e) {
                ess.getLogger().log(Level.SEVERE, "Invalid Translation key for '" + string + "': " + e.getMessage());
                format = format.replaceAll("\\{(\\D*?)}", "\\[$1\\]");
                messageFormat = new MessageFormat(format);
            }
            messageFormatCache.get(locale).put(format, messageFormat);
        }

        final Object[] processedArgs = mutateArgs(objects, arg -> {
            final String str = arg instanceof AdventureUtil.ParsedPlaceholder ? arg.toString() : AdventureUtil.miniMessage().escapeTags(arg.toString());
            return AdventureUtil.legacyToMini(str);
        });

        return messageFormat.format(processedArgs).replace(' ', ' '); // replace nbsp with a space
    }

    public static Object[] mutateArgs(final Object[] objects, final Function<Object, String> mutator) {
        final Object[] args = new Object[objects.length];
        for (int i = 0; i < objects.length; i++) {
            final Object object = objects[i];
            // MessageFormat will format these itself, troll face.
            if (object instanceof Number || object instanceof Date || object == null) {
                args[i] = object;
                continue;
            }

            args[i] = mutator.apply(object);
        }
        return args;
    }

    public void updateLocale(final String loc) {
        if (loc != null && !loc.isEmpty()) {
            currentLocale = getLocale(loc);
        }
        ResourceBundle.clearCache();
        loadedBundles.clear();
        messageFormatCache.clear();
        ess.getLogger().log(Level.INFO, String.format("Using locale %s", currentLocale.toString()));

        try {
            localeBundle = ResourceBundle.getBundle(MESSAGES, currentLocale, new UTF8PropertiesControl());
        } catch (final MissingResourceException ex) {
            localeBundle = NULL_BUNDLE;
        }
    }

    public static Locale getLocale(final String loc) {
        if (loc == null || loc.isEmpty()) {
            return instance.currentLocale;
        }
        final String[] parts = loc.split("[_.]");
        if (parts.length == 1) {
            return new Locale(parts[0]);
        }
        if (parts.length == 2) {
            return new Locale(parts[0], parts[1]);
        }
        if (parts.length == 3) {
            return new Locale(parts[0], parts[1], parts[2]);
        }
        return instance.currentLocale;
    }

    /**
     * Attempts to load properties files from the plugin directory before falling back to the jar.
     */
    private static class FileResClassLoader extends ClassLoader {
        private final transient File messagesFolder;

        FileResClassLoader(final ClassLoader classLoader, final IEssentials ess) {
            super(classLoader);
            this.messagesFolder = new File(ess.getDataFolder(), "messages");
            //noinspection ResultOfMethodCallIgnored
            this.messagesFolder.mkdirs();
        }

        @Override
        public URL getResource(final String string) {
            final File file = new File(messagesFolder, string);
            if (file.exists()) {
                try {
                    return file.toURI().toURL();
                } catch (final MalformedURLException ignored) {
                }
            }
            return null;
        }

        @Override
        public InputStream getResourceAsStream(final String string) {
            final File file = new File(messagesFolder, string);
            if (file.exists()) {
                try {
                    return new FileInputStream(file);
                } catch (final FileNotFoundException ignored) {
                }
            }
            return null;
        }
    }

    /**
     * Reads .properties files as UTF-8 instead of ISO-8859-1, which is the default on Java 8/below.
     * Java 9 fixes this by defaulting to UTF-8 for .properties files.
     */
    private static class UTF8PropertiesControl extends ResourceBundle.Control {
        public ResourceBundle newBundle(final String baseName, final Locale locale, final String format, final ClassLoader loader, final boolean reload) throws IOException {
            final String resourceName = toResourceName(toBundleName(baseName, locale), "properties");
            ResourceBundle bundle = null;
            InputStream stream = null;
            if (reload) {
                final URL url = loader.getResource(resourceName);
                if (url != null) {
                    final URLConnection connection = url.openConnection();
                    if (connection != null) {
                        connection.setUseCaches(false);
                        stream = connection.getInputStream();
                    }
                }
            } else {
                stream = loader.getResourceAsStream(resourceName);
            }
            if (stream != null) {
                try {
                    // use UTF-8 here, this is the important bit
                    bundle = new PropertyResourceBundle(new InputStreamReader(stream, StandardCharsets.UTF_8));
                } finally {
                    stream.close();
                }
            }
            return bundle;
        }

        @Override
        public Locale getFallbackLocale(String baseName, Locale locale) {
            if (baseName == null || locale == null) {
                throw new NullPointerException();
            }
            return null;
        }
    }
}
