content: /*
   * Copyright (C) 2015 Actor LLC. <https://actor.im>
   */
  
  package im.actor.core.modules.notifications;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  
  import im.actor.core.PlatformType;
  import im.actor.core.entity.ContentDescription;
  import im.actor.core.entity.GroupType;
  import im.actor.core.entity.Notification;
  import im.actor.core.entity.Peer;
  import im.actor.core.entity.PeerType;
  import im.actor.core.modules.ModuleContext;
  import im.actor.core.events.AppVisibleChanged;
  import im.actor.core.events.PeerChatClosed;
  import im.actor.core.events.PeerChatOpened;
  import im.actor.core.modules.notifications.entity.PendingNotification;
  import im.actor.core.modules.notifications.entity.PendingStorage;
  import im.actor.core.modules.notifications.entity.ReadState;
  import im.actor.core.modules.ModuleActor;
  import im.actor.runtime.Storage;
  import im.actor.runtime.eventbus.Event;
  import im.actor.runtime.storage.SyncKeyValue;
  
  import static im.actor.core.util.JavaUtil.last;
  
  /**
   * Actor that controls all notifications in application
   * <p>
   * NotificationsActor keeps all unread messages for showing last unread messages in notifications
   * Actor also control sound effects playing logic
   */
  public class NotificationsActor extends ModuleActor {
  
      /**
       * Maximum amount of messages in notification
       */
      private static final int MAX_NOTIFICATION_COUNT = 10;
  
      /**
       * KeyValue storage name for actor state
       */
      private static final String STORAGE_NOTIFICATIONS_DEPRECATED = "notifications";
      private static final String STORAGE_NOTIFICATIONS = "limited_notifications";
  
  
      /**
       * Storage for Actor internal state
       */
      private SyncKeyValue storage;
      /**
       * BSer object for pending notifications storage
       */
      private PendingStorage pendingStorage;
      /**
       * in memory not limited pending storage
       */
      private ArrayList<PendingNotification> allPendingNotifications = new ArrayList<PendingNotification>();
      /**
       * Cached read states
       */
      private HashMap<Peer, Long> readStates = new HashMap<>();
  
  
      /**
       * Current visible peer
       */
      private Peer visiblePeer;
      /**
       * Is Application visible state
       */
      private boolean isAppVisible = false;
  
  
      /**
       * Is Notifications paused
       */
      private boolean isNotificationsPaused = false;
      /**
       * Stored notifications during pause
       */
      private HashMap<Peer, Boolean> notificationsDuringPause = new HashMap<>();
  
  
      /**
       * Is current platform is mobile
       */
      private boolean isMobilePlatform = false;
  
      /**
       * Constructor of Actor
       *
       * @param context Module context
       */
      public NotificationsActor(ModuleContext context) {
          super(context);
      }
  
      @Override
      public void preStart() {
  
          isMobilePlatform = config().getPlatformType() == PlatformType.ANDROID ||
                  config().getPlatformType() == PlatformType.IOS;
  
          // Building storage
          storage = new SyncKeyValue(Storage.createKeyValue(STORAGE_NOTIFICATIONS));
  
          // Loading pending messages
          pendingStorage = new PendingStorage(MAX_NOTIFICATION_COUNT);
          byte[] storage = this.storage.get(0);
          if (storage != null) {
              try {
                  pendingStorage = PendingStorage.fromBytes(storage, MAX_NOTIFICATION_COUNT);
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
  
          subscribe(AppVisibleChanged.EVENT);
          subscribe(PeerChatOpened.EVENT);
          subscribe(PeerChatClosed.EVENT);
      }
  
      /**
       * Handling event about incoming notification
       *
       * @param peer                  peer of message
       * @param sender                sender uid of message
       * @param date                  date of message
       * @param description           content description of message
       * @param hasCurrentUserMention does message have user mention
       */
      public void onNewMessage(Peer peer, int sender, long date, ContentDescription description,
                               boolean hasCurrentUserMention, int messagesCount, int dialogsCount) {
  
          // Check if message already read to avoid incorrect notifications
          // for already read messages
          if (date <= getLastReadDate(peer)) {
              return;
          }
  
          // If wee need to play "out-app" notification for message
          boolean isEnabled = isNotificationsEnabled(peer, hasCurrentUserMention);
  
          // Save to pending storage
          if (isEnabled) {
              List<PendingNotification> pendingNotifications = getNotifications();
              PendingNotification pendingNotification = new PendingNotification(peer, sender, date, description);
              pendingNotifications.add(pendingNotification);
              pendingStorage.setMessagesCount(messagesCount);
              pendingStorage.setDialogsCount(dialogsCount);
              allPendingNotifications.add(pendingNotification);
              saveStorage();
          }
  
          // Adding to paused cache if notifications paused
          // This peers will be released during call onNotificationsResumed
          if (isNotificationsPaused) {
              if (!notificationsDuringPause.containsKey(peer)) {
                  notificationsDuringPause.put(peer, hasCurrentUserMention);
              } else {
                  if (hasCurrentUserMention && !notificationsDuringPause.get(peer)) {
                      notificationsDuringPause.put(peer, true);
                  }
  
              }
              return;
          }
  
          // Performing notification
          //
          // There are two separate modes of notifications:
          // First one is when app is visible, and second one when not.
          //
          // For in app notifications app need to be more careful as people already pay
          // full attention to app or may be particular chat.
          // For "in-app" notifications playing only sound effects for new messages.
          //
          // For "out-app" just playing default notifications.
          //
          // WARRING: This implementation is copied to onNotificationsResumed with small changes
          // to fit logic
  
          if (isAppVisible) {
  
              // If application is visible application play only sound effects and
              // doesn't show any "out-app" notifications
  
              if (visiblePeer != null && visiblePeer.equals(peer)) {
  
                  // If message arrive to visible chat
                  if (isMobilePlatform) {
  
                      // Play sound effect on mobile
                      playEffectIfEnabled();
                  } else {
  
                      // Do nothing on desktop as it is very distracting
                      // Most of the people use mobile messengers occasionally and
                      // with disabled sounds and it doesn't make problems as much as on
                      // non-mobile platforms
                  }
              } else {
  
                  if (isMobilePlatform) {
  
                      // Don't play any sounds not in chat screen on mobile phone
                      // We done this because we have unread badge in chat screen on mobile
                  } else {
  
                      // For desktop environments play sounds for all chats that
                      // doesn't explicitly disabled
                      if (isEnabled) {
                          playEffectIfEnabled();
                      }
                  }
              }
  
          } else {
  
              // If application is not visible play regular notification for enabled chats
              if (isEnabled) {
                  showNotification();
              }
          }
      }
  
      /**
       * Processing event about messages read
       *
       * @param peer     peer
       * @param fromDate read from date
       */
      public void onMessagesRead(Peer peer, long fromDate) {
  
          // Filtering obsolete read events
          if (fromDate < getLastReadDate(peer)) {
              return;
          }
  
          // Removing read messages from pending storage
          getNotifications().clear();
          pendingStorage.setMessagesCount(0);
          pendingStorage.setDialogsCount(0);
          allPendingNotifications.clear();
          saveStorage();
          updateNotification();
  
          // Setting last read date
          setLastReadDate(peer, fromDate);
      }
  
      /**
       * Pausing notifications
       */
      public void onNotificationsPaused() {
          isNotificationsPaused = true;
      }
  
      /**
       * Resuming notifications.
       * Checking all pending notification peers and play notifications if it is required.
       * WARRING: Implementation contains modified copy of code of onNewMessage method
       */
      public void onNotificationsResumed() {
          isNotificationsPaused = false;
  
          // If there are notifications during pause
          if (notificationsDuringPause.size() > 0) {
  
              if (isAppVisible) {
                  if (visiblePeer != null && notificationsDuringPause.containsKey(visiblePeer)) {
  
                      // If there was message from visible chat
                      if (isMobilePlatform) {
  
                          // Playing sound effects for mobile platforms
                          playEffectIfEnabled();
                      } else {
  
                          // Don't play sounds in chat on non-mobile platforms
                      }
                  } else {
  
                      // If there are no messages from visible peer
                      if (isMobilePlatform) {
  
                          // Don't play sounds not from current chat on mobile platforms
                      } else {
  
                          // Find any suitable peer and if found play sound effect
                          for (Peer p : notificationsDuringPause.keySet()) {
  
                              if (isNotificationsEnabled(p, notificationsDuringPause.get(p))) {
                                  playEffectIfEnabled();
                                  break; // Can't do return because we need to make more work later
                              }
                          }
                      }
                  }
              } else {
  
                  // Just show out-app notification
                  for (Peer p : notificationsDuringPause.keySet()) {
  
                      if (isNotificationsEnabled(p, notificationsDuringPause.get(p))) {
                          showNotification();
                          break;
                      }
                  }
              }
  
              // Clearing of notifications
              notificationsDuringPause.clear();
          }
      }
  
      /**
       * Processing Conversation visible event
       *
       * @param peer peer
       */
      public void onConversationVisible(Peer peer) {
          this.visiblePeer = peer;
      }
  
      /**
       * Processing Conversation hidden event
       *
       * @param peer peer
       */
      public void onConversationHidden(Peer peer) {
          if (visiblePeer != null && visiblePeer.equals(peer)) {
              this.visiblePeer = null;
          }
      }
  
      /**
       * Processing Application visible event
       */
      public void onAppVisible() {
          isAppVisible = true;
  
          // Hiding all notifications right after opening application
          hideNotification();
      }
  
      /**
       * Processing Application hidden event
       */
      public void onAppHidden() {
          isAppVisible = false;
      }
  
      /**
       * Performing notifications
       */
  
      /**
       * Playing sound effects
       */
      private void playEffectIfEnabled() {
          if (isEffectsEnabled()) {
              config().getNotificationProvider().onMessageArriveInApp(context().getMessenger());
          }
      }
  
      /**
       * Updating notifications
       */
      private void updateNotification() {
          performNotificationImp(true);
      }
  
      /**
       * Showing new notifications
       */
      private void showNotification() {
          performNotificationImp(false);
      }
  
      /**
       * Hiding notifications
       */
      private void hideNotification() {
          config().getNotificationProvider().hideAllNotifications();
      }
  
      /**
       * Method for showing/updating notifications
       *
       * @param performUpdate is need to perform update instead of showing
       */
      private void performNotificationImp(boolean performUpdate) {
          // Getting pending notifications list
          List<PendingNotification> destNotifications = getNotifications();
          if (destNotifications.size() == 0) {
              hideNotification();
              return;
          }
  
          // Converting to PendingNotifications
          List<Notification> res = new ArrayList<>();
          for (PendingNotification p : destNotifications) {
              boolean isChannel = false;
              if (p.getPeer().getPeerType() == PeerType.GROUP) {
                  isChannel = groups().getValue(p.getPeer().getPeerId()).getGroupType() == GroupType.CHANNEL;
              }
              res.add(new Notification(p.getPeer(), isChannel, p.getSender(), p.getContent()));
          }
  
          // Performing notifications
          if (performUpdate) {
              config().getNotificationProvider().onUpdateNotification(context().getMessenger(), res,
                      pendingStorage.getMessagesCount(), pendingStorage.getDialogsCount());
          } else {
              config().getNotificationProvider().onNotification(context().getMessenger(), res,
                      pendingStorage.getMessagesCount(), pendingStorage.getDialogsCount());
          }
      }
  
      /**
       * Storage and settings methods
       */
  
      /**
       * Convenience method for checking if sound effects are enabled
       *
       * @return is sound effects are enabled
       */
      private boolean isEffectsEnabled() {
          return context().getSettingsModule().isConversationTonesEnabled();
      }
  
      /**
       * Testing if notifications enabled for message
       *
       * @param peer       peer of message
       * @param hasMention does peer have mention
       * @return is notification enabled for peer
       */
      private boolean isNotificationsEnabled(Peer peer, boolean hasMention) {
  
          // If notifications doesn't enabled at all
          if (!context().getSettingsModule().isNotificationsEnabled()) {
              return false;
          }
  
          // Notifications for groups
          if (peer.getPeerType() == PeerType.GROUP) {
  
              // Disable notifications for hidden groups
              if (getGroup(peer.getPeerId()).isHidden()) {
                  return false;
              }
  
              if (context().getSettingsModule().isGroupNotificationsEnabled()) {
  
                  // If there are mention in group always allow notification
                  if (hasMention) {
                      return true;
                  }
  
                  if (context().getSettingsModule().isNotificationsEnabled(peer)) {
  
                      // If forced only mentions
                      if (context().getSettingsModule().isGroupNotificationsOnlyMentionsEnabled()) {
                          return false; // hasMention always false at this line
                      } else {
                          return true;
                      }
                  } else {
  
                      // Notifications are not enabled in group
                      return false;
                  }
              } else {
  
                  // All group notifications are disabled
                  return false;
              }
          } else if (peer.getPeerType() == PeerType.PRIVATE) {
  
              // For private conversations only check if peer notifications enabled
              return context().getSettingsModule().isNotificationsEnabled(peer);
          } else {
  
              // Never happens
              throw new RuntimeException("Unknown peer type");
          }
      }
  
      /**
       * Convenience method for getting all notifications
       *
       * @return all pending notifications
       */
      private List<PendingNotification> getNotifications() {
          return pendingStorage.getNotifications();
      }
  
      /**
       * Saving pending messages storage
       */
      private void saveStorage() {
          this.storage.put(0, pendingStorage.toByteArray());
      }
  
      /**
       * Getting last read sort key for peer
       *
       * @param peer peer for key
       * @return sort key, 0 if not available
       */
      private long getLastReadDate(Peer peer) {
          if (readStates.containsKey(peer)) {
              return readStates.get(peer);
          }
  
          byte[] data = storage.get(peer.getUnuqueId());
          if (data != null) {
              try {
                  return ReadState.fromBytes(data).getSortDate();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
          return 0;
      }
  
      /**
       * Setting last read date for peer
       *
       * @param peer peer
       * @param date date
       */
      private void setLastReadDate(Peer peer, long date) {
          storage.put(peer.getUnuqueId(), new ReadState(date).toByteArray());
          readStates.put(peer, date);
      }
  
      /**
       * Actor stuff
       */
  
      /**
       * Receiving messages
       *
       * @param message message
       */
      @Override
      public void onReceive(Object message) {
          if (message instanceof NewMessage) {
              NewMessage newMessage = (NewMessage) message;
              onNewMessage(newMessage.getPeer(), newMessage.getSender(), newMessage.getSortDate(),
                      newMessage.getContentDescription(), newMessage.getHasCurrentUserMention(),
                      newMessage.getUnreadMessagesCount(), newMessage.getUnreadDialogsCount());
          } else if (message instanceof MessagesRead) {
              MessagesRead read = (MessagesRead) message;
              onMessagesRead(read.getPeer(), read.getFromDate());
          } else if (message instanceof PauseNotifications) {
              onNotificationsPaused();
          } else if (message instanceof ResumeNotifications) {
              onNotificationsResumed();
          } else {
              super.onReceive(message);
          }
      }
  
      /**
       * Receiving bus events
       *
       * @param event event
       */
      @Override
      public void onBusEvent(Event event) {
          if (event instanceof AppVisibleChanged) {
              AppVisibleChanged visibleChanged = (AppVisibleChanged) event;
              if (visibleChanged.isVisible()) {
                  onAppVisible();
              } else {
                  onAppHidden();
              }
          } else if (event instanceof PeerChatOpened) {
              onConversationVisible(((PeerChatOpened) event).getPeer());
          } else if (event instanceof PeerChatClosed) {
              onConversationHidden(((PeerChatClosed) event).getPeer());
          }
      }
  
      public static class NewMessage {
          private Peer peer;
          private int sender;
          private long sortDate;
          private ContentDescription contentDescription;
          private boolean hasCurrentUserMention;
          private int unreadMessagesCount;
          private int unreadDialogsCount;
  
          public NewMessage(Peer peer, int sender, long sortDate, ContentDescription contentDescription,
                            boolean hasCurrentUserMention, int unreadMessagesCount, int unreadDialogsCount) {
              this.peer = peer;
              this.sender = sender;
              this.sortDate = sortDate;
              this.contentDescription = contentDescription;
              this.hasCurrentUserMention = hasCurrentUserMention;
              this.unreadMessagesCount = unreadMessagesCount;
              this.unreadDialogsCount = unreadDialogsCount;
          }
  
          public Peer getPeer() {
              return peer;
          }
  
          public int getSender() {
              return sender;
          }
  
          public long getSortDate() {
              return sortDate;
          }
  
          public ContentDescription getContentDescription() {
              return contentDescription;
          }
  
          public boolean getHasCurrentUserMention() {
              return hasCurrentUserMention;
          }
  
          public int getUnreadMessagesCount() {
              return unreadMessagesCount;
          }
  
          public int getUnreadDialogsCount() {
              return unreadDialogsCount;
          }
      }
  
      public static class MessagesRead {
          private Peer peer;
          private long fromDate;
  
          public MessagesRead(Peer peer, long fromDate) {
              this.peer = peer;
              this.fromDate = fromDate;
          }
  
          public Peer getPeer() {
              return peer;
          }
  
          public long getFromDate() {
              return fromDate;
          }
      }
  
      public static class PauseNotifications {
  
      }
  
      public static class ResumeNotifications {
  
      }
  }
  