/**
 * Activation strategy that enables features for a given percentage of users. This strategy is
 * typically used to implement gradual rollouts. The implementation is based on a hashcode created
 * from the name of the acting user which is calculated by {@link #calculateHashCode(FeatureUser)}.
 *
 * @author Christian Kaltepoth
 */
public class GradualActivationStrategy implements ActivationStrategy {

  private final Log log = LogFactory.getLog(GradualActivationStrategy.class);

  public static final String ID = "gradual";
  public static final String PARAM_PERCENTAGE = "percentage";

  @Override
  public String getId() {
    return ID;
  }

  @Override
  public String getName() {
    return "Gradual rollout";
  }

  @Override
  public boolean isActive(FeatureState state, FeatureUser user) {

    if (user != null && Strings.isNotBlank(user.getName())) {

      String percentageAsString = state.getParameter(PARAM_PERCENTAGE);
      try {

        int percentage = Integer.valueOf(percentageAsString);

        if (percentage > 0) {
          int hashCode = Math.abs(calculateHashCode(user, state.getFeature()));
          return (hashCode % 100) < percentage;
        }

      } catch (NumberFormatException e) {
        log.error(
            "Invalid gradual rollout percentage for feature "
                + state.getFeature().name()
                + ": "
                + percentageAsString);
      }
    }

    return false;
  }

  /** @deprecated Use {@link #calculateHashCode(FeatureUser, Feature)} instead */
  @Deprecated
  protected int calculateHashCode(FeatureUser user) {
    return calculateHashCode(user, null);
  }

  protected int calculateHashCode(FeatureUser user, Feature feature) {

    Validate.notNull(user, "user is required");

    return new StringBuilder()
        .append(user.getName().toLowerCase(Locale.ENGLISH).trim())
        .append(":")
        .append(feature != null ? feature.name() : "")
        .toString()
        .hashCode();
  }

  @Override
  public Parameter[] getParameters() {
    return new Parameter[] {
      ParameterBuilder.create(PARAM_PERCENTAGE)
          .label("Percentage")
          .matching("\\d{1,3}")
          .description(
              "Percentage of users for which the feature should be active (i.e. '25' for every fourth user).")
    };
  }
}
public class ScriptEngineActivationStrategy implements ActivationStrategy {

  private final Log log = LogFactory.getLog(ScriptEngineActivationStrategy.class);

  public static final String ID = "script";
  public static final String PARAM_SCRIPT = "script";
  public static final String PARAM_LANG = "lang";

  private final ScriptEngineManager engineManager;

  public ScriptEngineActivationStrategy() {
    engineManager = new ScriptEngineManager();
  }

  @Override
  public String getId() {
    return ID;
  }

  @Override
  public String getName() {
    return "Java Scripting API";
  }

  @Override
  public boolean isActive(FeatureState featureState, FeatureUser user) {

    String lang = featureState.getParameter(PARAM_LANG);
    String script = featureState.getParameter(PARAM_SCRIPT);

    ScriptEngine engine = engineManager.getEngineByName(lang);
    if (engine == null) {
      log.error("Could not find script engine for: " + lang);
      return false;
    }

    engine.put("user", user);
    engine.put("date", new Date());
    try {

      Object result = engine.eval(script);
      if (result instanceof Boolean) {
        return ((Boolean) result).booleanValue();
      }

    } catch (ScriptException e) {
      log.error(
          "Could not evaluate script for feature "
              + featureState.getFeature().name()
              + ": "
              + e.getMessage());
    }
    return false;
  }

  @Override
  public Parameter[] getParameters() {
    return new Parameter[] {
      new ScriptLanguageParameter(engineManager),
      ParameterBuilder.create(PARAM_SCRIPT)
          .label("Script")
          .largeText()
          .description(
              "The script to check if the feature is active. "
                  + "The script context provides access to some default objects. "
                  + "The variable 'user' refers to the current acting FeatureUser "
                  + "and 'date' to the current time represented as a java.util.Date.")
    };
  }

  private static class ScriptLanguageParameter implements Parameter {

    private List<String> languages = new ArrayList<String>();

    public ScriptLanguageParameter(ScriptEngineManager engineManager) {
      for (ScriptEngineFactory factory : engineManager.getEngineFactories()) {
        languages.add(factory.getLanguageName());
      }
    }

    @Override
    public String getName() {
      return PARAM_LANG;
    }

    @Override
    public String getLabel() {
      return "Language";
    }

    @Override
    public String getDescription() {
      return "The script language to use. Your system seems to support the following languages: "
          + Strings.join(languages, ", ");
    }

    @Override
    public boolean isOptional() {
      return false;
    }

    @Override
    public boolean isLargeText() {
      return false;
    }

    @Override
    public boolean isValid(String value) {
      return Strings.isNotBlank(value) && languages.contains(value);
    }
  }
}