/*
 * Copyright (C) 2003-2012 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.settings.cache;

import org.exoplatform.commons.api.settings.SettingService;
import org.exoplatform.commons.api.settings.SettingValue;
import org.exoplatform.commons.api.settings.data.*;
import org.exoplatform.services.cache.CacheService;
import org.exoplatform.services.cache.ExoCache;
import org.exoplatform.services.cache.future.FutureExoCache;
import org.exoplatform.services.cache.future.Loader;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.settings.cache.selector.SettingCacheSelector;
import org.exoplatform.settings.jpa.JPASettingServiceImpl;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Created by The eXo Platform SAS Author : eXoPlatform bangnv@exoplatform.com
 * Nov 15, 2012 CacheSettingServiceImpl is implemented for application which
 * uses cache. CacheSettingServiceImpl contains also settingService for
 * database. In case of saving and removing setting properties,
 * CacheSettingService will effect the change in cache and database. Otherwise,
 * it will search setting properties in cache at first and then in database,
 * that allows to improve performance.
 * 
 * @LevelAPI Experimental
 */
public class CacheSettingServiceImpl implements SettingService {
  /** Logger */
  private static final Log                                           LOG                  =
                                                                         ExoLogger.getLogger(CacheSettingServiceImpl.class);

  private final static String                                        SETTING_CACHING_NAME = "commons.SettingService";

  protected ExoCache<SettingKey, SettingValue>                       settingCache;

  protected FutureExoCache<SettingKey, SettingValue, SettingService> futureExoCache;

  private static final Log                                           log                  =
                                                                         ExoLogger.getLogger(CacheSettingServiceImpl.class);

  private final SettingService                                       service;

  /**
   * Create cache setting service object with service for database and service for
   * cache
   * 
   * @param service Setting service for database
   * @param cacheService Cache service
   * @LevelAPI Experimental
   */
  public CacheSettingServiceImpl(JPASettingServiceImpl service, CacheService cacheService) {

    settingCache = cacheService.getCacheInstance(SETTING_CACHING_NAME);

    Loader<SettingKey, SettingValue, SettingService> loader = new Loader<SettingKey, SettingValue, SettingService>() {
      @Override
      public SettingValue retrieve(SettingService service, SettingKey key) throws Exception {
        SettingValue<?> settingValue = service.get(key.getContext(), key.getScope(), key.getKey());
        if (settingValue == null) {
          settingValue = NullSettingValue.getInstance();
        }
        return settingValue;
      }
    };
    futureExoCache = new FutureExoCache<SettingKey, SettingValue, SettingService>(loader, settingCache);
    this.service = service;

  }

  /**
   * Set the specified value with the key which is composed by context, scope,
   * key. The value will be saved in the cache and the database
   * 
   * @param context context with which the specified value is to be associated
   * @param scope scope with which the specified value is to be associated
   * @param key key with which the specified value is to be associated
   * @param value value to be associated with the specified key.
   * @LevelAPI Experimental
   */
  @Override
  public void set(Context context, Scope scope, String key, SettingValue<?> value) {
    SettingKey settingKey = new SettingKey(context, scope, key);
    settingCache.put(settingKey, value);
    service.set(context, scope, key, value);
  }

  /**
   * Get setting value associated with composite key(context, scope, key) This
   * service will search in the cache first and then in the database.
   * 
   * @return Setting value with type of setting property, and null if the cache
   *         and the database doesn't contain the value for the composite key
   * @LevelAPI Experimental
   */
  @Override
  public SettingValue<?> get(Context context, Scope scope, String key) {
    try {
      SettingValue<?> settingValue = futureExoCache.get(service, new SettingKey(context, scope, key));
      if (settingValue == NullSettingValue.getInstance() || settingValue.getValue() == null) {
        return null;
      }
      return settingValue;
    } catch (Exception e) {
      if (LOG.isDebugEnabled()) {
        LOG.error("Exception raising when getting setting value ", e);
      } else {
        if (context != null) {
          LOG.warn("Exception raising when getting setting value associated to the key " + key + " and the context "
              + context.getName());
        } else {
          LOG.warn("Can't get setting value. The context is null");
        }
      }
    }
    return null;
  }

  /**
   * Remove all the value associated with the composite key(context,scope,key) in
   * cache and also in database.
   * 
   * @param context context with which the specified value is to be associated.
   *          The context type must be USER and context.id must be not null.
   * @param scope scope with which the specified value is to be associated. The
   *          scope.id must be not null.
   * @param key key with which the specified value is to be associated
   * @LevelAPI Experimental
   */
  @Override
  public void remove(Context context, Scope scope, String key) {
    SettingKey settingKey = new SettingKey(context, scope, key);
    settingCache.remove(settingKey);
    service.remove(context, scope, key);
  }

  /**
   * remove all the value associated with the specified context and specified
   * scope in cache and database also.
   * 
   * @param context context with which the specified value is to be associated.
   *          The context type must be USER and context.id must be not null.
   * @param scope scope with which the specified value is to be associated. The
   *          scope.id must be not null.
   * @LevelAPI Experimental
   */
  @Override
  public void remove(Context context, Scope scope) {
    SettingScope settingScope = new SettingScope(context, scope);
    try {
      settingCache.select(new SettingCacheSelector(settingScope));
    } catch (Exception e) {
      LOG.error("Cannot get setting cache", e);
    }
    service.remove(context, scope);
  }

  /**
   * remove all the value associated with the specified context in cache and
   * database also.
   * 
   * @param context context with which the specified value is to be associated.
   *          The context type must be USER and context.id must be not null.
   * @LevelAPI Experimental
   */
  @Override
  public void remove(Context context) {
    SettingContext settingContext = new SettingContext(context);
    try {
      settingCache.select(new SettingCacheSelector(settingContext));
    } catch (Exception e) {
      LOG.error("cannot get setting context", e);
    }
    service.remove(context);
  }

  @Override
  public long countContextsByType(String contextType) {
    return service.countContextsByType(contextType);
  }

  @Override
  public List<String> getContextNamesByType(String contextType, int offset, int limit) {
    return service.getContextNamesByType(contextType, offset, limit);
  }

  @Override
  public Map<Scope, Map<String, SettingValue<String>>> getSettingsByContext(Context context) {
    return service.getSettingsByContext(context);
  }

  @Override
  public List<Context> getContextsByTypeAndScopeAndSettingName(String contextType,
                                                               String scopeType,
                                                               String scopeName,
                                                               String settingName,
                                                               int offset,
                                                               int limit) {
    return service.getContextsByTypeAndScopeAndSettingName(contextType, scopeType, scopeName, settingName, offset, limit);
  }

  @Override
  public Set<String> getEmptyContextsByTypeAndScopeAndSettingName(String contextType,
                                                                  String scopeType,
                                                                  String scopeName,
                                                                  String settingName,
                                                                  int offset,
                                                                  int limit) {
    return service.getEmptyContextsByTypeAndScopeAndSettingName(contextType, scopeType, scopeName, settingName, offset, limit);
  }

  @Override
  public void save(Context context) {
    service.save(context);
  }

  @Override
  public Map<String, SettingValue> getSettingsByContextAndScope(String contextType,
                                                                String contextName,
                                                                String scopeType,
                                                                String scopeName) {
    return service.getSettingsByContextAndScope(contextType, contextName, scopeType, scopeName);
  }

}
