/*
 * 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.xmlcache;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.logging.log4j.Logger;

import org.apache.geode.annotations.Immutable;
import org.apache.geode.logging.internal.log4j.api.LogService;

/**
 * Helper class for CacheXmlPropertyResolver. Helps in parsing ${...${}..}..${} strings.
 *
 * @since GemFire 6.6
 */
public class CacheXmlPropertyResolverHelper {
  private static final Logger logger = LogService.getLogger();

  public static final String DEFAULT_PROPERTY_STRING_PREFIX = "${";

  public static final String DEFAULT_PROPERTY_STRING_SUFFIX = "}";

  public static final String DEFAULT_PREFIX_FOR_SUFFIX = "{";
  /**
   * This <code>HashMap </code> contains valid suffixes and prefixes to be parsed by
   * {@link CacheXmlPropertyResolverHelper} like {}, [] or ().
   */
  @Immutable
  private static final Map<String, String> validSuffixAndPrefixes;

  static {
    Map<String, String> map = new HashMap<>();
    map.put("}", "{");
    map.put("]", "[");
    map.put(")", "(");
    validSuffixAndPrefixes = Collections.unmodifiableMap(map);
  }
  /* String specifying the suffice for property key prefix */
  private String propertyPrefix = DEFAULT_PROPERTY_STRING_PREFIX;

  /* String specifying the suffice for property key suffix */
  private String propertySuffix = DEFAULT_PROPERTY_STRING_SUFFIX;

  private String prefixForSuffix = DEFAULT_PREFIX_FOR_SUFFIX;

  public CacheXmlPropertyResolverHelper(String propPrefix, String propSuffix) {
    if (propPrefix != null && propSuffix != null) {
      String validPrefix = validSuffixAndPrefixes.get(propSuffix);
      if (validPrefix != null && propPrefix.endsWith(validPrefix)) {
        prefixForSuffix = validPrefix;
      } else {
        prefixForSuffix = propPrefix;
      }
      propertyPrefix = propPrefix;

      propertySuffix = propSuffix;
    }
  }

  /**
   * Parses the given string which are supposed to be like ${} for system and/or Gemfire properties
   * to be replaced. This will return property.name from ${property.name}.
   *
   */
  protected String parseResolvablePropString(String unparsedString, PropertyResolver resolver,
      Set<String> visitedReplaceableStrings) {
    StringBuilder buf = new StringBuilder(unparsedString);
    int prefixIndex = buf.indexOf(propertyPrefix);

    while (prefixIndex != -1) {
      int suffixIndex = findSuffixIndex(buf, prefixIndex + propertyPrefix.length());
      if (suffixIndex != -1) {
        String replaceableString =
            buf.substring(prefixIndex + propertyPrefix.length(), suffixIndex);
        // Check for circular references
        if (!visitedReplaceableStrings.add(replaceableString)) {
          logger.info(
              "Some still unresolved string {} was replaced by resolver, leading to circular references.",
              replaceableString);
          throw new IllegalArgumentException("Some still unresolved string " + replaceableString
              + " was replaced by resolver, leading to circular references.");
        }
        /* Find the replacement using given <code>resolver</code> */
        replaceableString =
            parseResolvablePropString(replaceableString, resolver, visitedReplaceableStrings);
        String replacement = resolver.resolveReplaceString(replaceableString);

        if (replacement != null) {
          /*
           * put replacement in <code>unparsedString</code> and call
           * <code>parseResolvablePropString</code> recursively to find more unparsedStrings in the
           * replaced value of given unparsedString.
           */
          replacement = parseResolvablePropString(replacement, resolver, visitedReplaceableStrings);
          buf.replace(prefixIndex, suffixIndex + propertySuffix.length(), replacement);
          prefixIndex = buf.indexOf(propertyPrefix, prefixIndex + replacement.length());
        } else if (resolver.isIgnoreUnresolvedProperties()) {
          /* Look for more replaceable strings in given <code>unparsedString</code>. */
          prefixIndex = buf.indexOf(propertyPrefix, suffixIndex + propertySuffix.length());
        } else {
          throw new IllegalArgumentException(
              "No replacement found for property : " + replaceableString);
        }
        // Before iterating again remove replaceable string from visitedReplaceableStrings as it can
        // appear again.
        visitedReplaceableStrings.remove(replaceableString);
      } else {
        prefixIndex = -1;
      }
    }

    return buf.toString();
  }

  /**
   * Finds index of suffix in a string from a specified index. Like finds index of "}" in string
   * "${my.prop.name}" starting from index 2, which is 14.
   *
   */
  private int findSuffixIndex(StringBuilder buf, int index) {
    int inNestedProperty = 0;
    while (index < buf.length()) {
      if (buf.substring(index, index + propertySuffix.length())
          .equalsIgnoreCase(propertySuffix)) {
        if (inNestedProperty > 0) {
          inNestedProperty--;
          index = index + propertySuffix.length();
        } else {
          return index;
        }
      } else if (buf.substring(index, index + prefixForSuffix.length())
          .equalsIgnoreCase(prefixForSuffix)) {
        inNestedProperty++;
        index = index + prefixForSuffix.length();
      } else {
        index++;
      }
    }
    return -1;
  }
}
