content: /*
   * Copyright (C) 2015 Actor LLC. <https://actor.im>
   */
  
  package im.actor.core.modules.presence;
  
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  
  import im.actor.core.api.ApiGroupOutPeer;
  import im.actor.core.api.ApiUserOutPeer;
  import im.actor.core.api.rpc.RequestSubscribeToGroupOnline;
  import im.actor.core.api.rpc.RequestSubscribeToOnline;
  import im.actor.core.api.rpc.ResponseVoid;
  import im.actor.core.entity.Group;
  import im.actor.core.entity.GroupType;
  import im.actor.core.entity.Peer;
  import im.actor.core.entity.PeerType;
  import im.actor.core.entity.User;
  import im.actor.core.modules.ModuleContext;
  import im.actor.core.events.NewSessionCreated;
  import im.actor.core.events.PeerChatOpened;
  import im.actor.core.events.PeerInfoOpened;
  import im.actor.core.events.UserVisible;
  import im.actor.core.modules.ModuleActor;
  import im.actor.core.viewmodel.GroupVM;
  import im.actor.core.viewmodel.UserPresence;
  import im.actor.core.viewmodel.UserVM;
  import im.actor.runtime.Log;
  import im.actor.runtime.actors.ActorCreator;
  import im.actor.runtime.actors.ActorRef;
  import im.actor.runtime.actors.ActorSystem;
  import im.actor.runtime.actors.ActorCancellable;
  import im.actor.runtime.actors.Props;
  import im.actor.runtime.annotations.Verified;
  import im.actor.runtime.eventbus.BusSubscriber;
  import im.actor.runtime.eventbus.Event;
  import im.actor.runtime.function.Consumer;
  import im.actor.runtime.promise.Promise;
  import im.actor.runtime.promise.PromisesArray;
  
  @Verified
  public class PresenceActor extends ModuleActor implements BusSubscriber {
  
      public static ActorRef create(final ModuleContext messenger) {
          return ActorSystem.system().actorOf("actor/presence", () -> new PresenceActor(messenger));
      }
  
      private static final int ONLINE_TIMEOUT = 5 * 60 * 1000;
  
      // private static final String TAG = "PresenceActor";
  
      private HashMap<Integer, Long> lastUidState = new HashMap<>();
      private HashMap<Integer, Long> lastGidState = new HashMap<>();
      private HashMap<Integer, ActorCancellable> uidCancellables = new HashMap<>();
      private HashSet<Integer> uids = new HashSet<>();
      private HashSet<Integer> gids = new HashSet<>();
  
      private boolean isRequesting = false;
      private ArrayList<Peer> pendingPeers = new ArrayList<>();
  
      public PresenceActor(ModuleContext messenger) {
          super(messenger);
      }
  
      @Override
      public void preStart() {
          context().getEvents().subscribe(this, NewSessionCreated.EVENT);
          context().getEvents().subscribe(this, PeerChatOpened.EVENT);
          context().getEvents().subscribe(this, PeerInfoOpened.EVENT);
          context().getEvents().subscribe(this, UserVisible.EVENT);
      }
  
      @Verified
      private void onUserOnline(int uid, long updateDate) {
          // Log.d(TAG, "onUserOnline  #" + uid + " at " + updateDate);
          if (lastUidState.containsKey(uid) && lastUidState.get(uid) >= updateDate) {
              // Log.d(TAG, "onUserOnline:ignored - too old");
              return;
          }
          lastUidState.put(uid, updateDate);
          // Log.d(TAG, "onUserOnline:updated");
  
          UserVM vm = getUserVM(uid);
          if (vm != null) {
              vm.getPresence().change(new UserPresence(UserPresence.State.ONLINE));
          }
  
          // Updating timeout
          if (uidCancellables.containsKey(uid)) {
              uidCancellables.remove(uid).cancel();
          }
          uidCancellables.put(uid, schedule(new OnlineUserTimeout(uid, (int) ((updateDate + ONLINE_TIMEOUT) / 1000L),
                  updateDate + ONLINE_TIMEOUT), ONLINE_TIMEOUT));
      }
  
      @Verified
      private void onUserOffline(int uid, long updateDate) {
          // Log.d(TAG, "onUserOffline  #" + uid + " at " + updateDate);
          if (lastUidState.containsKey(uid) && lastUidState.get(uid) >= updateDate) {
              // Log.d(TAG, "onUserOffline:ignored - too old");
              return;
          }
          lastUidState.put(uid, updateDate);
          // Log.d(TAG, "onUserOffline:updated");
  
          UserVM vm = getUserVM(uid);
          if (vm != null) {
              vm.getPresence().change(new UserPresence(UserPresence.State.OFFLINE));
          }
  
          // Cancel timeout
          if (uidCancellables.containsKey(uid)) {
              uidCancellables.remove(uid).cancel();
          }
      }
  
      @Verified
      private void onUserLastSeen(int uid, int date, long updateDate) {
          // Log.d(TAG, "onUserLastSeen  #" + uid + " at " + date + " at " + updateDate);
          if (lastUidState.containsKey(uid) && lastUidState.get(uid) >= updateDate) {
              // Log.d(TAG, "onUserLastSeen:ignored - too old");
              return;
          }
          lastUidState.put(uid, updateDate);
          // Log.d(TAG, "onUserLastSeen:updated");
  
          UserVM vm = getUserVM(uid);
          if (vm != null) {
              vm.getPresence().change(new UserPresence(UserPresence.State.OFFLINE, date));
          }
  
          // Cancel timeout
          if (uidCancellables.containsKey(uid)) {
              uidCancellables.remove(uid).cancel();
          }
      }
  
      private void onUserGoesOffline(int uid, int date, long updateDate) {
          // Log.d(TAG, "onUserGoesOffline  #" + uid + " at " + date + " at " + updateDate);
          if (lastUidState.containsKey(uid) && lastUidState.get(uid) >= updateDate) {
              // Log.d(TAG, "onUserGoesOffline:ignored - too old");
              return;
          }
          lastUidState.put(uid, updateDate);
          // Log.d(TAG, "onUserGoesOffline:updated");
  
          UserVM vm = getUserVM(uid);
          if (vm != null) {
              vm.getPresence().change(new UserPresence(UserPresence.State.OFFLINE, date));
          }
  
          // Cancel timeout
          if (uidCancellables.containsKey(uid)) {
              uidCancellables.remove(uid).cancel();
          }
      }
  
      @Verified
      private void onGroupOnline(int gid, int count, long updateDate) {
          // Log.d(TAG, "onGroupOnline  #" + gid + " " + count + " at " + updateDate);
          if (lastGidState.containsKey(gid) && lastGidState.get(gid) >= updateDate) {
              // Log.d(TAG, "onGroupOnline:ignored - too old");
              return;
          }
          lastGidState.put(gid, updateDate);
          // Log.d(TAG, "onGroupOnline:updated");
  
          GroupVM vm = getGroupVM(gid);
          if (vm != null) {
              vm.getPresence().change(count);
          }
      }
  
      @Verified
      private void subscribe(Peer peer) {
  
          // Log.d(TAG, "subscribe:" + peer);
  
          if (peer.getPeerType() == PeerType.PRIVATE) {
  
              // Already subscribed
              if (uids.contains(peer.getPeerId())) {
                  return;
              }
  
              User user = getUser(peer.getPeerId());
              if (user == null) {
                  return;
              }
  
              // Subscribing to user online sates
              uids.add(user.getUid());
  
          } else if (peer.getPeerType() == PeerType.GROUP) {
  
              // Already subscribed
              if (gids.contains(peer.getPeerId())) {
                  return;
              }
  
              Group group = getGroup(peer.getPeerId());
              if (group == null) {
                  return;
              }
  
              // Ignore subscription to channels
              if (group.getGroupType() == GroupType.CHANNEL) {
                  return;
              }
  
              // Subscribing to group online sates
              gids.add(peer.getPeerId());
  
          } else {
              return;
          }
  
          // Adding Pending Peer
          if (pendingPeers.contains(peer)) {
              return;
          }
          pendingPeers.add(peer);
  
          onCheckQueue();
      }
  
      @Verified
      private void onNewSessionCreated() {
  
          // Resubscribing for online states of users
          for (int uid : uids) {
              Peer p = Peer.user(uid);
              if (!pendingPeers.contains(p)) {
                  pendingPeers.add(p);
              }
          }
  
          // Resubscribing for online states of groups
          for (int gid : gids) {
              Peer p = Peer.group(gid);
              if (!pendingPeers.contains(p)) {
                  pendingPeers.add(p);
              }
          }
  
          onCheckQueue();
      }
  
      private void onCheckQueue() {
  
          if (isRequesting) {
              return;
          }
  
          if (pendingPeers.size() == 0) {
              return;
          }
  
          ArrayList<Peer> destPeers = new ArrayList<>(pendingPeers);
          pendingPeers.clear();
          ArrayList<ApiUserOutPeer> outUserPeers = new ArrayList<>();
          ArrayList<ApiGroupOutPeer> outGroupPeers = new ArrayList<>();
  
          for (Peer p : destPeers) {
              if (p.getPeerType() == PeerType.GROUP) {
                  Group g = getGroup(p.getPeerId());
                  if (g != null) {
                      outGroupPeers.add(new ApiGroupOutPeer(p.getPeerId(), g.getAccessHash()));
                  }
              } else if (p.getPeerType() == PeerType.PRIVATE) {
                  User u = getUser(p.getPeerId());
                  if (u != null) {
                      outUserPeers.add(new ApiUserOutPeer(p.getPeerId(), u.getAccessHash()));
                  }
              }
          }
  
          ArrayList<Promise<ResponseVoid>> requests = new ArrayList<>();
          if (outUserPeers.size() > 0) {
              requests.add(api(new RequestSubscribeToOnline(outUserPeers)));
          }
          if (outGroupPeers.size() > 0) {
              requests.add(api(new RequestSubscribeToGroupOnline(outGroupPeers)));
          }
  
          if (requests.size() > 0) {
              isRequesting = true;
              PromisesArray.ofPromises(requests).zip().then(responseVoids -> {
                  isRequesting = false;
                  onCheckQueue();
              }).failure(e -> {
                  isRequesting = false;
                  onCheckQueue();
              });
          }
      }
  
      // Messages
  
      @Override
      public void onReceive(Object message) {
          if (message instanceof UserOnline) {
              UserOnline online = (UserOnline) message;
              onUserOnline(online.getUid(), online.getUpdateDate());
          } else if (message instanceof UserOffline) {
              UserOffline offline = (UserOffline) message;
              onUserOffline(offline.getUid(), offline.getUpdateDate());
          } else if (message instanceof UserLastSeen) {
              UserLastSeen lastSeen = (UserLastSeen) message;
              onUserLastSeen(lastSeen.getUid(), lastSeen.getDate(), lastSeen.getUpdateDate());
          } else if (message instanceof GroupOnline) {
              GroupOnline groupOnline = (GroupOnline) message;
              onGroupOnline(groupOnline.getGid(), groupOnline.getCount(), groupOnline.getUpdateDate());
          } else if (message instanceof Subscribe) {
              subscribe(((Subscribe) message).getPeer());
          } else if (message instanceof SessionCreated) {
              onNewSessionCreated();
          } else if (message instanceof OnlineUserTimeout) {
              OnlineUserTimeout timeout = (OnlineUserTimeout) message;
              onUserGoesOffline(timeout.getUid(), timeout.getDate(), timeout.getUpdateDate());
          } else {
              super.onReceive(message);
          }
      }
  
      @Override
      public void onBusEvent(Event event) {
          if (event instanceof NewSessionCreated) {
              self().send(new SessionCreated());
          } else if (event instanceof PeerChatOpened) {
              self().send(new Subscribe(((PeerChatOpened) event).getPeer()));
          } else if (event instanceof PeerInfoOpened) {
              self().send(new Subscribe(((PeerInfoOpened) event).getPeer()));
          } else if (event instanceof UserVisible) {
              self().send(new Subscribe(Peer.user(((UserVisible) event).getUid())));
          }
      }
  
      public static class UserOnline {
          private int uid;
          private long updateDate;
  
          public UserOnline(int uid, long updateDate) {
              this.uid = uid;
              this.updateDate = updateDate;
          }
  
          public int getUid() {
              return uid;
          }
  
          public long getUpdateDate() {
              return updateDate;
          }
  
      }
  
      public static class UserOffline {
          private int uid;
          private long updateDate;
  
          public UserOffline(int uid, long updateDate) {
              this.uid = uid;
              this.updateDate = updateDate;
          }
  
          public int getUid() {
              return uid;
          }
  
          public long getUpdateDate() {
              return updateDate;
          }
  
      }
  
      public static class UserLastSeen {
          private int uid;
          private int date;
          private long updateDate;
  
          public UserLastSeen(int uid, int date, long updateDate) {
              this.uid = uid;
              this.date = date;
              this.updateDate = updateDate;
          }
  
          public int getUid() {
              return uid;
          }
  
          public int getDate() {
              return date;
          }
  
          public long getUpdateDate() {
              return updateDate;
          }
  
      }
  
      public static class GroupOnline {
          private int gid;
          private int count;
          private long updateDate;
  
          public GroupOnline(int gid, int count, long updateDate) {
              this.gid = gid;
              this.count = count;
              this.updateDate = updateDate;
          }
  
          public int getGid() {
              return gid;
          }
  
          public int getCount() {
              return count;
          }
  
          public long getUpdateDate() {
              return updateDate;
          }
      }
  
      private static class OnlineUserTimeout {
  
          private int uid;
          private int date;
          private long updateDate;
  
          public OnlineUserTimeout(int uid, int date, long updateDate) {
              this.uid = uid;
              this.date = date;
              this.updateDate = updateDate;
          }
  
          public int getUid() {
              return uid;
          }
  
          public int getDate() {
              return date;
          }
  
          public long getUpdateDate() {
              return updateDate;
          }
      }
  
      public static class Subscribe {
          private Peer peer;
  
          public Subscribe(Peer peer) {
              this.peer = peer;
          }
  
          public Peer getPeer() {
              return peer;
          }
      }
  
      public static class SessionCreated {
  
      }
  }
  