/*
 * Copyright (C) 2003-2014 eXo Platform SAS.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package org.exoplatform.commons.notification.impl.service.storage.cache;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.lang.StringUtils;

import org.exoplatform.commons.api.notification.model.NotificationInfo;
import org.exoplatform.commons.api.notification.model.WebNotificationFilter;
import org.exoplatform.commons.api.notification.service.storage.WebNotificationStorage;
import org.exoplatform.commons.notification.impl.service.storage.WebNotificationStorageImpl;
import org.exoplatform.commons.notification.impl.service.storage.cache.model.IntegerData;
import org.exoplatform.commons.notification.impl.service.storage.cache.model.ListWebNotificationsData;
import org.exoplatform.commons.notification.impl.service.storage.cache.model.ListWebNotificationsKey;
import org.exoplatform.commons.notification.impl.service.storage.cache.model.WebNotifInfoCacheKey;
import org.exoplatform.commons.notification.impl.service.storage.cache.model.WebNotifInfoData;
import org.exoplatform.services.cache.CacheService;
import org.exoplatform.services.cache.CachedObjectSelector;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.cache.ObjectCacheInfo;

public class CachedWebNotificationStorage implements WebNotificationStorage {
  //
  private final static String WEB_NOTIFICATION_CACHING_NAME = "commons.WebNotificationCache";
  private final static String LIST_WEB_NOTIFICATION_CACHING_NAME = "commons.WebNotificationsCache";
  private final static String WEB_NOTIFICATION_COUNT_CACHING_NAME = "commons.WebNotificationCountCache";
  //
  private final ExoCache<WebNotifInfoCacheKey, WebNotifInfoData> exoWebNotificationCache;
  private final ExoCache<WebNotifInfoCacheKey, IntegerData> exoWebNotificationCountCache;
  private final ExoCache<ListWebNotificationsKey, ListWebNotificationsData> exoWebNotificationsCache;
  //
  private FutureExoCache<WebNotifInfoCacheKey, WebNotifInfoData, ServiceContext<WebNotifInfoData>> futureWebNotificationCache;
  private FutureExoCache<ListWebNotificationsKey, ListWebNotificationsData, ServiceContext<ListWebNotificationsData>> futureWebNotificationsCache;
  private FutureExoCache<WebNotifInfoCacheKey, IntegerData, ServiceContext<IntegerData>> futureWebNotificationCountCache;
  
  private WebNotificationStorage storage;

  public CachedWebNotificationStorage(WebNotificationStorageImpl storage, CacheService cacheService) {
    this.storage = storage;
    exoWebNotificationCache = cacheService.getCacheInstance(WEB_NOTIFICATION_CACHING_NAME);
    exoWebNotificationsCache = cacheService.getCacheInstance(LIST_WEB_NOTIFICATION_CACHING_NAME);
    exoWebNotificationCountCache = cacheService.getCacheInstance(WEB_NOTIFICATION_COUNT_CACHING_NAME);
    //
    futureWebNotificationCache = createFutureCache(exoWebNotificationCache);
    futureWebNotificationsCache = createFutureCache(exoWebNotificationsCache);
    futureWebNotificationCountCache = createFutureCache(exoWebNotificationCountCache);
  }

  @Override
  public void save(NotificationInfo notification) {
    //check the notification is existing or not
    //calling update or create new. 
    if (notification.isUpdate()) {
      storage.update(notification, true);
      //
      WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(notification.getId());
      exoWebNotificationCache.put(key, new WebNotifInfoData(notification));
      clearWebNotificationCountCache(notification.getTo());
    } else {
      storage.save(notification);
      // Update notification count
      WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(notification.getTo());
      IntegerData data = exoWebNotificationCountCache.get(key);
      if (data != null) {
        Integer current = data.build();
        exoWebNotificationCountCache.put(key, new IntegerData(current + 1));
      }
      // Add newly created notification to cache
      exoWebNotificationCache.put(WebNotifInfoCacheKey.key(notification.getId()), new WebNotifInfoData(notification));
    }
    clearUserWebNotificationList(notification.getTo());
  }

  @Override
  public void update(NotificationInfo notification, boolean moveTop) {
    notification.setUpdate(true);
    notification.setResetOnBadge(!moveTop);

    storage.update(notification, moveTop);
    //
    WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(notification.getId());
    exoWebNotificationCache.put(key, new WebNotifInfoData(notification));
    clearWebNotificationCountCache(notification.getTo());
    clearUserWebNotificationList(notification.getTo());
  }

  @Override
  public void markRead(String notificationId) {
    storage.markRead(notificationId);
    //
    updateRead(notificationId, true);
  }

  @Override
  public void markAllRead(String userId) {
    storage.markAllRead(userId);
    updateCacheByUser(userId, true);
  }

  @Override
  public void hidePopover(String notificationId) {
    storage.hidePopover(notificationId);

    // update data showPopover
    WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(notificationId);
    WebNotifInfoData infoData = exoWebNotificationCache.get(key);
    if (infoData != null) {
      infoData.updateShowPopover(false);
      exoWebNotificationCache.put(key, infoData);

      clearWebNotificationCountCache(infoData.getTo());
      clearUserWebNotificationList(infoData.getTo());
    }
  }

  @Override
  public List<NotificationInfo> get(final WebNotificationFilter filter, final int offset, final int limit) {
    final ListWebNotificationsKey key = ListWebNotificationsKey.key(filter.getUserId(), filter.isOnPopover(), offset, limit);
      //
    ListWebNotificationsData keys = futureWebNotificationsCache.get(
        new ServiceContext<ListWebNotificationsData>() {
          public ListWebNotificationsData execute() {
            List<NotificationInfo> got = storage.get(filter, offset, limit);
            return buildWebNotifDataIds(key, got);
          }
        }, key);
      //
    return buildNotifications(keys);
  }

  @Override
  public boolean remove(String notificationId) {
    NotificationInfo notification = get(notificationId);
    if (notification == null) {
      return false;
    }
    //
    storage.remove(notificationId);
    //
    clearWebNotificationCache(notificationId);
    //clear badge number in for notification's TO user.
    clearWebNotificationCountCache(notification.getTo());

    // Clear cache notifications Lists for user
    clearUserWebNotificationList(notification.getTo());
    return true;
  }

  @Override
  public NotificationInfo get(final String notificationId) {
    WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(notificationId);
    WebNotifInfoData notificationInfo = futureWebNotificationCache.get(
        new ServiceContext<WebNotifInfoData>() {
          public WebNotifInfoData execute() {
            try {
              NotificationInfo got = storage.get(notificationId);
              if (got != null) {
                return new WebNotifInfoData(got);
              }
              return null;
            } catch (Exception e) {
              throw new RuntimeException(e);
            }
          }
        }, key);
    //
    if(notificationInfo == null) {
      return null;
    }
    return notificationInfo.build();
  }

  public NotificationInfo getUnreadNotification(String pluginId, String activityId, String owner) {
    return storage.getUnreadNotification(pluginId, activityId, owner);
  }

  @Override
  public boolean remove(String userId, long seconds) {
    clearWebNotificationCountCache(userId);
    updateCacheByUser(userId, false);
    //
    return storage.remove(userId, seconds);
  }

  @Override
  public boolean remove(long seconds) {
    boolean removed = false;
    try {
      removed = storage.remove(seconds);
      return removed;
    } finally {
      if(removed) {
        exoWebNotificationCache.clearCache();
        exoWebNotificationCountCache.clearCache();
      }
    }
  }

  @Override
  public int getNumberOnBadge(final String userId) {
    if (StringUtils.isNotBlank(userId)) {
      WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(userId);
      IntegerData numberOfMessageData = futureWebNotificationCountCache.get(
              new ServiceContext<IntegerData>() {
                public IntegerData execute() {
                  try {
                    int number = storage.getNumberOnBadge(userId);
                    return new IntegerData(number);
                  } catch (Exception e) {
                    throw new RuntimeException(e);
                  }
                }
              }, key);
      return numberOfMessageData.build().intValue();
    } else {
      return 0;
    }
  }

  @Override
  public void resetNumberOnBadge(String userId) {
    storage.resetNumberOnBadge(userId);
    //
    clearWebNotificationCountCache(userId);
  }

  public void setStorage(WebNotificationStorage storage) {
    this.storage = storage;
  }

  public void updateAllRead(String userId) throws Exception {
    updateCacheByUser(userId, true);
  }

  private void updateRead(String notificationId, boolean isRead) {
    WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(notificationId);
    WebNotifInfoData infoData = exoWebNotificationCache.get(key);
    if (infoData != null) {
      infoData.updateRead(isRead);
      exoWebNotificationCache.put(key, infoData);
    }
  }

  private void updateCacheByUser(final String userId, final boolean isUpdateRead) {
    if (!isUpdateRead) {
      clearUserWebNotificationList(userId);
      clearUserWebNotifications(userId);
    } else {
      // In case isUpdateRead, we will just update the cached WebNotification entries of user
      updateReadForUserWebNotifications(userId);
    }
  }

  private void updateReadForUserWebNotifications(final String userId) {
    try {
      exoWebNotificationCache.select(new CachedObjectSelector<WebNotifInfoCacheKey, WebNotifInfoData>() {
        @Override
        public boolean select(WebNotifInfoCacheKey key, ObjectCacheInfo<? extends WebNotifInfoData> ocinfo) {
          return ocinfo.get() != null && userId.equals(ocinfo.get().getTo());
        }

        @Override
        public void onSelect(ExoCache<? extends WebNotifInfoCacheKey, ? extends WebNotifInfoData> cache,
                             WebNotifInfoCacheKey key,
                             ObjectCacheInfo<? extends WebNotifInfoData> ocinfo) throws Exception {
          ocinfo.get().updateRead(true);
        }
      });
    } catch (Exception e) {
      throw new IllegalStateException("Can't update Notification Cache entries for user " + userId, e);
    }
  }

  private void clearUserWebNotifications(final String userId) {
    try {
      exoWebNotificationCache.select(new CachedObjectSelector<WebNotifInfoCacheKey, WebNotifInfoData>() {
        @Override
        public boolean select(WebNotifInfoCacheKey key, ObjectCacheInfo<? extends WebNotifInfoData> ocinfo) {
          return ocinfo.get() != null && userId.equals(ocinfo.get().getTo());
        }
        
        @Override
        public void onSelect(ExoCache<? extends WebNotifInfoCacheKey, ? extends WebNotifInfoData> cache,
                             WebNotifInfoCacheKey key,
                             ObjectCacheInfo<? extends WebNotifInfoData> ocinfo) throws Exception {
          cache.remove(key);
        }
      });
    } catch (Exception e) {
      throw new IllegalStateException("Can't update Notifications List Cache entries for user " + userId, e);
    }
  }

  private void clearUserWebNotificationList(final String userId) {
    try {
      exoWebNotificationsCache.select(new CachedObjectSelector<ListWebNotificationsKey, ListWebNotificationsData>() {
        @Override
        public boolean select(ListWebNotificationsKey key, ObjectCacheInfo<? extends ListWebNotificationsData> ocinfo) {
          return userId.equals(key.getUserId());
        }

        @Override
        public void onSelect(ExoCache<? extends ListWebNotificationsKey, ? extends ListWebNotificationsData> cache,
                             ListWebNotificationsKey key,
                             ObjectCacheInfo<? extends ListWebNotificationsData> ocinfo) throws Exception {
          cache.remove(key);
        }
      });
    } catch (Exception e) {
      throw new IllegalStateException("Can't update Notification Cache entries for user " + userId, e);
    }
  }

  private ListWebNotificationsData getWebNotificationsData(ListWebNotificationsKey key) {
    ListWebNotificationsData data = this.exoWebNotificationsCache.get(key);
    if (data == null) {
      data = new ListWebNotificationsData(key);
      this.exoWebNotificationsCache.put(key, data);
    }
    return data;
  }

  private ListWebNotificationsData buildWebNotifDataIds(ListWebNotificationsKey key, List<NotificationInfo> notifications) {
    ListWebNotificationsData data = getWebNotificationsData(key);
    //
    for (int i = 0, len = notifications.size(); i < len; i++) {
      NotificationInfo notif = notifications.get(i);
      // handle the activity is NULL
      if (notif == null) {
        continue;
      }
      // Update single Nofification cache when the notification doesn't exit there yet
      WebNotifInfoCacheKey webNotifKey = WebNotifInfoCacheKey.key(notif.getId());
      if (exoWebNotificationCache.get(webNotifKey) == null) {
        exoWebNotificationCache.put(webNotifKey, new WebNotifInfoData(notif));
      }
      // Insert notification at the end of list
      if (!data.contains(notif.getId())) {
        data.insertLast(notif.getId());
      }
    }
    return data;
  }
  
  private List<NotificationInfo> buildNotifications(ListWebNotificationsData data) {
    List<NotificationInfo> notifications = new ArrayList<NotificationInfo>();
    for (String id : data.getList()) {
      NotificationInfo a = get(id);
      if (a != null) {
        notifications.add(a);
      }
    }
    return notifications;
  }

  /**
   * Clear the notification badge number of the specified user.
   * @param userId
   */
  private void clearWebNotificationCountCache(String userId) {
    WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(userId);
    exoWebNotificationCountCache.remove(key);
  }

  /**
   * Clear the notification from the cache.
   * @param notificationId
   */
  private void clearWebNotificationCache(String notificationId) {
    WebNotifInfoCacheKey key = WebNotifInfoCacheKey.key(notificationId);
    exoWebNotificationCache.remove(key);
  }

  private <K extends CacheKey, V extends Serializable> FutureExoCache<K, V, ServiceContext<V>> createFutureCache(ExoCache<K, V> cache) {
    return new FutureExoCache<K, V, ServiceContext<V>>(new CacheLoader<K, V>(), cache);
  }
}
