/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package org.apache.geode.internal.cache;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.AttributesFactory;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.DuplicatePrimaryPartitionException;
import org.apache.geode.cache.EvictionAttributes;
import org.apache.geode.cache.FixedPartitionAttributes;
import org.apache.geode.cache.PartitionAttributes;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.Scope;
import org.apache.geode.internal.Assert;
import org.apache.geode.logging.internal.log4j.api.LogService;

public class PartitionRegionConfigValidator {

  private static final Logger logger = LogService.getLogger();

  private final PartitionedRegion pr;

  // Incompatible LRU memory eviction attributes maximum message fragment, for
  // tests
  public static final String EVICTION_ATTRIBUTE_MAXIMUM_MEMORY_MESSAGE =
      " the Eviction Attribute for maximum memory, ";

  // Incompatible LRU entry eviction attributes maximum message fragment, for
  // tests
  public static final String EVICTION_ATTRIBUTE_MAXIMUM_ENTRIES_MESSAGE =
      " the Eviction Attribute for maximum entries, ";

  // Incompatible eviction attributes exception message fragment, for tests
  public static final String EVICTION_ATTRIBUTES_ARE_INCOMPATIBLE_MESSAGE =
      " is incompatible with other VMs which have EvictionAttributes ";

  public PartitionRegionConfigValidator(PartitionedRegion pr) {
    this.pr = pr;
  }

  /**
   * This method validates the PartitionedAttributes that user provided PR Attributes with PR
   * Attributes set in PR Config obtained from global meta-data allPartitionedRegion region
   */
  void validatePartitionAttrsFromPRConfig(PartitionRegionConfig prconf) {
    final PartitionAttributes prconfPA = prconf.getPartitionAttrs();
    final PartitionAttributes userPA = pr.getAttributes().getPartitionAttributes();

    if (userPA.getTotalSize() != prconfPA.getTotalSize()) {
      throw new IllegalStateException(
          String.format(
              "Total size in PartitionAttributes is incompatible with globally set total size. Set the total size to %sMB.",
              prconfPA.getTotalSize()));
    }
    if (userPA.getRedundantCopies() != prconfPA.getRedundantCopies()) {
      throw new IllegalStateException(
          String.format("Requested redundancy %s is incompatible with existing redundancy %s",
              userPA.getRedundantCopies(),
              prconfPA.getRedundantCopies()));
    }

    if (prconf.isFirstDataStoreCreated() && pr.isDataStore()) {
      validateDistributedEvictionAttributes(prconf.getEvictionAttributes());
    }

    Scope prconfScope = prconf.getScope();
    Scope myScope = pr.getScope();
    if (!myScope.equals(prconfScope)) {
      throw new IllegalStateException(
          String.format(
              "Scope in PartitionAttributes is incompatible with already set scope.Set the scope to %s .",
              prconfScope));
    }

    final int prconfTotalNumBuckets = prconfPA.getTotalNumBuckets();
    if (userPA.getTotalNumBuckets() != prconfTotalNumBuckets) {
      throw new IllegalStateException(
          String.format(
              "The total number of buckets found in PartitionAttributes ( %s ) is incompatible with the total number of buckets used by other distributed members. Set the number of buckets to %s",
              userPA.getTotalNumBuckets(),
              prconfTotalNumBuckets));
    }
    validatePartitionListeners(prconf, userPA);
    validatePartitionResolver(prconf, userPA);
    validateColocatedWith(prconf, userPA);
    validateExpirationAttributes(pr.getAttributes(), prconf);
  }

