package org.infinispan.configuration.cache;

import static org.infinispan.commons.configuration.AbstractTypedPropertiesConfiguration.PROPERTIES;
import static org.infinispan.configuration.cache.InterceptorConfiguration.AFTER;
import static org.infinispan.configuration.cache.InterceptorConfiguration.BEFORE;
import static org.infinispan.configuration.cache.InterceptorConfiguration.INDEX;
import static org.infinispan.configuration.cache.InterceptorConfiguration.INTERCEPTOR;
import static org.infinispan.configuration.cache.InterceptorConfiguration.INTERCEPTOR_CLASS;
import static org.infinispan.configuration.cache.InterceptorConfiguration.POSITION;

import java.util.Properties;

import org.infinispan.commons.CacheConfigurationException;
import org.infinispan.commons.configuration.Builder;
import org.infinispan.commons.configuration.attributes.Attribute;
import org.infinispan.commons.configuration.attributes.AttributeSet;
import org.infinispan.commons.util.TypedProperties;
import org.infinispan.configuration.cache.InterceptorConfiguration.Position;
import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.interceptors.AsyncInterceptor;
import org.infinispan.interceptors.BaseCustomAsyncInterceptor;
import org.infinispan.interceptors.base.BaseCustomInterceptor;
import org.infinispan.interceptors.base.CommandInterceptor;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * This builder defines details of a specific custom interceptor.
 */
public class InterceptorConfigurationBuilder extends AbstractCustomInterceptorsConfigurationChildBuilder implements Builder<InterceptorConfiguration> {
   private static final Log log = LogFactory.getLog(InterceptorConfigurationBuilder.class);
   private final AttributeSet attributes;

   InterceptorConfigurationBuilder(CustomInterceptorsConfigurationBuilder builder) {
      super(builder);
      attributes = InterceptorConfiguration.attributeDefinitionSet();
   }

   /**
    * Dictates that the custom interceptor appears immediately <i>after</i> the specified interceptor. If the specified
    * interceptor is not found in the interceptor chain, a {@link CacheConfigurationException} will be thrown when the
    * cache starts.
    *
    * @param after the class of the interceptor to look for
    */
   public InterceptorConfigurationBuilder after(Class<? extends AsyncInterceptor> after) {
      attributes.attribute(AFTER).set(after);
      return this;
   }

   /**
    * Dictates that the custom interceptor appears immediately <i>before</i> the specified interceptor. If the specified
    * interceptor is not found in the interceptor chain, a {@link CacheConfigurationException} will be thrown when the
    * cache starts.
    *
    * @param before the class of the interceptor to look for
    */
   public InterceptorConfigurationBuilder before(Class<? extends AsyncInterceptor> before) {
      attributes.attribute(BEFORE).set(before);
      return this;
   }

   /**
    * Class of the new custom interceptor to add to the configuration.
    * @param interceptorClass an instance of {@link AsyncInterceptor}
    */
   public InterceptorConfigurationBuilder interceptorClass(Class<? extends AsyncInterceptor> interceptorClass) {
      attributes.attribute(INTERCEPTOR_CLASS).set(interceptorClass);
      return this;
   }

   /**
    * @deprecated Since 9.0, please use {@link #interceptor(AsyncInterceptor)} instead.
    */
   @Deprecated
   public InterceptorConfigurationBuilder interceptor(CommandInterceptor interceptor) {
      attributes.attribute(INTERCEPTOR).set(interceptor);
      return this;
   }

   /**
    * An instance of the new custom interceptor to add to the configuration.
    * Warning: if you use this configuration for multiple caches, the interceptor instance will
    * be shared, which will corrupt interceptor stack. Use {@link #interceptorClass} instead.
    *
    * @param interceptor an instance of {@link AsyncInterceptor}
    */
   public InterceptorConfigurationBuilder interceptor(AsyncInterceptor interceptor) {
      attributes.attribute(INTERCEPTOR).set(interceptor);
      return this;
   }

