package org.infinispan.configuration.cache;

import static java.util.Arrays.asList;
import static org.infinispan.commons.dataconversion.MediaType.APPLICATION_OBJECT_TYPE;
import static org.infinispan.configuration.cache.Configuration.SIMPLE_CACHE;

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.infinispan.commons.CacheConfigurationException;
import org.infinispan.commons.configuration.Builder;
import org.infinispan.commons.configuration.ConfigurationUtils;
import org.infinispan.commons.configuration.attributes.AttributeSet;
import org.infinispan.configuration.global.GlobalConfiguration;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

public class ConfigurationBuilder implements ConfigurationChildBuilder {
   private static final Log log = LogFactory.getLog(ConfigurationBuilder.class, Log.class);

   private final ClusteringConfigurationBuilder clustering;
   private final CustomInterceptorsConfigurationBuilder customInterceptors;
   private final DataContainerConfigurationBuilder dataContainer;
   private final DeadlockDetectionConfigurationBuilder deadlockDetection;
   private final EncodingConfigurationBuilder encoding;
   private final EvictionConfigurationBuilder eviction;
   private final ExpirationConfigurationBuilder expiration;
   private final IndexingConfigurationBuilder indexing;
   private final InvocationBatchingConfigurationBuilder invocationBatching;
   private final JMXStatisticsConfigurationBuilder jmxStatistics;
   private final PersistenceConfigurationBuilder persistence;
   private final LockingConfigurationBuilder locking;
   private final SecurityConfigurationBuilder security;
   private final StoreAsBinaryConfigurationBuilder storeAsBinary;
   private final TransactionConfigurationBuilder transaction;
   private final VersioningConfigurationBuilder versioning;
   private final UnsafeConfigurationBuilder unsafe;
   private final List<Builder<?>> modules = new ArrayList<>();
   private final SitesConfigurationBuilder sites;
   private final CompatibilityModeConfigurationBuilder compatibility;
   private final MemoryConfigurationBuilder memory;
   private final AttributeSet attributes;

   private boolean template = false;

   public ConfigurationBuilder() {
      this.attributes = Configuration.attributeDefinitionSet();
      this.clustering = new ClusteringConfigurationBuilder(this);
      this.customInterceptors = new CustomInterceptorsConfigurationBuilder(this);
      this.dataContainer = new DataContainerConfigurationBuilder(this);
      this.deadlockDetection = new DeadlockDetectionConfigurationBuilder(this);
      this.encoding = new EncodingConfigurationBuilder(this);
      this.eviction = new EvictionConfigurationBuilder(this);
      this.expiration = new ExpirationConfigurationBuilder(this);
      this.indexing = new IndexingConfigurationBuilder(this);
      this.invocationBatching = new InvocationBatchingConfigurationBuilder(this);
      this.jmxStatistics = new JMXStatisticsConfigurationBuilder(this);
      this.persistence = new PersistenceConfigurationBuilder(this);
      this.locking = new LockingConfigurationBuilder(this);
      this.security = new SecurityConfigurationBuilder(this);
      this.storeAsBinary = new StoreAsBinaryConfigurationBuilder(this);
      this.transaction = new TransactionConfigurationBuilder(this);
      this.versioning = new VersioningConfigurationBuilder(this);
      this.unsafe = new UnsafeConfigurationBuilder(this);
      this.sites = new SitesConfigurationBuilder(this);
      this.compatibility = new CompatibilityModeConfigurationBuilder(this);
      this.memory = new MemoryConfigurationBuilder(this);
   }

   @Override
   public ConfigurationBuilder simpleCache(boolean simpleCache) {
      attributes.attribute(SIMPLE_CACHE).set(simpleCache);
      return this;
   }

   @Override
   public boolean simpleCache() {
      return attributes.attribute(SIMPLE_CACHE).get();
   }

   @Override
   public ClusteringConfigurationBuilder clustering() {
      return clustering;
   }

   @Override
   public CustomInterceptorsConfigurationBuilder customInterceptors() {
      return customInterceptors;
   }

   @Override
   public DataContainerConfigurationBuilder dataContainer() {
      return dataContainer;
   }

