package com.sedmelluq.lava.player.extras.stream;

import com.sedmelluq.discord.lavaplayer.filter.PcmFilterFactory;
import com.sedmelluq.discord.lavaplayer.player.AudioPlayer;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEvent;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventAdapter;
import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener;
import com.sedmelluq.discord.lavaplayer.player.event.PlayerPauseEvent;
import com.sedmelluq.discord.lavaplayer.player.event.PlayerResumeEvent;
import com.sedmelluq.discord.lavaplayer.player.event.TrackEndEvent;
import com.sedmelluq.discord.lavaplayer.player.event.TrackExceptionEvent;
import com.sedmelluq.discord.lavaplayer.player.event.TrackStartEvent;
import com.sedmelluq.discord.lavaplayer.player.event.TrackStuckEvent;
import com.sedmelluq.discord.lavaplayer.tools.FriendlyException;
import com.sedmelluq.discord.lavaplayer.track.AudioTrack;
import com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason;
import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame;
import com.sedmelluq.discord.lavaplayer.track.playback.MutableAudioFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;

import static com.sedmelluq.discord.lavaplayer.track.AudioTrackEndReason.REPLACED;

public class StreamAudioPlayer implements AudioPlayer {
  private static final Logger log = LoggerFactory.getLogger(StreamAudioPlayer.class);

  private final AudioPlayer fallback;
  private final StreamAudioPlayerManager manager;
  private final Object lock;
  private final List<AudioEventListener> listeners;
  private final DetachListener detachListener;
  private StreamInstance.Cursor streamCursor;

  public StreamAudioPlayer(AudioPlayer fallback, StreamAudioPlayerManager manager) {
    this.fallback = fallback;
    this.manager = manager;
    this.lock = new Object();
    this.listeners = new ArrayList<>();
    this.detachListener = new DetachListener();

    fallback.addListener(new StreamEventListener());
  }

  @Override
  public AudioTrack getPlayingTrack() {
    synchronized (lock) {
      if (streamCursor != null) {
        return streamCursor.getTrack();
      } else {
        return fallback.getPlayingTrack();
      }
    }
  }

  @Override
  public void playTrack(AudioTrack track) {
    startTrack(track, false);
  }

  @Override
  public boolean startTrack(AudioTrack track, boolean noInterrupt) {
    if (track == null) {
      stopTrack();
    } else {
      synchronized (lock) {
        AudioTrack previousTrack = getPlayingTrack();

        if (noInterrupt && previousTrack != null) {
          return false;
        }

        if (previousTrack != null) {
          if (streamCursor == null) {
            fallback.stopTrack();
          } else {
            detachStream();
          }

          dispatchEvent(new TrackEndEvent(this, previousTrack, REPLACED));
        }

        streamCursor = manager.openTrack(track, detachListener);

        if (streamCursor == null) {
          fallback.startTrack(track, false);
        }

        dispatchEvent(new TrackStartEvent(this, track));
      }
    }

    return true;
  }

  @Override
  public void stopTrack() {
    synchronized (lock) {
      if (streamCursor != null) {
        streamCursor.close();
        streamCursor = null;
      }

      fallback.stopTrack();
    }
  }

  @Override
  public int getVolume() {
    return fallback.getVolume();
  }

  @Override
  public void setVolume(int volume) {
    fallback.setVolume(volume);
  }

  @Override
  public void setFilterFactory(PcmFilterFactory factory) {
    fallback.setFilterFactory(factory);
  }

  @Override
  public void setFrameBufferDuration(Integer duration) {
    fallback.setFrameBufferDuration(duration);
  }

  @Override
  public boolean isPaused() {
    return fallback.isPaused();
  }

  @Override
  public void setPaused(boolean value) {
    fallback.setPaused(value);
  }

  @Override
  public void destroy() {
    synchronized (lock) {
      if (streamCursor != null) {
        streamCursor.close();
        streamCursor = null;
      }

      fallback.destroy();
    }
  }

  @Override
  public void addListener(AudioEventListener listener) {
    synchronized (lock) {
      listeners.add(listener);
    }
  }

  @Override
  public void removeListener(AudioEventListener listener) {
    synchronized (lock) {
      listeners.removeIf(audioEventListener -> audioEventListener == listener);
    }
  }

  @Override
  public void checkCleanup(long threshold) {

  }

  @Override
  public AudioFrame provide() {
    synchronized (lock) {
      if (streamCursor != null) {
        AudioFrame frame = streamCursor.provide();

        if (frame == null) {
          if (streamCursor.getTrack() == null) {
            detachStream();
          } else {
            return null;
          }
        } else {
          return frame;
        }
      }

      return fallback.provide();
    }
  }

  @Override
  public AudioFrame provide(long timeout, TimeUnit unit) throws TimeoutException, InterruptedException {
    synchronized (lock) {
      if (streamCursor != null) {
        throw new UnsupportedOperationException();
      }

      return fallback.provide(timeout, unit);
    }
  }

  @Override
  public boolean provide(MutableAudioFrame targetFrame) {
    synchronized (lock) {
      if (streamCursor != null) {
        throw new UnsupportedOperationException();
      }

      return fallback.provide(targetFrame);
    }
  }

  @Override
  public boolean provide(MutableAudioFrame targetFrame, long timeout, TimeUnit unit) throws TimeoutException,
      InterruptedException {

    synchronized (lock) {
      if (streamCursor != null) {
        throw new UnsupportedOperationException();
      }

      return fallback.provide(targetFrame, timeout, unit);
    }
  }

  private void detachStream() {
    if (streamCursor != null) {
      streamCursor.close();
      streamCursor = null;
    }
  }

  private void dispatchEvent(AudioEvent event) {
    synchronized (lock) {
      for (AudioEventListener listener : listeners) {
        try {
          listener.onEvent(event);
        } catch (Exception e) {
          log.error("Handler of event {} threw an exception.", event, e);
        }
      }
    }
  }

  private class DetachListener implements Consumer<StreamInstance.Cursor> {

    @Override
    public void accept(StreamInstance.Cursor cursor) {
      synchronized (lock) {
        if (streamCursor == cursor) {
          detachStream();
        }
      }
    }
  }

  private class StreamEventListener extends AudioEventAdapter {
    @Override
    public void onPlayerPause(AudioPlayer player) {
      dispatchEvent(new PlayerPauseEvent(StreamAudioPlayer.this));
    }

    @Override
    public void onPlayerResume(AudioPlayer player) {
      dispatchEvent(new PlayerResumeEvent(StreamAudioPlayer.this));
    }

    @Override
    public void onTrackStart(AudioPlayer player, AudioTrack track) {
      log.debug("Received start event from delegate player for track {}.", track.getIdentifier());
    }

    @Override
    public void onTrackEnd(AudioPlayer player, AudioTrack track, AudioTrackEndReason endReason) {
      if (endReason.mayStartNext || endReason == AudioTrackEndReason.CLEANUP) {
        dispatchEvent(new TrackEndEvent(StreamAudioPlayer.this, track, endReason));
      }
    }

    @Override
    public void onTrackException(AudioPlayer player, AudioTrack track, FriendlyException exception) {
      dispatchEvent(new TrackExceptionEvent(StreamAudioPlayer.this, track, exception));
    }

    @Override
    public void onTrackStuck(AudioPlayer player, AudioTrack track, long thresholdMs) {
      dispatchEvent(new TrackStuckEvent(StreamAudioPlayer.this, track, thresholdMs));
    }
  }
}