   /**
    * Specifies a position in the interceptor chain to place the new interceptor.  The index starts at 0 and goes up to
    * the number of interceptors in a given configuration.  An {@link IllegalArgumentException} is thrown if the index is
    * less than 0 or greater than the maximum number of interceptors in the chain.
    *
    * @param i positional index in the interceptor chain to place the new interceptor.
    */
   public InterceptorConfigurationBuilder index(int i) {
      if (i < 0) throw new IllegalArgumentException("Index cannot be negative");
      attributes.attribute(INDEX).set(i);
      return this;
   }

   /**
    * Specifies a position, denoted by the {@link Position} enumeration, where to place the new interceptor.
    *
    * @param p position to place the new interceptor
    */
   public InterceptorConfigurationBuilder position(Position p) {
      attributes.attribute(POSITION).set(p);
      return this;
   }

   /**
    * Sets interceptor properties
    *
    * @return this InterceptorConfigurationBuilder
    */
   public InterceptorConfigurationBuilder withProperties(Properties properties) {
      attributes.attribute(PROPERTIES).set(TypedProperties.toTypedProperties(properties));
      return this;
   }

   /**
    * Clears the interceptor properties
    *
    * @return this InterceptorConfigurationBuilder
    */
   public InterceptorConfigurationBuilder clearProperties() {
      TypedProperties properties = attributes.attribute(PROPERTIES).get();
      properties.clear();
      attributes.attribute(PROPERTIES).set(TypedProperties.toTypedProperties(properties));
      return this;
   }

   public InterceptorConfigurationBuilder addProperty(String key, String value) {
      TypedProperties properties = attributes.attribute(PROPERTIES).get();
      properties.put(key, value);
      attributes.attribute(PROPERTIES).set(TypedProperties.toTypedProperties(properties));
      return this;
   }

   public InterceptorConfigurationBuilder removeProperty(String key) {
      TypedProperties properties = attributes.attribute(PROPERTIES).get();
      properties.remove(key);
      attributes.attribute(PROPERTIES).set(TypedProperties.toTypedProperties(properties));
      return this;
   }

   @Override
   public void validate() {
      Attribute<Class> interceptorClassAttribute = attributes.attribute(INTERCEPTOR_CLASS);
      Attribute<AsyncInterceptor> interceptorAttribute = attributes.attribute(INTERCEPTOR);


      if (!interceptorClassAttribute.isNull() && !interceptorAttribute.isNull()) {
         throw log.interceptorClassAndInstanceDefined(interceptorClassAttribute.get().getName(), interceptorAttribute.get().toString());
      } else if (interceptorClassAttribute.isNull() && interceptorAttribute.isNull()) {
         throw log.customInterceptorMissingClass();
      }
      Class<? extends AsyncInterceptor> interceptorClass = interceptorClassAttribute.get();
      if (interceptorClass == null) {
         interceptorClass = interceptorAttribute.get().getClass();
      }

      if (!BaseCustomInterceptor.class.isAssignableFrom(interceptorClass) &&
            !BaseCustomAsyncInterceptor.class.isAssignableFrom(interceptorClass)) {
         final String className = interceptorClass.getName();
         //Suppress noisy warnings if the interceptor is one of our own (like one of those from Query):
         if (! className.startsWith("org.infinispan.")) {
            log.suggestCustomInterceptorInheritance(className);
         }
      }

      // Make sure more than one 'position' isn't picked.
      int positions = 0;

      if (!attributes.attribute(BEFORE).isNull()) positions++;
      if (!attributes.attribute(AFTER).isNull()) positions++;
      if (attributes.attribute(INDEX).get() > -1) positions++;
      if (attributes.attribute(POSITION).isModified()) positions++;

      switch (positions) {
         case 0:
            throw log.missingCustomInterceptorPosition(interceptorClass.getName());
         case 1:
            break;
         default:
            throw log.multipleCustomInterceptorPositions(interceptorClass.getName());
      }
   }

   @Override
   public void validate(GlobalConfiguration globalConfig) {
   }

   @Override
   public InterceptorConfiguration create() {
      return new InterceptorConfiguration(attributes.protect());
   }

   @Override
   public InterceptorConfigurationBuilder read(InterceptorConfiguration template) {
      attributes.read(template.attributes());
      return this;
   }

   @Override
   public String toString() {
      return "InterceptorConfigurationBuilder [attributes=" + attributes + "]";
   }
}
