package org.bff.javampd.playlist;

import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import org.bff.javampd.album.MPDAlbum;
import org.bff.javampd.artist.MPDArtist;
import org.bff.javampd.command.CommandExecutor;
import org.bff.javampd.command.MPDCommand;
import org.bff.javampd.file.MPDFile;
import org.bff.javampd.genre.MPDGenre;
import org.bff.javampd.server.ServerStatus;
import org.bff.javampd.song.MPDSong;
import org.bff.javampd.song.SongDatabase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * MPDPlaylist represents a playlist controller to a MPD server. To obtain an instance of the class
 * you must use the <code>getMPDPlaylist</code> method from the {@link org.bff.javampd.server.MPD}
 * connection class.
 *
 * @author Bill
 */
public class MPDPlaylist implements Playlist {
  private static final Logger LOGGER = LoggerFactory.getLogger(MPDPlaylist.class);

  private int oldVersion = -1;
  private int version = -1;

  private final List<PlaylistChangeListener> listeners;
  private final SongDatabase songDatabase;
  private final ServerStatus serverStatus;
  private final CommandExecutor commandExecutor;
  private final PlaylistSongConverter songConverter;

  private PlaylistProperties playlistProperties;

  /**
   * Playlist constructor
   *
   * @param songDatabase the song database
   * @param serverStatus the server status
   * @param playlistProperties playlist properties
   * @param commandExecutor command runner
   * @param songConverter song marshaller
   */
  @Inject
  public MPDPlaylist(
      SongDatabase songDatabase,
      ServerStatus serverStatus,
      PlaylistProperties playlistProperties,
      CommandExecutor commandExecutor,
      PlaylistSongConverter songConverter) {
    this.songDatabase = songDatabase;
    this.serverStatus = serverStatus;
    this.playlistProperties = playlistProperties;
    this.commandExecutor = commandExecutor;
    this.songConverter = songConverter;
    this.listeners = new ArrayList<>();
    this.playlistProperties = new PlaylistProperties();
  }

  @Override
  public synchronized void addPlaylistChangeListener(PlaylistChangeListener pcl) {
    listeners.add(pcl);
  }

  @Override
  public synchronized void removePlaylistStatusChangedListener(PlaylistChangeListener pcl) {
    listeners.remove(pcl);
  }

  /**
   * Sends the appropriate {@link PlaylistChangeEvent} to all registered {@link
   * PlaylistChangeListener}.
   *
   * @param event the {@link PlaylistChangeEvent.Event} to send
   */
  protected synchronized void firePlaylistChangeEvent(PlaylistChangeEvent.Event event) {
    var pce = new PlaylistChangeEvent(this, event);

    for (PlaylistChangeListener pcl : listeners) {
      pcl.playlistChanged(pce);
    }
  }

  /**
   * Sends the appropriate {@link PlaylistChangeEvent} to all registered {@link
   * PlaylistChangeListener}.
   *
   * @param event the {@link PlaylistChangeEvent.Event} to send
   * @param name name of the added entity
   */
  protected synchronized void firePlaylistChangeEvent(
      PlaylistChangeEvent.Event event, String name) {
    var pce = new PlaylistChangeEvent(this, event, name);

    for (PlaylistChangeListener pcl : listeners) {
      pcl.playlistChanged(pce);
    }
  }

  @Override
  public void loadPlaylist(String playlistName) {
    String name = playlistName;
    if (name.endsWith(".m3u")) {
      name = name.substring(0, name.length() - 4);
    }

    commandExecutor.sendCommand(playlistProperties.getLoad(), name);
    updatePlaylist();
  }

  @Override
  public void addSong(MPDSong song) {
    addSong(song, true);
  }

  @Override
  public void addSong(MPDSong song, boolean fireEvent) {
    addSong(song.getFile(), fireEvent);
  }

  @Override
  public void addSong(String file) {
    this.addSong(file, true);
  }

  @Override
  public void addSong(String file, boolean fireEvent) {
    commandExecutor.sendCommand(playlistProperties.getAdd(), file);
    updatePlaylist();
    if (fireEvent) {
      firePlaylistChangeEvent(PlaylistChangeEvent.Event.SONG_ADDED, file);
    }
  }

  @Override
  public boolean addSongs(List<MPDSong> songList) {
    return addSongs(songList, true);
  }

