package tc.oc.commons.bukkit.settings;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;

import com.google.common.collect.ImmutableMap;
import me.anxuiz.settings.Setting;
import me.anxuiz.settings.SettingCallback;
import me.anxuiz.settings.SettingCallbackManager;
import me.anxuiz.settings.SettingManager;
import me.anxuiz.settings.SettingRegistry;
import me.anxuiz.settings.TypeParseException;
import me.anxuiz.settings.base.AbstractSettingManager;
import me.anxuiz.settings.bukkit.PlayerSettingCallback;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import tc.oc.api.bukkit.users.BukkitUserStore;
import tc.oc.api.bukkit.users.OnlinePlayers;
import tc.oc.api.docs.Server;
import tc.oc.api.docs.User;
import tc.oc.api.users.ChangeSettingRequest;
import tc.oc.api.users.UserService;
import tc.oc.commons.bukkit.event.UserLoginEvent;
import tc.oc.commons.core.plugin.PluginFacet;

/**
 * A singleton with several responsibilities related to player settings:
 *
 * - Provides access to settings through the {@link SettingManagerProvider} interface
 * - Loads saved settings on player login
 * - Listens for setting changes and saves them
 * - Registers settings and callbacks that were bound at configuration time
 */
@Singleton
class SettingManagerProviderImpl implements SettingManagerProvider, Listener, PluginFacet {

    private final Server localServer;
    private final BukkitUserStore userStore;
    private final UserService userService;
    private final OnlinePlayers onlinePlayers;
    private final SettingRegistry settingRegistry;

    @Inject
    SettingManagerProviderImpl(Server localServer,
                               SettingCallbackManager callbackManager,
                               BukkitUserStore userStore,
                               UserService userService,
                               OnlinePlayers onlinePlayers,
                               SettingRegistry settingRegistry,
                               Set<Setting> settings,
                               Map<Setting, SettingCallback> callbacks) {

        this.localServer = localServer;
        this.userStore = userStore;
        this.userService = userService;
        this.onlinePlayers = onlinePlayers;
        this.settingRegistry = settingRegistry;

        callbackManager.addGlobalCallback(new Callback());

        settings.forEach(settingRegistry::register);
        callbacks.forEach(callbackManager::addCallback);
    }

    @Override
    public SettingManager getManager(Player player) {
        return me.anxuiz.settings.bukkit.PlayerSettings.getManager(player);
    }

    @Override
    public SettingManager getManager(User user) {
        final Player player = onlinePlayers.find(user);
        return player != null ? getManager(player)
                              : new UserSettingManager(user);
    }

    /**
     * On login, copy settings from the player's {@link User} document
     * to their PlayerSettingManager.
     */
    @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
    public void loadSettings(UserLoginEvent event) {
        new UserSettingManager(event.getUser()).copyTo(getManager(event.getPlayer()));
    }

    /**
     * Push setting changes to the API
     */
    private class Callback extends PlayerSettingCallback {
        @Override
        public void notifyChange(@Nonnull Player player, @Nonnull Setting setting, @Nullable Object oldValue, @Nullable Object newValue) {
            userService.changeSetting(userStore.getUser(player), new ChangeSettingRequest() {
                @Override public String profile() {
                    return localServer.settings_profile();
                }

                @Override public String setting() {
                    return setting.getName();
                }

                @Override public String value() {
                    return newValue == null ? null : setting.getType().serialize(newValue);
                }
            });
        }
    }

    /**
     * Read settings directly from a {@link User} document.
     *
     * This is read-only, write methods will throw an exception.
     */
    private class UserSettingManager extends AbstractSettingManager {

        final User user;

        UserSettingManager(User user) {
            this.user = user;
        }

        Map<String, String> profile() {
            return Optional.ofNullable(user.mc_settings_by_profile()
                                           .get(localServer.settings_profile()))
                           .orElseGet(ImmutableMap::of);
        }

        void copyTo(SettingManager that) {
            final Map<String, String> profile = profile();
            settingRegistry.getSettings().forEach(setting -> {
                that.setValue(setting, getRawValue(setting, profile), false);
            });
        }

        @Nullable Object getRawValue(Setting setting, Map<String, String> profile) {
            final String text = profile.get(setting.getName());
            if(text == null) return null;

            try {
                return setting.getType().parse(text);
            } catch(TypeParseException e) {
                return null;
            }
        }

        @Override
        public @Nullable Object getRawValue(Setting setting) {
            return getRawValue(setting, profile());
        }

        @Override
        protected void setRawValue(Setting setting, Object value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public SettingCallbackManager getCallbackManager() {
            throw new UnsupportedOperationException();
        }
    }
}