  private void validatePartitionListeners(final PartitionRegionConfig prconf,
      final PartitionAttributes userPA) {

    ArrayList<String> prconfList = prconf.getPartitionListenerClassNames();

    if (userPA.getPartitionListeners() == null && userPA.getPartitionListeners().length == 0
        && prconfList != null) {
      throw new IllegalStateException(
          String.format(
              "The PartitionListeners=%s are incompatible with the PartitionListeners=%s used by other distributed members.",
              null, prconfList));
    }
    if (userPA.getPartitionListeners() != null && prconfList != null) {
      ArrayList<String> userPRList = new ArrayList<>();
      for (int i = 0; i < userPA.getPartitionListeners().length; i++) {
        userPRList.add(userPA.getPartitionListeners()[i].getClass().getName());
      }

      if (userPA.getPartitionListeners().length != prconfList.size()) {
        throw new IllegalStateException(
            String.format(
                "The PartitionListeners=%s are incompatible with the PartitionListeners=%s used by other distributed members.",
                userPRList, prconfList));
      }

      for (String listener : prconfList) {
        if (!(userPRList.contains(listener))) {
          throw new IllegalStateException(
              String.format(
                  "The PartitionListeners=%s are incompatible with the PartitionListeners=%s used by other distributed members.",
                  userPRList, prconfList));
        }
      }
    }
  }

  private void validatePartitionResolver(final PartitionRegionConfig prconf,
      final PartitionAttributes userPA) {
    /*
     * if (userPA.getPartitionResolver() == null && prconf.getPartitionResolverClassName() != null)
     * { throw new IllegalStateException(
     * String.
     * format("The PartitionResolver=%s is incompatible with the PartitionResolver=%s used by other distributed members."
     * ,
     * new Object[] { "null", prconf.getPartitionResolverClassName() })); }
     */
    if (userPA.getPartitionResolver() != null && prconf.getResolverClassName() != null) {
      if (!(prconf.getResolverClassName()
          .equals(userPA.getPartitionResolver().getClass().getName()))) {
        throw new IllegalStateException(
            String.format(
                "The PartitionResolver=%s is incompatible with the PartitionResolver=%s used by other distributed members.",
                userPA.getPartitionResolver().getClass().getName(),
                prconf.getResolverClassName()));
      }
    }
  }

  private void validateColocatedWith(final PartitionRegionConfig prconf,
      final PartitionAttributes userPA) {
    if (userPA.getColocatedWith() == null && prconf.getColocatedWith() != null) {
      throw new IllegalStateException(
          String.format(
              "The colocatedWith=%s found in PartitionAttributes is incompatible with the colocatedWith=%s used by other distributed members.",
              "null", prconf.getColocatedWith()));

    }
    if (userPA.getColocatedWith() != null && prconf.getColocatedWith() != null) {
      if (!(prconf.getColocatedWith().equals(userPA.getColocatedWith()))) {
        throw new IllegalStateException(
            String.format(
                "The colocatedWith=%s found in PartitionAttributes is incompatible with the colocatedWith=%s used by other distributed members.",

                userPA.getColocatedWith(), prconf.getColocatedWith()));
      }
    }
  }

  private void validateExpirationAttributes(final RegionAttributes userRA,
      final PartitionRegionConfig prconf) {
    if (!userRA.getRegionIdleTimeout().equals(prconf.getRegionIdleTimeout())) {
      throw new IllegalStateException(
          String.format(
              "The %1$s set in RegionAttributes is incompatible with %1$s used by other distributed members.",
              " region idle timout "));
    }
    if (!userRA.getRegionTimeToLive().equals(prconf.getRegionTimeToLive())) {
      throw new IllegalStateException(
          String.format(
              "The %1$s set in RegionAttributes is incompatible with %1$s used by other distributed members.",
              " region time to live "));
    }
    if (!userRA.getEntryIdleTimeout().equals(prconf.getEntryIdleTimeout())) {
      throw new IllegalStateException(
          String.format(
              "The %1$s set in RegionAttributes is incompatible with %1$s used by other distributed members.",
              " entry idle timout "));
    }
    if (!userRA.getEntryTimeToLive().equals(prconf.getEntryTimeToLive())) {
      throw new IllegalStateException(
          String.format(
              "The %1$s set in RegionAttributes is incompatible with %1$s used by other distributed members.",
              " entry time to live "));
    }
  }