  @Override
  public boolean addSongs(List<MPDSong> songList, boolean fireEvent) {
    commandExecutor.sendCommands(
        songList.stream()
            .map(song -> new MPDCommand(playlistProperties.getAdd(), song.getFile()))
            .toList());

    int oldCount = songList.size();
    updatePlaylist();

    if (fireEvent) {
      firePlaylistChangeEvent(
          PlaylistChangeEvent.Event.MULTIPLE_SONGS_ADDED, Integer.toString(songList.size()));
    }

    return oldCount < songList.size();
  }

  @Override
  public void addFileOrDirectory(MPDFile file) {
    commandExecutor.sendCommand(playlistProperties.getAdd(), file.getPath());
    updatePlaylist();
    firePlaylistChangeEvent(PlaylistChangeEvent.Event.FILE_ADDED, file.getPath());
  }

  @Override
  public void removeSong(MPDPlaylistSong song) {
    if (song.getId() > -1) {
      updatePlaylist();
      firePlaylistChangeEvent(PlaylistChangeEvent.Event.SONG_DELETED, song.getName());
      commandExecutor.sendCommand(playlistProperties.getRemoveId(), song.getId());
    } else {
      removeSong(song.getPosition());
    }
  }

  @Override
  public void removeSong(int position) {
    if (position > -1) {
      commandExecutor.sendCommand(playlistProperties.getRemove(), position);
      updatePlaylist();
      firePlaylistChangeEvent(PlaylistChangeEvent.Event.SONG_DELETED);
    }
  }

  @Override
  public MPDPlaylistSong getCurrentSong() {
    List<MPDPlaylistSong> songs =
        convertResponseToSong(commandExecutor.sendCommand(playlistProperties.getCurrentSong()));
    return songs.isEmpty() ? null : songs.getFirst();
  }

  private List<MPDPlaylistSong> convertResponseToSong(List<String> response) {
    return songConverter.convertResponseToSongs(response);
  }

  @Override
  public void clearPlaylist() {
    commandExecutor.sendCommand(playlistProperties.getClear());
    updatePlaylist();
    firePlaylistChangeEvent(PlaylistChangeEvent.Event.PLAYLIST_CLEARED);
  }

  @Override
  public void deletePlaylist(MPDSavedPlaylist playlist) {
    deletePlaylist(playlist.getName());
  }

  @Override
  public void deletePlaylist(String playlistName) {
    commandExecutor.sendCommand(playlistProperties.getDelete(), playlistName);
    firePlaylistChangeEvent(PlaylistChangeEvent.Event.PLAYLIST_DELETED);
  }

  @Override
  public void move(MPDPlaylistSong song, int to) {
    if (song.getId() > -1) {
      commandExecutor.sendCommand(playlistProperties.getMoveId(), song.getId(), to);
    } else if (song.getPosition() > -1) {
      commandExecutor.sendCommand(playlistProperties.getMove(), song.getPosition(), to);
    }

    updatePlaylist();
  }

  @Override
  public void shuffle() {
    commandExecutor.sendCommand(playlistProperties.getShuffle());
    updatePlaylist();
  }

  @Override
  public void swap(MPDPlaylistSong song1, MPDPlaylistSong song2) {
    if (song1.getId() > -1 && song2.getId() > -1) {
      commandExecutor.sendCommand(playlistProperties.getSwapId(), song1.getId(), song2.getId());
    } else if (song1.getPosition() > -1 && song2.getPosition() > -1) {
      commandExecutor.sendCommand(
          playlistProperties.getSwap(), song1.getPosition(), song2.getPosition());
    }
    updatePlaylist();
  }

  @Override
  public boolean savePlaylist(String playlistName) {
    if (playlistName != null) {
      commandExecutor.sendCommand(playlistProperties.getSave(), playlistName);
      firePlaylistChangeEvent(PlaylistChangeEvent.Event.PLAYLIST_SAVED);

      return true;
    } else {
      LOGGER.error("Playlist not saved since name was null");
      return false;
    }
  }

  private void updatePlaylist() {
    int v = getPlaylistVersion();
    setVersion(v);

    if (v != oldVersion) {
      oldVersion = getVersion();
      firePlaylistChangeEvent(PlaylistChangeEvent.Event.PLAYLIST_CHANGED);
    }
  }

  private int getPlaylistVersion() {
    return serverStatus.getPlaylistVersion();
  }

