public interface PlaylistPositionSync {

  /**
   * Returns updated playlist position
   * @return
   */
  public Position getPlaylistPosition();
} public class TrackAndPosition {

  private Track track;

  private TrackPosition position;

  public TrackAndPosition(Track track, TrackPosition position) {
    this.track = track;
    this.position = position;
  }

  public Track getTrack() {
    return track;
  }

  public TrackPosition getPosition() {
    return position;
  }
} public abstract class AbstractRoomHandler<T> implements RoomHandler<T> {

  protected void sendAll(RoomContext ctx, T msg) {
    sendAll(ctx, null, msg);
  }

  protected void send(final RoomContext ctx, final Channel channel, final T msg) {
    ctx.executor().execute(new Runnable() {
      
      public void run() {
        ChannelFuture future = channel.writeAndFlush(msg);

        future.addListener(new ChannelFutureListener() {

          public void operationComplete(ChannelFuture f) throws Exception {
            if (!f.isSuccess()) {
              RoomContext newCtx = ctx.forChannel(channel);
              handleWriteFailure(newCtx);
            }
          }
        });
      }
    });
  }

  protected void send(RoomContext ctx, T msg) {
    send(ctx, ctx.channel(), msg);
  }

  protected void sendAll(RoomContext ctx, Channel exclude, final T msg) {

    for (final Channel channel : ctx.channels()) {
      if (channel.equals(exclude)) {
        continue;
      }

      send(ctx, channel, msg);
    }
  }

  public abstract void handleWriteFailure(RoomContext ctx);

  public abstract void handleMessage(RoomContext ctx, T message);
} public class ChannelJoined extends ParticipantJoined {

  private List<Participant> participants;

  private List<Track> tracks;

  private Room room;

  private Date time;

  public ChannelJoined(Participant user, List<Participant> participants, List<Track> tracks, Room room) {
    super(user);

    this.tracks = tracks;
    this.participants = participants;

    this.room = room;
    this.time = new Date();
  }

  public ChannelJoined() {
  }

  public List<Participant> getParticipants() {
    return participants;
  }

  public Room getRoom() {
    return room;
  }

  public void setParticipants(List<Participant> participants) {
    this.participants = participants;
  }

  public void setRoom(Room room) {
    this.room = room;
  }

  public List<Track> getTracks() {
    return tracks;
  }

  public void setTracks(List<Track> tracks) {
    this.tracks = tracks;
  }

  public Date getTime() {
    return time;
  }

  public void setTime(Date time) {
    this.time = time;
  }
} public class Disco {

  public static Datastore DATA_STORE;

  private static String MONGO_DB_HOST = "localhost";
  private static String MONGO_DB_NAME = "silent-disco";

  public static Track getTrack(String id) {
    return getDatastore().get(Track.class, new ObjectId(id));
  }

  public static Room getRoom(String name) {
    Room room = getRoomByNameQuery(name).get();

    if (room == null) {
      room = new Room(name);
      getDatastore().save(room);
    }

    return room;
  }

  public static void stopPlay(String trackId) {
    Track track = getTrack(trackId);
    if (track == null) {
      throw new RuntimeException("#stopPlay(): track not found with id " + trackId);
    }

    updatePlaylistPosition(track.getRoomName(), new Position(track.getTrackId(), 0, Status.STOPPED));
  }

  public static void startPlay(String trackId, int position) {

    Track track = getTrack(trackId);
    if (track == null) {
      throw new RuntimeException("#startPlay(): track not found with id " + trackId);
    }

    updatePlaylistPosition(track.getRoomName(), new Position(track.getTrackId(), position, Status.PLAYING));
  }

  private static Track getLastTrack(String roomName) {
    return getDatastore().find(Track.class).filter("roomName =", roomName).order("-added, -position").limit(1).get();
  }

  public static List<Track> getTracks(String roomName) {
    return getTracksByRoomQuery(roomName).asList();
  }

  private static Track getTrackAtPosition(String roomName, int position) {
    return getTracksByRoomQuery(roomName).offset(position).limit(1).get();
  }

  private static Query<Track> getTracksByRoomQuery(String roomName) {
    return getDatastore().find(Track.class).order("position, added").filter("deleted", false).filter("roomName =", roomName);
  }

  public static void moveTrack(String trackId, TrackPosition position) {

    Track track = getTrack(trackId);
    if (track == null) {
      return;
    }

    updateTrackPosition(track, position);

    getDatastore().merge(track);
  }

  public static void delete(String trackId) {

    Track track = getTrack(trackId);
    if (track == null) {
      return;
    }

    track.setDeleted(true);

    getDatastore().merge(track);
  }

  public static TrackAndPosition undelete(String trackId) {

    Track track = getTrack(trackId);
    if (track == null) {
      return null;
    }

    track.setDeleted(false);

    getDatastore().merge(track);

    return new TrackAndPosition(track, getTrackPosition(track));
  }

  public static Track addTrack(Track track, String roomName, TrackPosition position) {

    // make sure room exists
    getRoom(roomName);

    track.setRoomName(roomName);

    Track lastTrack = getLastTrack(roomName);

    if (lastTrack != null) {
      track.setPosition(lastTrack.getPosition() + 1);
    }

    if (position != null) {
      updateTrackPosition(track, position);
    }

    getDatastore().save(track);

    return track;
  }

  public static Datastore createDatastore(String host, String name) throws UnknownHostException {
    MongoClient mongoClient = new MongoClient(host);

    Morphia morphia = new Morphia();
    morphia.map(Track.class).map(Room.class);

    Datastore ds = morphia.createDatastore(mongoClient, name);

    ds.ensureIndexes();
    ds.ensureCaps();

    return ds;
  }

  public static Datastore getDatastore() {
    if (DATA_STORE == null) {
      try {
        DATA_STORE = createDatastore(MONGO_DB_HOST, MONGO_DB_NAME);
      } catch (UnknownHostException e) {
        throw new RuntimeException("Failed to connect to database", e);
      }
    }

    return DATA_STORE;
  }

  private static Query<Room> getRoomByNameQuery(String name) {
    return getDatastore().find(Room.class, "name =", name);
  }

  private static TrackPosition getTrackPosition(Track track) {
    long count = getDatastore()
               .find(Track.class)
                 .filter("roomName =", track.getRoomName())
                 .filter("position <", track.getPosition())
                 .countAll();

    // TODO: unsafe cast from long to int
    return new TrackPosition((int) count);
  }

  private static void updateTrackPosition(Track track, TrackPosition position) {

    long pos = 0;

    if (position.getPosition() != 0) {

      Track previous = getTrackAtPosition(track.getRoomName(), position.getPosition());
      if (previous == null) {
        return;
      }

      pos = previous.getPosition() + 1;
    }

    Query<Track> query = getDatastore()
        .find(Track.class)
          .filter("roomName = ", track.getRoomName())
          .filter("position >= ", pos);

    UpdateOperations<Track> updateOperation = getDatastore().createUpdateOperations(Track.class).inc("position", 1);

    UpdateResults<Track> updateResult = getDatastore().update(query, updateOperation);

    track.setPosition(pos);
  }

  public static void updatePlaylistPosition(String roomName, Position position) {
    Room room = getRoomByNameQuery(roomName).get();

    room.setPosition(position);

    getDatastore().merge(room);
  }
} public class Position {

  public enum Status {
    PLAYING,
    STOPPED
  }

  private String trackId;

  private Date date;

  private Status status;

  private int position;

  public Position(String trackId, int position, Status status) {
    this.trackId = trackId;
    this.date = new Date();
    this.status = status;
    this.position = position;
  }

  public Position() {
  }

  public String getTrackId() {
    return trackId;
  }

  public Status getStatus() {
    return status;
  }

  public void setStatus(Status status) {
    this.status = status;
  }

  public Date getDate() {
    return date;
  }

  public void setDate(Date date) {
    this.date = date;
  }

  public int getPosition() {
    return position;
  }
} public enum Status {
  PLAYING,
  STOPPED
} @Entity
public class Room {