   /**
    * @deprecated Since 9.0, deadlock detection is always disabled.
    */
   @Deprecated
   @Override
   public DeadlockDetectionConfigurationBuilder deadlockDetection() {
      return deadlockDetection;
   }

   @Override
   public EncodingConfigurationBuilder encoding() {
      return encoding;
   }

   /**
    * @deprecated Since 9.0, Use {@link ConfigurationBuilder#memory()} instead
    */
   @Deprecated
   @Override
   public EvictionConfigurationBuilder eviction() {
      return eviction;
   }

   @Override
   public ExpirationConfigurationBuilder expiration() {
      return expiration;
   }

   @Override
   public IndexingConfigurationBuilder indexing() {
      return indexing;
   }

   @Override
   public InvocationBatchingConfigurationBuilder invocationBatching() {
      return invocationBatching;
   }

   @Override
   public JMXStatisticsConfigurationBuilder jmxStatistics() {
      return jmxStatistics;
   }

   /**
    * Deprecated since 9.0, please use {@link ConfigurationBuilder#memory()} instead.
    */
   @Deprecated
   @Override
   public StoreAsBinaryConfigurationBuilder storeAsBinary() {
      return storeAsBinary;
   }

   @Override
   public PersistenceConfigurationBuilder persistence() {
      return persistence;
   }

   @Override
   public LockingConfigurationBuilder locking() {
      return locking;
   }

   @Override
   public SecurityConfigurationBuilder security() {
      return security;
   }

   @Override
   public TransactionConfigurationBuilder transaction() {
      return transaction;
   }

   @Override
   public VersioningConfigurationBuilder versioning() {
      return versioning;
   }

   @Override
   public UnsafeConfigurationBuilder unsafe() {
      return unsafe;
   }

   @Override
   public SitesConfigurationBuilder sites() {
      return sites;
   }

   @Override
   @Deprecated
   public CompatibilityModeConfigurationBuilder compatibility() {
      return compatibility;
   }

   @Override
   public MemoryConfigurationBuilder memory() { return memory; }

   public List<Builder<?>> modules() {
      return modules;
   }

   public ConfigurationBuilder clearModules() {
      modules.clear();
      return this;
   }

   public <T extends Builder<?>> T addModule(Class<T> klass) {
      try {
         Constructor<T> constructor = klass.getDeclaredConstructor(ConfigurationBuilder.class);
         T builder = constructor.newInstance(this);
         this.modules.add(builder);
         return builder;
      } catch (Exception e) {
         throw new CacheConfigurationException("Could not instantiate module configuration builder '" + klass.getName() + "'", e);
      }
   }

   @Override
   public ConfigurationBuilder template(boolean template) {
      this.template = template;
      return this;
   }

   public boolean template() {
      return template;
   }

   @SuppressWarnings("unchecked")
   public void validate() {
      if (attributes.attribute(SIMPLE_CACHE).get()) {
         validateSimpleCacheConfiguration();
      }
      List<RuntimeException> validationExceptions = new ArrayList<>();
      for (Builder<?> validatable:
            asList(clustering, customInterceptors, dataContainer, deadlockDetection, eviction, expiration, indexing,
                   invocationBatching, jmxStatistics, persistence, locking, storeAsBinary, transaction,
                   versioning, unsafe, sites, compatibility, memory)) {
         try {
            validatable.validate();
         } catch (RuntimeException e) {
            validationExceptions.add(e);
         }
      }
      for (Builder<?> m : modules) {
         try {
            m.validate();
         } catch (RuntimeException e) {
            validationExceptions.add(e);
         }
      }
      CacheConfigurationException.fromMultipleRuntimeExceptions(validationExceptions).ifPresent(e -> { throw e; });
   }

   private void validateSimpleCacheConfiguration() {
      if (clustering().cacheMode().isClustered()
            || (transaction.transactionMode() != null && transaction.transactionMode().isTransactional())
            || !customInterceptors.create().interceptors().isEmpty()
            || !persistence.stores().isEmpty()
            || invocationBatching.isEnabled()
            || indexing.enabled()
            || memory.create().storageType() == StorageType.BINARY
            || compatibility.create().enabled()) {
         throw log.notSupportedInSimpleCache();
      }
   }

