package tc.oc.commons.bukkit.broadcast;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.logging.Logger;
import java.util.stream.Stream;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import net.md_5.bungee.api.chat.BaseComponent;
import org.bukkit.command.ConsoleCommandSender;
import tc.oc.api.bukkit.users.OnlinePlayers;
import tc.oc.api.docs.Server;
import tc.oc.commons.bukkit.broadcast.model.BroadcastSchedule;
import tc.oc.commons.bukkit.broadcast.model.BroadcastSet;
import tc.oc.commons.bukkit.chat.ComponentRenderContext;
import tc.oc.commons.bukkit.localization.LocalizedMessageMap;
import tc.oc.commons.core.logging.Loggers;
import tc.oc.commons.core.plugin.PluginFacet;
import tc.oc.commons.core.scheduler.Scheduler;
import tc.oc.commons.core.scheduler.Task;
import tc.oc.commons.core.stream.Collectors;
import tc.oc.commons.core.util.Pair;
import tc.oc.parse.DocumentWatcher;

/**
 * Manages all broadcast messages
 */
@Singleton
public class BroadcastScheduler implements PluginFacet {

    // Configuration file, relative to config root
    private static final Path CONFIG_FILE = Paths.get("broadcasts.xml");

    private static final Path SOURCES_PATH = Paths.get("localized/broadcasts");

    // Base path for (untranslated) message sources, relative to config root
    private static final Path TRANSLATIONS_PATH = Paths.get("broadcasts");

    private final Logger logger;
    private final Path configPath;
    private final Path configFile;
    private final Scheduler scheduler;
    private final ConsoleCommandSender console;
    private final OnlinePlayers onlinePlayers;
    private final LocalizedMessageMap.Factory messageMapFactory;
    private final DocumentWatcher.Factory<List<BroadcastSchedule>> documentWatcherFactory;
    private final Server localServer;
    private final BroadcastFormatter formatter;
    private final ComponentRenderContext renderer;
    private final BroadcastSettings settings;

    private final Random random = new Random();

    private DocumentWatcher<List<BroadcastSchedule>> scheduleWatcher;
    private List<ScheduledTask> tasks = ImmutableList.of();

    @Inject BroadcastScheduler(Loggers loggers,
                               @Named("configuration") Path configPath,
                               Scheduler scheduler,
                               ConsoleCommandSender console,
                               OnlinePlayers onlinePlayers,
                               LocalizedMessageMap.Factory messageMapFactory,
                               DocumentWatcher.Factory<List<BroadcastSchedule>> documentWatcherFactory,
                               Server localServer,
                               BroadcastFormatter formatter,
                               ComponentRenderContext renderer,
                               BroadcastSettings settings) {

        this.logger = loggers.get(getClass());
        this.configPath = configPath;
        this.configFile = configPath.resolve(CONFIG_FILE);
        this.scheduler = scheduler;
        this.console = console;
        this.onlinePlayers = onlinePlayers;
        this.messageMapFactory = messageMapFactory;
        this.documentWatcherFactory = documentWatcherFactory;
        this.localServer = localServer;
        this.formatter = formatter;
        this.renderer = renderer;
        this.settings = settings;
    }

    @Override
    public void enable() {
        scheduleWatcher = documentWatcherFactory.create(configFile, schedule -> {
            tasks.forEach(ScheduledTask::cancel);
            tasks = schedule.map(doc -> ImmutableList.copyOf(Lists.transform(doc, ScheduledTask::new)))
                            .orElse(ImmutableList.of());
        });
    }

    @Override
    public void disable() {
        tasks.forEach(ScheduledTask::cancel);

        if(scheduleWatcher != null) {
            scheduleWatcher.cancel();
            scheduleWatcher = null;
        }
    }

    /**
     * Handles a single {@link BroadcastSchedule}
     */
    private class ScheduledTask {

        final BroadcastSchedule schedule;
        final Map<BroadcastSet, LocalizedMessageMap> messages;
        final Task task;

        ScheduledTask(BroadcastSchedule schedule) {
            logger.fine(() -> "Starting broadcast schedule " + schedule);

            this.schedule = schedule;
            this.messages = schedule.messages().stream().collect(Collectors.mappingTo(
                set -> messageMapFactory.create(configPath.resolve(SOURCES_PATH).resolve(set.path()),
                                                TRANSLATIONS_PATH.resolve(set.path())))
            );
            this.task = scheduler.createRepeatingTask(schedule.interval(), this::dispatch);
        }

        void dispatch() {
            if(!schedule.serverFilter().test(localServer)) return;

            final List<Pair<BroadcastSet, String>> choices = messages.entrySet()
                                                                     .stream()
                                                                     .flatMap(entry -> entry.getValue()
                                                                                            .keySet()
                                                                                            .stream()
                                                                                            .map(key -> Pair.of(entry.getKey(), key)))
                                                                     .collect(Collectors.toImmutableList());
            if(choices.isEmpty()) return;

            final Pair<BroadcastSet, String> choice = choices.get(random.nextInt(choices.size()));
            final BaseComponent message = formatter.broadcast(choice.first.prefix(),
                                                              messages.get(choice.first).get(choice.second));
            Stream.concat(Stream.of(console), onlinePlayers.all().stream()).forEach(viewer -> {
                if(settings.isVisible(choice.first.prefix(), viewer)) {
                    renderer.send(message, viewer);
                }
            });
        }

        void cancel() {
            task.cancel();
            messages.values().forEach(LocalizedMessageMap::disable);
        }
    }
}