  /**
   * The 2nd step of Eviction Attributes validation to ensure that all VMs are reasonably similar to
   * prevent weird config. issues.
   *
   * @param prconfEa the eviction attributes currently used by other VMs
   * @see AttributesFactory#validateAttributes(RegionAttributes)
   * @see #validateEvictionAttributesAgainstLocalMaxMemory()
   */
  private void validateDistributedEvictionAttributes(final EvictionAttributes prconfEa) {
    final EvictionAttributes ea = pr.getAttributes().getEvictionAttributes();
    // there is no such thing as null EvictionAttributes, assert that is true
    Assert.assertTrue(ea != null);
    Assert.assertTrue(prconfEa != null);

    // Enforce that all VMs with this PR have the same Eviction Attributes
    // Even an accessor should do this to stay consistent with all other
    // accessor validation/enforcement
    // *and* because an accessor can set the first/global eviction attributes
    // If this is an accessor with Evicitonttributes, log an info message
    // indicating that no eviction will
    // occur in this VM (duh)
    // Further validation should occur for datastores to ensure consistent
    // behavior wrt local max memory and
    // total number of buckets
    final boolean equivAlgoAndAction = ea.getAlgorithm().equals(prconfEa.getAlgorithm())
        && ea.getAction().equals(prconfEa.getAction());

    if (!equivAlgoAndAction) {
      throw new IllegalStateException(
          "For Partitioned Region " + pr.getFullPath() + " the configured EvictionAttributes " + ea
              + EVICTION_ATTRIBUTES_ARE_INCOMPATIBLE_MESSAGE + prconfEa);
    } else {
      // Same algo, action...
      // It is ok to have disparate heap or memory sizes since different
      // VMs may have different heap or memory sizes, particularly if
      // the action is overflow, but...
      // It *is* dangerous to locally destroy entries if all VMs don't have the
      // same maximum
      // basically the VM with the smallest maximum may cause erroneous misses
      // to occur. Warn the user,
      // but allow the configuration.
      if (ea.getAction().isLocalDestroy()) {
        // LRUHeap doesn't support maximum, but other eviction algos do
        if (!ea.getAlgorithm().isLRUHeap() && ea.getMaximum() != prconfEa.getMaximum()) {
          logger.warn(
              "For Partitioned Region {} the locally configured EvictionAttributes {} do not match with other EvictionAttributes {} and may cause misses during reads from VMs with smaller maximums.",
              new Object[] {pr.getFullPath(), ea, prconfEa});
        }
      }
    } // end Same algo, action...
  }

  /**
   * The 3rd step of EvictionAttributes validation, where mutation is acceptible This should be done
   * before buckets are created. Validate EvictionAttributes with respect to localMaxMemory
   * potentially changing the eviction attributes.
   *
   * @see AttributesFactory#validateAttributes(RegionAttributes)
   * @see #validateDistributedEvictionAttributes(EvictionAttributes)
   */
  void validateEvictionAttributesAgainstLocalMaxMemory() {
    final EvictionAttributes ea = pr.getEvictionAttributes();
    if (pr.getLocalMaxMemory() == 0 && !ea.getAction().isNone()) {
      // This is an accessor which won't ever do eviction, say so
      logger.info(
          "EvictionAttributes {} will have no effect for Partitioned Region {} on this VM because localMaxMemory is {}.",
          new Object[] {ea, pr.getFullPath(), pr.localMaxMemory});
    }
  }

  /**
   * validates the persistence for datastores should match between members
   */
  void validatePersistentMatchBetweenDataStores(PartitionRegionConfig prconf) {
    final boolean isPersistent =
        pr.getAttributes().getDataPolicy() == DataPolicy.PERSISTENT_PARTITION;
    if (pr.getLocalMaxMemory() == 0 || prconf == null) {
      return;
    }
    Set<Node> nodes = prconf.getNodes();
    for (final Node n : nodes) {
      if (n.getPRType() != Node.ACCESSOR_DATASTORE) {
        continue;
      } else {
        if (n.isPersistent() != (pr.getAttributes()
            .getDataPolicy() == DataPolicy.PERSISTENT_PARTITION)) {
          throw new IllegalStateException(
              "DataPolicy for Datastore members should all be persistent or not.");
        }
      }
    }
  }