  /**
   * Returns the list of songs in the playlist.
   *
   * @return the list of songs
   */
  private List<MPDPlaylistSong> listSongs() {
    return convertResponseToSong(commandExecutor.sendCommand(playlistProperties.getInfo()));
  }

  @Override
  public void insertAlbum(MPDArtist artist, MPDAlbum album) {
    for (MPDSong song : songDatabase.findAlbumByArtist(artist, album)) {
      addSong(song, false);
    }
    firePlaylistChangeEvent(PlaylistChangeEvent.Event.ALBUM_ADDED, album.getName());
  }

  @Override
  public void insertAlbum(String artist, String album) {
    for (MPDSong song : songDatabase.findAlbumByArtist(artist, album)) {
      addSong(song, false);
    }
    firePlaylistChangeEvent(PlaylistChangeEvent.Event.ALBUM_ADDED, album);
  }

  @Override
  public void insertAlbum(MPDAlbum album) {
    for (MPDSong song : songDatabase.findAlbum(album)) {
      addSong(song, false);
    }
    firePlaylistChangeEvent(PlaylistChangeEvent.Event.ALBUM_ADDED, album.getName());
  }

  @Override
  public void insertAlbum(String album) {
    for (MPDSong song : songDatabase.findAlbum(album)) {
      addSong(song, false);
    }
    firePlaylistChangeEvent(PlaylistChangeEvent.Event.ALBUM_ADDED, album);
  }

  @Override
  public void removeAlbum(MPDArtist artist, MPDAlbum album) {
    removeAlbum(artist.name(), album.getName());
  }

  @Override
  public void removeAlbum(String artistName, String albumName) {
    List<MPDPlaylistSong> removeList =
        getSongList().stream()
            .filter(
                song ->
                    song.getArtistName().equals(artistName)
                        && song.getAlbumName().equals(albumName))
            .toList();

    removeList.forEach(this::removeSong);
  }

  @Override
  public void insertArtist(MPDArtist artist) {
    insertArtist(artist.name());
  }

  @Override
  public void insertArtist(String artistName) {
    for (MPDSong song : songDatabase.findArtist(artistName)) {
      addSong(song, false);
    }

    firePlaylistChangeEvent(PlaylistChangeEvent.Event.ARTIST_ADDED, artistName);
  }

  @Override
  public void insertGenre(MPDGenre genre) {
    insertGenre(genre.name());
  }

  @Override
  public void insertGenre(String genreName) {
    for (MPDSong song : songDatabase.findGenre(genreName)) {
      addSong(song, false);
    }
    firePlaylistChangeEvent(PlaylistChangeEvent.Event.GENRE_ADDED, genreName);
  }

  @Override
  public void removeGenre(MPDGenre genre) {
    removeGenre(genre.name());
  }

  @Override
  public void removeGenre(String genreName) {
    List<MPDPlaylistSong> removeList =
        getSongList().stream().filter(song -> song.getGenre().equals(genreName)).toList();

    removeList.forEach(this::removeSong);
  }

  @Override
  public void insertYear(String year) {
    for (MPDSong song : songDatabase.findYear(year)) {
      addSong(song, false);
    }

    firePlaylistChangeEvent(PlaylistChangeEvent.Event.YEAR_ADDED, year);
  }

  @Override
  public void removeYear(String year) {
    List<MPDPlaylistSong> removeList = new ArrayList<>();
    for (MPDPlaylistSong song : getSongList()) {
      if (song.getDate().equals(year)) {
        removeList.add(song);
      }
    }

    removeList.forEach(this::removeSong);
  }

  @Override
  public void removeArtist(MPDArtist artist) {
    removeArtist(artist.name());
  }

  @Override
  public void removeArtist(String artistName) {
    List<MPDPlaylistSong> removeList = new ArrayList<>();
    for (MPDPlaylistSong song : getSongList()) {
      if (song.getArtistName().equals(artistName)) {
        removeList.add(song);
      }
    }

    removeList.forEach(this::removeSong);
  }

  @Override
  public int getVersion() {
    return version;
  }

  private void setVersion(int version) {
    this.version = version;
  }

  @Override
  public List<MPDPlaylistSong> getSongList() {
    return listSongs();
  }

  @Override
  public void swap(MPDPlaylistSong song, int i) {
    commandExecutor.sendCommand(playlistProperties.getSwapId(), song.getId(), i);
    updatePlaylist();
  }
}
