package andesite.node;

import andesite.node.event.EventBuffer;
import andesite.node.event.EventDispatcherImpl;
import andesite.node.handler.RequestHandler;
import andesite.node.handler.RestHandler;
import andesite.node.handler.SingyeongHandler;
import andesite.node.player.Player;
import andesite.node.plugin.PluginManager;
import andesite.node.send.AudioHandler;
import andesite.node.send.MagmaHandler;
import andesite.node.util.ConfigUtil;
import andesite.node.util.FilterUtil;
import andesite.node.util.Init;
import andesite.node.util.LazyInit;
import com.github.natanbc.nativeloader.NativeLibLoader;
import com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.source.AudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.bandcamp.BandcampAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.beam.BeamAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.http.HttpAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.soundcloud.SoundCloudAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.twitch.TwitchStreamAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.vimeo.VimeoAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.source.youtube.YoutubeAudioSourceManager;
import com.sedmelluq.discord.lavaplayer.track.playback.NonAllocatingAudioFrameBuffer;
import com.typesafe.config.Config;
import io.vertx.core.Vertx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.lang.ref.Cleaner;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Andesite implements NodeState {
    private static final Logger log = LoggerFactory.getLogger(Andesite.class);
    private static final LazyInit<Cleaner> CLEANER = new LazyInit<>(() -> Cleaner.create(r -> {
        var t = new Thread(r, "Andesite-Cleaner");
        t.setDaemon(true);
        return t;
    }));
    private static final Map<String, Supplier<AudioSourceManager>> SOURCE_MANAGERS = Map.of(
            "bandcamp", BandcampAudioSourceManager::new,
            "beam", BeamAudioSourceManager::new,
            "http", HttpAudioSourceManager::new,
            "local", LocalAudioSourceManager::new,
            "soundcloud", SoundCloudAudioSourceManager::new,
            "twitch", TwitchStreamAudioSourceManager::new,
            "vimeo", VimeoAudioSourceManager::new,
            "youtube", YoutubeAudioSourceManager::new
    );
    private static final Set<String> DISABLED_BY_DEFAULT = Set.of("http", "local");
    
    private final PluginManager pluginManager = new PluginManager(this);
    private final AtomicLong nextBufferId = new AtomicLong();
    private final Map<Long, EventBuffer> buffers = new ConcurrentHashMap<>();
    private final Map<String, Map<String, Player>> players = new ConcurrentHashMap<>();
    private final AudioPlayerManager playerManager = new DefaultAudioPlayerManager();
    private final AudioPlayerManager pcmPlayerManager = new DefaultAudioPlayerManager();
    private final EventDispatcherImpl dispatcher = new EventDispatcherImpl(this);
    private final Vertx vertx;
    private final Config rootConfig;
    private final AudioHandler audioHandler;
    private final RequestHandler handler;
    private final Set<String> enabledSources;
    
    private Andesite(@Nonnull Vertx vertx, @Nonnull Config rootConfig) throws IOException {
        var config = rootConfig.getConfig("andesite");
        var plugins = new File("plugins").listFiles();
        if(plugins != null) {
            for(var f : plugins) {
                log.info("Loading plugins from {}", f);
                pluginManager.load(f);
            }
        }
        var extraPluginLocations = config.getStringList("extra-plugins");
        for(var f : extraPluginLocations) {
            log.info("Loading plugins from {}", f);
            pluginManager.load(new File(f));
        }
        this.vertx = vertx;
        this.rootConfig = pluginManager.applyPluginDefaults(rootConfig);
        this.audioHandler = createAudioHandler(config);
        this.handler = new RequestHandler(this);
        pluginManager.init();
        pluginManager.configurePlayerManager(playerManager);
        pluginManager.configurePlayerManager(pcmPlayerManager);
        this.enabledSources = SOURCE_MANAGERS.keySet().stream()
                .filter(key -> {
                    if(config.hasPath("source." + key)) {
                        return config.getBoolean("source." + key);
                    }
                    return !DISABLED_BY_DEFAULT.contains(key);
                })
                .peek(key -> playerManager.registerSourceManager(SOURCE_MANAGERS.get(key).get()))
                .peek(key -> pcmPlayerManager.registerSourceManager(SOURCE_MANAGERS.get(key).get()))
                .collect(Collectors.toSet());
        
        configureYt(playerManager, config);
        configureYt(pcmPlayerManager, config);
        
        log.info("Enabled default sources: {}", enabledSources);
        //we need to set the cleanup to basically never run so mixer players aren't destroyed without need.
        playerManager.setPlayerCleanupThreshold(Long.MAX_VALUE);
        playerManager.getConfiguration().setFilterHotSwapEnabled(true);
        if(config.getBoolean("lavaplayer.non-allocating")) {
            playerManager.getConfiguration().setFrameBufferFactory(NonAllocatingAudioFrameBuffer::new);
        }
        pcmPlayerManager.setPlayerCleanupThreshold(Long.MAX_VALUE);
        pcmPlayerManager.getConfiguration().setOutputFormat(StandardAudioDataFormats.DISCORD_PCM_S16_BE);
        pcmPlayerManager.getConfiguration().setFilterHotSwapEnabled(true);
        pcmPlayerManager.getConfiguration().setFrameBufferFactory(NonAllocatingAudioFrameBuffer::new);
    
        playerManager.setFrameBufferDuration(config.getInt("lavaplayer.frame-buffer-duration"));
        pcmPlayerManager.setFrameBufferDuration(config.getInt("lavaplayer.frame-buffer-duration"));
    }
    
    @Nonnull
    @CheckReturnValue
    public PluginManager pluginManager() {
        return pluginManager;
    }
    
    @Nonnull
    @CheckReturnValue
    public Set<String> enabledSources() {
        return enabledSources;
    }
    
    @CheckReturnValue
    public long nextConnectionId() {
        return nextBufferId.incrementAndGet();
    }
    
    @Nonnull
    @CheckReturnValue
    public EventBuffer createEventBuffer(long id) {
        var buffer = new EventBuffer();
        buffers.put(id, buffer);
        return buffer;
    }
    
    @Nullable
    public EventBuffer removeEventBuffer(long id) {
        return buffers.remove(id);
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public Config config() {
        return rootConfig;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public Vertx vertx() {
        return vertx;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public RequestHandler requestHandler() {
        return handler;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public AudioPlayerManager audioPlayerManager() {
        return playerManager;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public AudioPlayerManager pcmAudioPlayerManager() {
        return pcmPlayerManager;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public EventDispatcherImpl dispatcher() {
        return dispatcher;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public AudioHandler audioHandler() {
        return audioHandler;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public Map<String, Player> playerMap(@Nonnull String userId) {
        return players.computeIfAbsent(userId, __ -> new ConcurrentHashMap<>());
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public Player getPlayer(@Nonnull String userId, @Nonnull String guildId) {
        return playerMap(userId).computeIfAbsent(guildId, __ -> {
            var player = new Player(this, guildId, userId);
            dispatcher.onPlayerCreated(userId, guildId, player);
            return player;
        });
    }
    
    @Nullable
    @CheckReturnValue
    @Override
    public Player getExistingPlayer(@Nonnull String userId, @Nonnull String guildId) {
        var map = players.get(userId);
        return map == null ? null : map.get(guildId);
    }
    
    @Nullable
    @Override
    public Player removePlayer(@Nonnull String userId, @Nonnull String guildId) {
        var map = players.get(userId);
        if(map == null) return null;
        var player = map.remove(guildId);
        if(player != null) {
            dispatcher.onPlayerDestroyed(userId, guildId, player);
        }
        return player;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public Stream<Player> allPlayers() {
        return players.values().stream().flatMap(m -> m.values().stream());
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public Cleaner cleaner() {
        return CLEANER.get();
    }
    
    @Nonnull
    @CheckReturnValue
    private AudioHandler createAudioHandler(@Nonnull Config config) {
        var handlerName = config.getString("audio-handler");
        //noinspection SwitchStatementWithTooFewBranches
        switch(handlerName) {
            case "magma":
                return new MagmaHandler(this);
            default:
                return pluginManager.loadHandler(AudioHandler.class, handlerName);
        }
    }
    
    private static void configureYt(@Nonnull AudioPlayerManager manager, @Nonnull Config config) {
        var yt = manager.source(YoutubeAudioSourceManager.class);
        if(yt == null) {
            return;
        }
        yt.setPlaylistPageCount(config.getInt("lavaplayer.youtube.max-playlist-page-count"));
        yt.setMixLoaderMaximumPoolSize(config.getInt("lavaplayer.youtube.mix-loader-max-pool-size"));
    }
    
    public static void main(String[] args) throws IOException {
        try {
            log.info("System info: {}", NativeLibLoader.loadSystemInfo());
        } catch(Throwable t) {
            String message = "Unable to load system info.";
            if(t instanceof UnsatisfiedLinkError || (t instanceof RuntimeException && t.getCause() instanceof UnsatisfiedLinkError)) {
                message += " This is not an error.";
            }
            log.warn(message, t);
        }
        log.info("Starting andesite version {}, commit {}", Version.VERSION, Version.COMMIT);
    
        var andesite = createAndesite();
        var config = andesite.config().getConfig("andesite");
        Init.postInit(andesite);
        //NOTE: use the bitwise or operator, as it forces evaluation of all elements
        if(!(RestHandler.setup(andesite)
                | SingyeongHandler.setup(andesite)
                | andesite.pluginManager().startListeners())) {
            log.error("No handlers enabled, aborting");
            System.exit(-1);
        }
        log.info("Handlers: REST {}, WebSocket {}, Singyeong {}",
                config.getBoolean("transport.http.rest") ? "enabled" : "disabled",
                config.getBoolean("transport.http.ws") ? "enabled" : "disabled",
                config.getBoolean("transport.singyeong.enabled") ? "enabled" : "disabled"
        );
        log.info("Timescale {}", FilterUtil.TIMESCALE_AVAILABLE ? "available" : "unavailable");
    }
    
    @Nonnull
    @CheckReturnValue
    private static Andesite createAndesite() throws IOException {
        var rootConfig = ConfigUtil.load();
        var config = rootConfig.getConfig("andesite");
        Init.preInit(config);
        return new Andesite(Vertx.vertx(), rootConfig);
    }
}