  void validateColocation() {
    final PartitionAttributesImpl userPA =
        (PartitionAttributesImpl) pr.getAttributes().getPartitionAttributes();

    userPA.validateColocation(pr.getCache()); // do this here to fix bug 47197

    PartitionedRegion colocatedPR = ColocationHelper.getColocatedRegion(pr);
    if (colocatedPR != null) {
      if (colocatedPR.getPartitionAttributes().getTotalNumBuckets() != userPA
          .getTotalNumBuckets()) {
        throw new IllegalStateException(
            "Colocated regions should have same number of total-num-buckets");
      }
      if (colocatedPR.getPartitionAttributes().getRedundantCopies() != userPA
          .getRedundantCopies()) {
        throw new IllegalStateException(
            "Colocated regions should have same number of redundant-copies");
      }
      if ((colocatedPR.getPartitionAttributes().getLocalMaxMemory() == 0)
          && (userPA.getLocalMaxMemory() != 0)) {
        throw new IllegalStateException("Colocated regions should have accessors at the same node");
      }
      if ((colocatedPR.getLocalMaxMemory() != 0) && (userPA.getLocalMaxMemory() == 0)) {
        throw new IllegalStateException("Colocated regions should have accessors at the same node");
      }
      if (!pr.isShadowPR()) {
        if (pr.getAttributes().getDataPolicy().withPersistence()) {
          if (!colocatedPR.getDataPolicy().withPersistence()) {
            throw new IllegalStateException(
                "Cannot colocate a persistent region with a non persistent region");
          }
        }
      }
    }
  }

  public void validateCacheLoaderWriterBetweenDataStores(PartitionRegionConfig prconf) {
    if (pr.getLocalMaxMemory() == 0 || prconf == null) {
      return;
    }
    Set<Node> nodes = prconf.getNodes();
    for (final Node n : nodes) {
      if (n.getPRType() != Node.ACCESSOR_DATASTORE) {
        continue;
      } else {
        if (n.isCacheLoaderAttached() && pr.getAttributes().getCacheLoader() == null) {
          throw new IllegalStateException(
              String.format(
                  "Incompatible CacheLoader. CacheLoader is not null in partitionedRegion %s on another datastore.",
                  pr.getName()));
        }
        if (!n.isCacheLoaderAttached() && pr.getAttributes().getCacheLoader() != null) {
          throw new IllegalStateException(
              String.format(
                  "Incompatible CacheLoader. CacheLoader is null in partitionedRegion %s on another datastore.",
                  pr.getName()));
        }
        if (n.isCacheWriterAttached() && pr.getAttributes().getCacheWriter() == null) {
          throw new IllegalStateException(
              String.format(
                  "Incompatible CacheWriter. CacheWriter is not null in partitionedRegion %s on another datastore.",
                  pr.getName()));
        }
        if (!n.isCacheWriterAttached() && pr.getAttributes().getCacheWriter() != null) {
          throw new IllegalStateException(
              String.format(
                  "Incompatible CacheWriter. CacheWriter is null in partitionedRegion %s on another datastore.",
                  pr.getName()));
        }
      }
    }
  }

  void validateFixedPartitionAttributes() {
    if (pr.getFixedPartitionAttributesImpl() != null) {
      validatePrimaryFixedPartitionAttributes();
      validateFixedPartitionAttributesAgainstRedundantCopies();
      validateFixedPartitionAttributesAgainstTotalNumberBuckets();
    }
  }

  /**
   * validate that for all partitions defined across all datastores, sum of num-buckets is not more
   * than total-num-buckets defined
   */
  private void validateFixedPartitionAttributesAgainstTotalNumberBuckets() {
    for (FixedPartitionAttributesImpl fpa : pr.getFixedPartitionAttributesImpl()) {
      int numBuckets = 0;

      Set<FixedPartitionAttributesImpl> allFPAs = new HashSet<>(
          pr.getRegionAdvisor().adviseAllFixedPartitionAttributes());
      allFPAs.add(fpa);

      for (FixedPartitionAttributes samefpa : allFPAs) {
        numBuckets = numBuckets + samefpa.getNumBuckets();
      }
      if (numBuckets > pr.getTotalNumberOfBuckets()) {
        throw new IllegalStateException(
            String.format(
                "For region %s,sum of num-buckets %s for different primary partitions should not be greater than total-num-buckets %s.",
                pr.getName(), numBuckets, pr.getTotalNumberOfBuckets()));
      }
    }
  }

