package ro.isdc.wro.cache.support;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ro.isdc.wro.cache.CacheKey;
import ro.isdc.wro.cache.CacheStrategy;
import ro.isdc.wro.cache.CacheValue;
import ro.isdc.wro.config.ReadOnlyContext;
import ro.isdc.wro.model.group.Inject;
import ro.isdc.wro.model.group.processor.GroupsProcessor;
import ro.isdc.wro.model.group.processor.Injector;
import ro.isdc.wro.model.resource.support.MutableResourceAuthorizationManager;
import ro.isdc.wro.model.resource.support.ResourceAuthorizationManager;
import ro.isdc.wro.model.resource.support.change.ResourceWatcher;
import ro.isdc.wro.model.resource.support.hash.HashStrategy;
import ro.isdc.wro.util.LazyInitializer;
import ro.isdc.wro.util.SchedulerHelper;


/**
 * Responsible for invoking {@link GroupsProcessor} when cache key is missed.
 *
 * @author Alex Objelean
 * @crated 2 May 2012
 * @since 1.4.6
 */
public class DefaultSynchronizedCacheStrategyDecorator
    extends AbstractSynchronizedCacheStrategyDecorator<CacheKey, CacheValue> {
  private static final Logger LOG = LoggerFactory.getLogger(DefaultSynchronizedCacheStrategyDecorator.class);
  @Inject
  private GroupsProcessor groupsProcessor;
  @Inject
  private HashStrategy hashStrategy;
  @Inject
  private ResourceAuthorizationManager authorizationManager;
  @Inject
  private ReadOnlyContext context;
  @Inject
  private Injector injector;
  private ResourceWatcher resourceWatcher;
  /**
   * Holds the keys that were checked for change. As long as a key is contained in this set, it won't be checked again.
   */
  private final Set<CacheKey> checkedKeys = Collections.synchronizedSet(new HashSet<CacheKey>());
  private SchedulerHelper resourceWatcherScheduler;

  /**
   * Decorates the provided {@link CacheStrategy}. The provided {@link CacheStrategy} won't be decorated if the
   * operation is redundant.
   */
  public static CacheStrategy<CacheKey, CacheValue> decorate(
      final CacheStrategy<CacheKey, CacheValue> decorated) {
    return decorated instanceof DefaultSynchronizedCacheStrategyDecorator ? decorated
        : new DefaultSynchronizedCacheStrategyDecorator(decorated);
  }

  /**
   * @VisibleForTesting
   */
  DefaultSynchronizedCacheStrategyDecorator(final CacheStrategy<CacheKey, CacheValue> cacheStrategy) {
    super(cacheStrategy);
    resourceWatcherScheduler = SchedulerHelper.create(new LazyInitializer<Runnable>() {
      @Override
      protected Runnable initialize() {
        return new Runnable() {
          public void run() {
            checkedKeys.clear();
          }
        };
      }
    }, "resourceWatcherScheduler");
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected CacheValue loadValue(final CacheKey key) {
    resourceWatcherScheduler.scheduleWithPeriod(getResourceWatcherUpdatePeriod(), getTimeUnitForResourceWatcher());
    LOG.debug("load value in cache for key: {}", key);
    final String content = groupsProcessor.process(key);
    if (LOG.isDebugEnabled()) {
      LOG.debug("found content: {}", StringUtils.abbreviate(content, 30));
    }
    return computeCacheValueByContent(content);
  }

  private long getResourceWatcherUpdatePeriod() {
    return context.getConfig().getResourceWatcherUpdatePeriod();
  }

  /**
   * @return {@link TimeUnit} used to run resourceWatcher.
   * @VisibleForTesting
   */
  TimeUnit getTimeUnitForResourceWatcher() {
    return TimeUnit.SECONDS;
  }

  /**
   * Creates a {@link CacheValue} based on provided content.
   */
  private CacheValue computeCacheValueByContent(final String content) {
    String hash = null;
    try {
      if (content != null) {
        if (LOG.isDebugEnabled()) {
          LOG.debug("Content to fingerprint: [{}]", StringUtils.abbreviate(content, 30));
        }
        hash = hashStrategy.getHash(new ByteArrayInputStream(content.getBytes()));
      }
      final CacheValue entry = CacheValue.valueOf(content, hash);
      LOG.debug("computed entry: {}", entry);
      return entry;
    } catch (final IOException e) {
      throw new RuntimeException("Should never happen", e);
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void onBeforeGet(final CacheKey key) {
    if (shouldWatchForChange(key)) {
      LOG.debug("ResourceWatcher check key: {}", key);
      getResourceWatcher().check(key);
      checkedKeys.add(key);
    }
  }

  /**
   * @return the {@link ResourceWatcher} instance handling check for stale resources.
   * @VisibleForTesting
   */
  ResourceWatcher getResourceWatcher() {
    if (resourceWatcher == null) {
      resourceWatcher = new ResourceWatcher();
      injector.inject(resourceWatcher);
    }
    return resourceWatcher;
  }

  /**
   * @return true if the provided key should be checked for change.
   */
  private boolean shouldWatchForChange(final CacheKey key) {
    final boolean result = getResourceWatcherUpdatePeriod() > 0 && !checkedKeys.contains(key);
    LOG.debug("shouldWatchForChange: {}", result);
    return result;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void clear() {
    super.clear();
    // reset authorization manager (clear any stored uri's).
    if (authorizationManager instanceof MutableResourceAuthorizationManager) {
      ((MutableResourceAuthorizationManager) authorizationManager).clear();
    }
  }
}