  @Id
  private ObjectId id;

  private String name;

  @Embedded
  private Position position;

  public Room() {
  }

  public Room(String name) {
    this.name = name;
  }

  public String getName() {
    return name;
  }

  public void setPosition(Position position) {
    this.position = position;
  }

  public Position getPosition() {
    return position;
  }

  public String getId() {
    return id.toString();
  }

  public Set<Channel> channels() {
    throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
  }

  public Map<Channel, String> channelMap() {
    throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
  }

  public Collection<String> participantIds() {
    throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
  }
} @Entity
public class Track {

  @Id
  private ObjectId trackId;

  /**
   * The soundcloud id of the track
   */
  private String id;

  private String artwork_url;

  private Date added;

  private String permalink_url;

  private String title;

  @Embedded
  private SoundCloudUser user;

  private long duration;

  private String roomName;

  @JsonIgnore
  private boolean deleted = false;

  /**
   * The position of the track in the track list
   */
  @JsonIgnore
  private long position = 0;

  public Track() {

  }

  public Track(String id, String artwork_url, String permalink_url, String title, SoundCloudUser user, long duration, String roomName) {
    this.artwork_url = artwork_url;
    this.permalink_url = permalink_url;
    this.title = title;
    this.user = user;
    this.duration = duration;
    this.roomName = roomName;

    this.added = new Date();

    this.id = id;
  }

  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }

  public boolean isDeleted() {
    return deleted;
  }

  public void setDeleted(boolean deleted) {
    this.deleted = deleted;
  }

  public long getPosition() {
    return position;
  }

  public void setPosition(long position) {
    this.position = position;
  }

  public String getArtwork_url() {
    return artwork_url;
  }

  public void setArtwork_url(String artwork_url) {
    this.artwork_url = artwork_url;
  }

  public Date getAdded() {
    return added;
  }

  public void setAdded(Date added) {
    this.added = added;
  }

  public String getPermalink_url() {
    return permalink_url;
  }

  public void setPermalink_url(String permalink_url) {
    this.permalink_url = permalink_url;
  }

  public String getTitle() {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public SoundCloudUser getUser() {
    return user;
  }

  public void setUser(SoundCloudUser user) {
    this.user = user;
  }

  public long getDuration() {
    return duration;
  }

  public void setDuration(long duration) {
    this.duration = duration;
  }

  public String getRoomName() {
    return roomName;
  }

  public void setRoomName(String roomName) {
    this.roomName = roomName;
  }

  public String getTrackId() {
    return trackId.toString();
  }

  @Override
  public String toString() {
    return "Track(id=" + getTrackId() + ")";
  }
} public class AddTrack extends Base {

  private Track track;

  private TrackPosition position;

  public AddTrack() {
  }

  public AddTrack(Track track, TrackPosition position) {
    this.track = track;
    this.position = position;
  }
  
  public Track getTrack() {
    return track;
  }

  public TrackPosition getPosition() {
    return position;
  }
} public class Base {

} public class ChannelJoin extends Base {

  private String participantName;

  public String getParticipantName() {
    return participantName;
  }

  public void setParticipantName(String name) {
    this.participantName = name;
  }
} public class ChannelLeave extends Base {

  private String reason;

  public ChannelLeave() {
  }

  public ChannelLeave(String reason) {
    this.reason = reason;
  }

  public String getReason() {
    return reason;
  }
} public class ChannelOpen extends Base {

} public class MoveTrack extends TrackOperation implements PlaylistPositionSync {

  private TrackPosition from;

  private TrackPosition to;

  private Position playlistPosition;

  public MoveTrack() {

  }

  public TrackPosition getFrom() {
    return from;
  }

  public void setFrom(TrackPosition from) {
    this.from = from;
  }

  public TrackPosition getTo() {
    return to;
  }

  public void setTo(TrackPosition to) {
    this.to = to;
  }

  public Position getPlaylistPosition() {
    return playlistPosition;
  }

  public void setPlaylistPosition(Position playlistPosition) {
    this.playlistPosition = playlistPosition;
  }
} public class ParticipantJoined extends Base {

  private Participant user;

  public ParticipantJoined() {

  }

  public ParticipantJoined(Participant user) {

    this.user = user;
  }

  public Participant getUser() {
    return user;
  }

  public void setUser(Participant user) {
    this.user = user;
  }
} public class ParticipantLeft extends Base {

  private String userId;

  public ParticipantLeft() {

  }

  public ParticipantLeft(String userId) {

    this.userId = userId;
  }

  public String getUserId() {
    return userId;
  }

  public void setUserId(String id) {
    this.userId = id;
  }
} public class StartTrack extends TrackOperation {

  private int position;

  public StartTrack() {

  }

  public void setPosition(int position) {
    this.position = position;
  }

  public int getPosition() {
    return position;
  }
} public class StopTrack extends TrackOperation {

  public StopTrack() {

  }
} public class Text extends Base {

  private String message;
  private String author;

  public Text() {
  }

  public Text(String message, String author) {
    this.message = message;
    this.author = author;
  }

  /**
   * @return the message
   */
  public String getMessage() {
    return message;
  }

  /**
   * @param message the message to set
   */
  public void setMessage(String message) {
    this.message = message;
  }

  /**
   * @return the author
   */
  public String getAuthor() {
    return author;
  }

  /**
   * @param author the author to set
   */
  public void setAuthor(String author) {
    this.author = author;
  }

} public class TrackAdded extends Base {

  private Track track;

  private TrackPosition position;

  private String user;

  public TrackAdded(Track track, TrackPosition position, String user) {
    this.track = track;
    this.position = position;
    this.user = user;
  }

  public void setTrack(Track track) {
    this.track = track;
  }

  public Track getTrack() {
    return track;
  }

  public TrackPosition getPosition() {
    return position;
  }

  public void setPosition(TrackPosition position) {
    this.position = position;
  }

  public void setUser(String user) {
    this.user = user;
  }

  public String getUser() {
    return user;
  }
} public class TrackPosition {

  private int position = -1;

  public TrackPosition() {
  }

  public TrackPosition(int position) {
    this.position = position;
  }

  public int getPosition() {
    return position;
  }

  public void setPosition(int position) {
    this.position = position;
  }
} public class TrackStarted extends TrackOperation {

  private int position;

  public TrackStarted(String trackId, int position, String user) {
    super(trackId, user);

    this.position = position;
  }

  public int getPosition() {
    return position;
  }

  public void setPosition(int position) {
    this.position = position;
  }
} public class TrackStopped extends TrackOperation {

  public TrackStopped(String trackId, String user) {
    super(trackId, user);
  }
} public class Participant implements Comparable<Participant> {

  private ObjectId id;

  private String name;

  public Participant(String name) {
    this.name = name;

    this.id = new ObjectId();
  }

  public Participant() {
  }

  public String getId() {
    return id.toString();
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int compareTo(Participant o) {
    return o.id.compareTo(id);
  }
} public class RemoveTrack extends TrackOperation {

} public class TrackMoved extends TrackOperation {

  private TrackPosition to;

  public TrackMoved(TrackPosition to, String trackId, String user) {
    super(trackId, user);

    this.to = to;
  }

  public TrackMoved() {

  }

  public TrackPosition getTo() {
    return to;
  }

  public void setTo(TrackPosition to) {
    this.to = to;
  }
} public class TrackRemoved extends TrackOperation {

  public TrackRemoved() {
  }

  public TrackRemoved(String trackId, String user) {
    super(trackId, user);

    super.setUndo(new UndoAction(new UndoRemoveTrack(trackId)));
  }
} public class UndoRemoveTrack extends TrackOperation {

  public UndoRemoveTrack() {
  }

  public UndoRemoveTrack(String trackId) {
    super(trackId);
  }

} public interface RoomContext {

  public Executor executor();

  /**
   * Returns the current channel.
   *
   * @return
   */
  public Channel channel();

  /**
   * Returns all channels.
   *
   * @return
   */
  public Set<Channel> channels();

  public Map<Channel, String> channelMap();

  public <T> Attribute<T> attr(AttributeKey<T> key);

  public Room room();

  public RoomContext forChannel(Channel channel);
}
import de.nixis.web.disco.dto.PlaylistPositionSync;
import de.nixis.web.disco.dto.TrackAndPosition;
import de.nixis.web.disco.room.AbstractRoomHandler;
import de.nixis.web.disco.dto.ChannelJoined;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import de.nixis.web.disco.db.Disco;
import de.nixis.web.disco.db.entity.Position;
import de.nixis.web.disco.db.entity.Position.Status;
import de.nixis.web.disco.db.entity.Room;
import de.nixis.web.disco.db.entity.Track;
import de.nixis.web.disco.dto.AddTrack;
import de.nixis.web.disco.dto.Base;
import de.nixis.web.disco.dto.ChannelJoin;
import de.nixis.web.disco.dto.ChannelLeave;
import de.nixis.web.disco.dto.ChannelOpen;
import de.nixis.web.disco.dto.MoveTrack;
import de.nixis.web.disco.dto.ParticipantJoined;
import de.nixis.web.disco.dto.ParticipantLeft;
import de.nixis.web.disco.dto.StartTrack;
import de.nixis.web.disco.dto.StopTrack;
import de.nixis.web.disco.dto.Text;
import de.nixis.web.disco.dto.TrackAdded;
import de.nixis.web.disco.dto.TrackPosition;
import de.nixis.web.disco.dto.TrackStarted;
import de.nixis.web.disco.dto.TrackStopped;
import de.nixis.web.disco.dto.Participant;
import de.nixis.web.disco.dto.RemoveTrack;
import de.nixis.web.disco.dto.TrackMoved;
import de.nixis.web.disco.dto.TrackRemoved;
import de.nixis.web.disco.dto.UndoRemoveTrack;
import de.nixis.web.disco.room.RoomContext;
import io.netty.channel.Channel;
package de.nixis.web.disco.ws;




/**
 *
 * @author nico.rehwaldt
 */
public class DefaultRoomHandler extends AbstractRoomHandler<Base> {

  private static final Logger LOGGER = Logger.getLogger(DefaultRoomHandler.class.getName());

//  static {
//    Logger rootLogger = Logger.getLogger("");
//    rootLogger.setLevel(Level.ALL);
//    rootLogger.addHandler(new ConsoleHandler());
//  }

  @Override
  public void handleMessage(RoomContext ctx, Base message) {

    Map<Channel, String> channels = ctx.channelMap();

    String participantId = channels.get(ctx.channel());

    if (participantId == null &&
      !(message instanceof ChannelJoin) &&
      !(message instanceof ChannelOpen)) {