  /**
   * Validate that for the given partition, number if secondaries are never exceed redundant copies
   * defined Validate that the num-buckets defined for a partition are same across all datastores
   */
  private void validateFixedPartitionAttributesAgainstRedundantCopies() {
    for (FixedPartitionAttributesImpl fpa : pr.getFixedPartitionAttributesImpl()) {
      List<FixedPartitionAttributesImpl> allSameFPAs =
          pr.getRegionAdvisor().adviseSameFPAs(fpa);
      allSameFPAs.add(fpa);

      if (!allSameFPAs.isEmpty()) {
        int numSecondaries = 0;
        for (FixedPartitionAttributes otherfpa : allSameFPAs) {
          if (fpa.getNumBuckets() != otherfpa.getNumBuckets()) {
            throw new IllegalStateException(
                String.format(
                    "For region %s,for partition %s, num-buckets are not same (%s, %s)across nodes.",
                    pr.getName(), fpa.getPartitionName(),
                    fpa.getNumBuckets(), otherfpa.getNumBuckets()));
          }

          if (!otherfpa.isPrimary()) {
            if (++numSecondaries > (pr.getRedundantCopies())) {
              throw new IllegalStateException(
                  String.format(
                      "For region %s, number of secondary partitions %s of a partition %s should never exceed number of redundant copies %s.",
                      pr.getName(), numSecondaries,
                      fpa.getPartitionName(), pr.getRedundantCopies()));
            }
          }
        }
      }
    }
  }

  /**
   * Validate that same partition is not defined as primary on more that one datastore
   */
  private void validatePrimaryFixedPartitionAttributes() {
    List<FixedPartitionAttributesImpl> remotePrimaryFPAs =
        pr.getRegionAdvisor().adviseRemotePrimaryFPAs();

    for (FixedPartitionAttributes fpa : pr.getFixedPartitionAttributesImpl()) {
      if (fpa.isPrimary() && remotePrimaryFPAs.contains(fpa)) {
        throw new DuplicatePrimaryPartitionException(
            String.format(
                "For region %s, same partition name %s can not be defined as primary on more than one node.",
                pr.getName(), fpa.getPartitionName()));
      }
    }
  }

  public void validateFixedPABetweenDataStores(PartitionRegionConfig prconf) {
    boolean isDataStore = pr.localMaxMemory > 0;
    boolean isFixedPR = pr.fixedPAttrs != null;

    Set<Node> nodes = prconf.getNodes();
    for (final Node n : nodes) {
      if (isFixedPR) {
        if (n.getPRType() == Node.DATASTORE || n.getPRType() == Node.ACCESSOR_DATASTORE) {
          // throw exception
          Object[] prms = new Object[] {pr.getName()};
          throw new IllegalStateException(
              String.format(
                  "Region %s uses fixed partitioning but at least one datastore node (localMaxMemory > 0) has no fixed partitions. Please make sure that each datastore creating this region is configured with at least one fixed partition.",
                  prms));
        }
      } else {
        if (isDataStore) {
          if (n.getPRType() == Node.FIXED_PR_ACCESSOR || n.getPRType() == Node.FIXED_PR_DATASTORE) {
            // throw Exception
            Object[] prms = new Object[] {pr.getName()};
            throw new IllegalStateException(
                String.format(
                    "Region %s uses fixed partitioning but at least one datastore node (localMaxMemory > 0) has no fixed partitions. Please make sure that each datastore creating this region is configured with at least one fixed partition.",
                    prms));
          }
        }
      }
    }
  }
}