   @Override
   public void validate(GlobalConfiguration globalConfig) {
      List<RuntimeException> validationExceptions = new ArrayList<>();
      for (ConfigurationChildBuilder validatable:
            asList(clustering, customInterceptors, dataContainer, deadlockDetection, eviction, expiration, indexing,
                   invocationBatching, jmxStatistics, persistence, locking, storeAsBinary, transaction,
                   versioning, unsafe, sites, compatibility, security, memory)) {
         try {
            validatable.validate(globalConfig);
         } catch (RuntimeException e) {
            validationExceptions.add(e);
         }
      }
      // Modules cannot be checked with GlobalConfiguration
      CacheConfigurationException.fromMultipleRuntimeExceptions(validationExceptions).ifPresent(e -> { throw e; });
   }

   @Override
   public Configuration build() {
      return build(true);
   }

   public Configuration build(GlobalConfiguration globalConfiguration) {
      validate(globalConfiguration);
      return build(true);
   }

   public Configuration build(boolean validate) {
      if (validate) {
         validate();
      }
      if (compatibility.isEnabled()) {
         encoding.key().mediaType(APPLICATION_OBJECT_TYPE);
         encoding.value().mediaType(APPLICATION_OBJECT_TYPE);
      }
      List<Object> modulesConfig = new LinkedList<>();
      for (Builder<?> module : modules)
         modulesConfig.add(module.create());
      return new Configuration(template, attributes.protect(), clustering.create(), customInterceptors.create(),
               dataContainer.create(), deadlockDetection.create(), eviction.create(),
               expiration.create(), encoding.create(), indexing.create(), invocationBatching.create(),
               jmxStatistics.create(), persistence.create(), locking.create(), security.create(),
               storeAsBinary.create(), transaction.create(), unsafe.create(), versioning.create(), sites.create(),
               compatibility.create(), memory.create(),
               modulesConfig);
   }

   public ConfigurationBuilder read(Configuration template) {
      this.attributes.read(template.attributes());
      this.clustering.read(template.clustering());
      this.customInterceptors.read(template.customInterceptors());
      this.dataContainer.read(template.dataContainer());
      this.deadlockDetection.read(template.deadlockDetection());
      this.eviction.read(template.eviction());
      this.expiration.read(template.expiration());
      this.indexing.read(template.indexing());
      this.invocationBatching.read(template.invocationBatching());
      this.jmxStatistics.read(template.jmxStatistics());
      this.persistence.read(template.persistence());
      this.locking.read(template.locking());
      this.security.read(template.security());
      this.storeAsBinary.read(template.storeAsBinary());
      this.transaction.read(template.transaction());
      this.unsafe.read(template.unsafe());
      this.sites.read(template.sites());
      this.versioning.read(template.versioning());
      this.compatibility.read(template.compatibility());
      this.memory.read(template.memory());
      this.encoding.read(template.encoding());

      for (Object c : template.modules().values()) {
         Builder<Object> builder = this.addModule(ConfigurationUtils.builderFor(c));
         builder.read(c);
      }

      return this;
   }

   @Override
   public String toString() {
      return "ConfigurationBuilder{" +
            "clustering=" + clustering +
            ", customInterceptors=" + customInterceptors +
            ", dataContainer=" + dataContainer +
            ", deadlockDetection=" + deadlockDetection +
            ", eviction=" + eviction +
            ", expiration=" + expiration +
            ", indexing=" + indexing +
            ", invocationBatching=" + invocationBatching +
            ", jmxStatistics=" + jmxStatistics +
            ", persistence=" + persistence +
            ", locking=" + locking +
            ", modules=" + modules +
            ", security=" + security +
            ", storeAsBinary=" + storeAsBinary +
            ", transaction=" + transaction +
            ", versioning=" + versioning +
            ", unsafe=" + unsafe +
            ", sites=" + sites +
            ", compatibility=" + compatibility +
            '}';
   }

}
