package andesite.node.player;

import andesite.node.NodeState;
import andesite.node.player.filter.FilterChainConfiguration;
import com.sedmelluq.discord.lavaplayer.format.StandardAudioDataFormats;
import com.sedmelluq.discord.lavaplayer.format.transcoder.OpusChunkEncoder;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager;
import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame;
import io.vertx.core.json.JsonObject;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.time.Instant;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class TrackMixer implements AndesiteTrackMixer {
    private final Map<String, Player> players = new ConcurrentHashMap<>();
    private final ShortBuffer mixBuffer = ByteBuffer.allocateDirect(StandardAudioDataFormats.DISCORD_PCM_S16_BE.maximumChunkSize())
            .order(ByteOrder.nativeOrder())
            .asShortBuffer();
    private final ByteBuffer outputBuffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_OPUS.maximumChunkSize());
    
    private final AudioPlayerManager playerManager;
    private final AndesitePlayer parent;
    private final OpusChunkEncoder encoder;
    
    public TrackMixer(AudioPlayerManager playerManager, AndesitePlayer parent) {
        this.playerManager = playerManager;
        this.encoder = new OpusChunkEncoder(playerManager.getConfiguration(), StandardAudioDataFormats.DISCORD_OPUS);
        this.parent = parent;
    }
    
    @SuppressWarnings("unchecked")
    @Nonnull
    @CheckReturnValue
    @Override
    public Map<String, MixerPlayer> players() {
        return (Map) players;
    }
    
    @Nonnull
    @CheckReturnValue
    @Override
    public MixerPlayer getPlayer(@Nonnull String key) {
        return players.computeIfAbsent(key, k -> new Player(playerManager.createPlayer(), parent, k));
    }
    
    @Override
    public MixerPlayer getExistingPlayer(@Nonnull String key) {
        return players.get(key);
    }
    
    @Override
    public void removePlayer(@Nonnull String key) {
        var p = players.remove(key);
        if(p != null) {
            p.player.destroy();
        }
    }
    
    @CheckReturnValue
    @Override
    public boolean canProvide() {
        var v = false;
        for(var p : players.values()) {
            v |= p.tryProvide();
        }
        players.values().removeIf(p -> {
            var notPlaying = p.player.getPlayingTrack() == null && p.framesWithoutProvide > 250; //5 seconds
            if(notPlaying) {
                p.player.destroy();
            }
            return notPlaying;
        });
        return v;
    }
    
    @CheckReturnValue
    @Nonnull
    @Override
    public ByteBuffer provide() {
        var buffer = mixBuffer; //avoid getfield opcode
        buffer.clear().position(0);
        for(var p : players.values()) {
            if(p.provided) {
                var s = p.buffer.position(0).asShortBuffer();
                
                //http://atastypixel.com/blog/how-to-mix-audio-samples-properly-on-ios/
                for(int i = 0; i < s.capacity(); i++) {
                    var a = buffer.get(i);
                    var b = s.get(i);
                    if(a < 0 && b < 0) {
                        buffer.put(i, (short) ((a + b) - ((a * b) / Short.MIN_VALUE)));
                    } else if(a > 0 && b > 0) {
                        buffer.put(i, (short) ((a + b) - ((a * b) / Short.MAX_VALUE)));
                    } else {
                        buffer.put(i, (short) (a + b));
                    }
                }
            }
        }
        buffer.flip();
        
        encoder.encode(buffer, outputBuffer.position(0).limit(outputBuffer.capacity()));
        buffer.flip();
        return outputBuffer;
    }
    
    @Override
    public void close() {
        players.values().forEach(p -> p.player.destroy());
        encoder.close();
    }
    
    private static class Player implements MixerPlayer {
        private final ByteBuffer buffer = ByteBuffer.allocate(StandardAudioDataFormats.DISCORD_PCM_S16_BE.maximumChunkSize())
                .order(ByteOrder.BIG_ENDIAN);
        private final MutableAudioFrame frame = new MutableAudioFrame();
        private final FrameLossTracker frameLossTracker = new FrameLossTracker();
        private final FilterChainConfiguration filterConfig = new FilterChainConfiguration();
        private final AudioPlayer player;
        private final AndesitePlayer parent;
        private final String key;
        private boolean provided;
        private int framesWithoutProvide;
        
        Player(AudioPlayer player, AndesitePlayer parent, String key) {
            this.player = player;
            this.parent = parent;
            this.key = key;
            frame.setBuffer(buffer);
            buffer.limit(frame.getDataLength());
            this.player.addListener(frameLossTracker);
        }
        
        boolean tryProvide() {
            provided = player.provide(frame);
            if(provided) {
                framesWithoutProvide = 0;
                frameLossTracker.onSuccess();
            } else {
                framesWithoutProvide++;
                frameLossTracker.onFail();
            }
            return provided;
        }
        
        @Nonnull
        @Override
        public NodeState node() {
            return parent.node();
        }
        
        @Nonnull
        @Override
        public String userId() {
            return parent.userId();
        }
        
        @Nonnull
        @Override
        public String guildId() {
            return parent.guildId();
        }
        
        @Nonnull
        @Override
        public FrameLossCounter frameLossCounter() {
            return frameLossTracker;
        }
        
        @Nonnull
        @CheckReturnValue
        @Override
        public FilterChainConfiguration filterConfig() {
            return filterConfig;
        }
        
        @Nonnull
        @CheckReturnValue
        @Override
        public AudioPlayer audioPlayer() {
            return player;
        }
        
        @Nonnull
        @Override
        public JsonObject encodeState() {
            var track = player.getPlayingTrack();
            return new JsonObject()
                    .put("time", String.valueOf(Instant.now().toEpochMilli()))
                    .put("position", track == null ? null : track.getPosition())
                    .put("paused", player.isPaused())
                    .put("volume", player.getVolume())
                    .put("filters", filterConfig.encode())
                    .put("frame", new JsonObject()
                            .put("loss", frameLossTracker.lastMinuteLoss().sum())
                            .put("success", frameLossTracker.lastMinuteSuccess().sum())
                            .put("usable", frameLossTracker.isDataUsable())
                    );
        }
        
        @Nonnull
        @Override
        public AndesitePlayer parentPlayer() {
            return parent;
        }
        
        @Nonnull
        @Override
        public String key() {
            return key;
        }
    }
}