package ro.isdc.wro.model.resource.support;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;

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

import ro.isdc.wro.util.provider.ProviderFinder;


/**
 * Abstracts the configurable creation of the strategies based on {@link ProviderFinder}. Uses two generic parameters:
 *
 * @param <S>
 *          strategy type
 * @param <P>
 *          provider type
 * @author Alex Objelean
 * @created 26 Jun 2012
 * @since 1.4.7
 */
public abstract class AbstractConfigurableStrategySupport<S, P> {
  private static final Logger LOG = LoggerFactory.getLogger(AbstractConfigurableStrategySupport.class);

  private ProviderFinder<P> providerFinder;
  private Map<String, S> map;
  private Properties properties;

  /**
   * Override this method if a fallback search is required.
   *
   * @param alias
   *          of the strategy to use.
   * @return the strategy S if one is found or null if no corresponding strategy found.
   */
  protected S getStrategyForAlias(final String alias) {
    return getStrategyMap().get(alias);
  }

  /**
   * @return the value configured in the properties file using the strategy key.
   */
  protected final String getConfiguredValue() {
    return getProperties().getProperty(getStrategyKey());
  }

  /**
   * @return a set of available aliases.
   */
  public final Set<String> getAvailableAliases() {
    return getStrategyMap().keySet();
  }

  /**
   * @return a set of available strategies.
   * @VisibleForTesting
   */
  public final Collection<S> getAvailableStrategies() {
    return getStrategyMap().values();
  }

  /**
   * @return the map where the strategy alias will be searched. By default a {@link ProviderFinder} is used to build the
   *         map.
   */
  private final Map<String, S> newStrategyMap() {
    //use TreeMap to kep results sorted
    final Map<String, S> map = new TreeMap<String, S>();
    final List<P> providers = getProviderFinder().find();
    LOG.debug("providers: {}", providers);
    for (final P provider : providers) {
      try {
        map.putAll(getStrategies(provider));
      } catch (final Exception e) {
        LOG.warn("Could not load strategies for provider: {}, because of: {}. It will be skipped.", provider,
            e.getMessage());
      }
    }
    // allow client to override defaults
    overrideDefaultStrategyMap(map);
    return map;
  }

  /**
   * Invoked after the the map of strategies is built from providers. Allows client to override its keys or add new
   * entries.
   *
   * @param map
   *          the built map.
   */
  protected void overrideDefaultStrategyMap(final Map<String, S> map) {
  }

  /**
   * Utility method which copies all entries from source into target. Entries with the same keys from target will be
   * overridden with entries from source. This operation is similar to {@link Map#putAll(Map)}, but it doesn't require
   * changing generics to construction like
   *
   * <pre>
   * <? extends String, ? extends S>
   * </pre>
   *
   * @param source
   *          the map from where the entries will be copied into target.
   * @param target
   *          the map where to put entries from source.
   */
  protected final void copyAll(final Map<String, S> source, final Map<String, S> target) {
    Validate.notNull(source);
    Validate.notNull(target);
    for (final Map.Entry<String, S> entry : source.entrySet()) {
      target.put(entry.getKey(), entry.getValue());
    }
  }

  private Map<String, S> getStrategyMap() {
    if (map == null) {
      map = newStrategyMap();
    }
    return map;
  }

  /**
   * To be used for internal usage. Ensure that returned object is not null.
   */
  private Properties getProperties() {
    if (this.properties == null) {
      this.properties = newProperties();
    }
    return this.properties;
  }

  /**
   * @return {@link Properties} used to lookup namingStrategy alias.
   */
  protected Properties newProperties() {
    return new Properties();
  }

  /**
   * @param props
   *          {@link Properties} containing configured alias.
   */
  public final void setProperties(final Properties props) {
    Validate.notNull(props);
    this.properties = props;
  }

  /**
   * @return the {@link ProviderFinder} used to find all strategies.
   * @VisibleForTesting This method is not final only for testing purposes.
   */
  protected ProviderFinder<P> getProviderFinder() {
    if (providerFinder == null) {
      providerFinder = ProviderFinder.of(getProviderClass());
    }
    return providerFinder;
  }

  /**
   * @return the class of the provider of type P. Uses {@link ParameterizedType} to compute the class. Override it to
   *         support anonymous classes which do not play well with {@link ParameterizedType}'s.
   */
  @SuppressWarnings("unchecked")
  protected Class<P> getProviderClass() {
    final Type type = getClass().getGenericSuperclass();
    return (Class<P>) ((ParameterizedType) type).getActualTypeArguments()[1];
  }

  /**
   * @param provider
   *          the instance responsible for strategy lookup.
   * @return the map of provided strategies.
   */
  protected abstract Map<String, S> getStrategies(final P provider);

  /**
   * @return the key of the strategy property.
   */
  protected abstract String getStrategyKey();
}
