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

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import org.apache.hadoop.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.phoenix.thirdparty.com.google.common.base.Objects;
import org.apache.phoenix.thirdparty.com.google.common.collect.ImmutableMap;
import org.apache.phoenix.thirdparty.com.google.common.collect.Maps;

/**
 * Read-only properties that avoids unnecessary synchronization in java.util.Properties.
 */
public class ReadOnlyProps implements Iterable<Entry<String, String>> {
  private static final Logger LOGGER = LoggerFactory.getLogger(ReadOnlyProps.class);
  public static final ReadOnlyProps EMPTY_PROPS = new ReadOnlyProps();
  @Nonnull
  private final Map<String, String> props;
  @Nonnull
  private final Map<String, String> overrideProps;

  public ReadOnlyProps(ReadOnlyProps defaultProps, Iterator<Entry<String, String>> iterator) {
    Map<String, String> map = new HashMap<String, String>(defaultProps.asMap());
    while (iterator.hasNext()) {
      Entry<String, String> entry = iterator.next();
      map.put(entry.getKey(), entry.getValue());
    }
    this.props = ImmutableMap.copyOf(map);
    this.overrideProps = ImmutableMap.of();
  }

  public ReadOnlyProps(Iterator<Entry<String, String>> iterator) {
    this(EMPTY_PROPS, iterator);
  }

  private ReadOnlyProps() {
    this.props = ImmutableMap.of();
    this.overrideProps = ImmutableMap.of();
  }

  public ReadOnlyProps(Map<String, String> props) {
    this.props = ImmutableMap.copyOf(props);
    this.overrideProps = ImmutableMap.of();
  }

  private ReadOnlyProps(ReadOnlyProps defaultProps, Properties overridesArg) {
    this.props = defaultProps.props;
    if (overridesArg == null || overridesArg.isEmpty()) {
      this.overrideProps = defaultProps.overrideProps;
    } else {
      Map<String, String> combinedOverrides =
        Maps.newHashMapWithExpectedSize(defaultProps.overrideProps.size() + overridesArg.size());
      if (!defaultProps.overrideProps.isEmpty()) {
        combinedOverrides.putAll(defaultProps.overrideProps);
      }
      for (Entry<Object, Object> entry : overridesArg.entrySet()) {
        combinedOverrides.put(entry.getKey().toString(), entry.getValue().toString());
      }
      this.overrideProps = ImmutableMap.copyOf(combinedOverrides);
    }
  }

  private static Pattern varPat = Pattern.compile("\\$\\{[^\\}\\$\u0020]+\\}");
  private static int MAX_SUBST = 20;

  private String substituteVars(String expr) {
    if (expr == null) {
      return null;
    }
    Matcher match = varPat.matcher("");
    String eval = expr;
    for (int s = 0; s < MAX_SUBST; s++) {
      match.reset(eval);
      if (!match.find()) {
        return eval;
      }
      String var = match.group();
      var = var.substring(2, var.length() - 1); // remove ${ .. }
      String val = null;
      try {
        val = System.getProperty(var);
      } catch (SecurityException se) {
      }
      if (val == null) {
        val = getRaw(var);
      }
      if (val == null) {
        return eval; // return literal ${var}: var is unbound
      }
      // substitute
      eval = eval.substring(0, match.start()) + val + eval.substring(match.end());
    }
    throw new IllegalStateException(
      "Variable substitution depth too large: " + MAX_SUBST + " " + expr);
  }

  /**
   * Get the value of the <code>name</code> property, without doing
   * <a href="#VariableExpansion">variable expansion</a>.
   * @param name the property name.
   * @return the value of the <code>name</code> property, or null if no such property exists.
   */
  public String getRaw(String name) {
    String overridenValue = overrideProps.get(name);
    return overridenValue == null ? props.get(name) : overridenValue;
  }

  public String getRaw(String name, String defaultValue) {
    String value = getRaw(name);
    if (value == null) {
      return defaultValue;
    }
    return value;
  }

  /**
   * Get the value of the <code>name</code> property. If no such property exists, then
   * <code>defaultValue</code> is returned.
   * @param name         property name.
   * @param defaultValue default value.
   * @return property value, or <code>defaultValue</code> if the property doesn't exist.
   */
  public String get(String name, String defaultValue) {
    return substituteVars(getRaw(name, defaultValue));
  }

  /**
   * Get the value of the <code>name</code> property, <code>null</code> if no such property exists.
   * Values are processed for <a href="#VariableExpansion">variable expansion</a> before being
   * returned.
   * @param name the property name.
   * @return the value of the <code>name</code> property, or null if no such property exists.
   */
  public String get(String name) {
    return substituteVars(getRaw(name));
  }

  private String getHexDigits(String value) {
    boolean negative = false;
    String str = value;
    String hexString = null;
    if (value.startsWith("-")) {
      negative = true;
      str = value.substring(1);
    }
    if (str.startsWith("0x") || str.startsWith("0X")) {
      hexString = str.substring(2);
      if (negative) {
        hexString = "-" + hexString;
      }
      return hexString;
    }
    return null;
  }

  /**
   * Get the value of the <code>name</code> property as a <code>boolean</code>. If no such property
   * is specified, or if the specified value is not a valid <code>boolean</code>, then
   * <code>defaultValue</code> is returned.
   * @param name         property name.
   * @param defaultValue default value.
   * @return property value as a <code>boolean</code>, or <code>defaultValue</code>.
   */
  public boolean getBoolean(String name, boolean defaultValue) {
    String valueString = get(name);
    if ("true".equals(valueString)) return true;
    else if ("false".equals(valueString)) return false;
    else return defaultValue;
  }

  /**
   * Get the value of the <code>name</code> property as an <code>int</code>. If no such property
   * exists, or if the specified value is not a valid <code>int</code>, then
   * <code>defaultValue</code> is returned.
   * @param name         property name.
   * @param defaultValue default value.
   * @return property value as an <code>int</code>, or <code>defaultValue</code>.
   */
  public int getInt(String name, int defaultValue) {
    String valueString = get(name);
    if (valueString == null) return defaultValue;
    try {
      String hexString = getHexDigits(valueString);
      if (hexString != null) {
        return Integer.parseInt(hexString, 16);
      }
      return Integer.parseInt(valueString);
    } catch (NumberFormatException e) {
      return defaultValue;
    }
  }

  /**
   * Get the value of the <code>name</code> property as a <code>long</code>. If no such property is
   * specified, or if the specified value is not a valid <code>long</code>, then
   * <code>defaultValue</code> is returned.
   * @param name         property name.
   * @param defaultValue default value.
   * @return property value as a <code>long</code>, or <code>defaultValue</code>.
   */
  public long getLong(String name, long defaultValue) {
    String valueString = get(name);
    if (valueString == null) return defaultValue;
    try {
      String hexString = getHexDigits(valueString);
      if (hexString != null) {
        return Long.parseLong(hexString, 16);
      }
      return Long.parseLong(valueString);
    } catch (NumberFormatException e) {
      return defaultValue;
    }
  }

  /**
   * Get the value of the <code>name</code> property as a <code>long</code> or human-readable
   * format. If no such property exists, the provided default value is returned, or if the specified
   * value is not a valid <code>long</code> or human-readable format, then an error is thrown. You
   * can use the following suffix (case insensitive): k(kilo), m(mega), g(giga), t(tera), p(peta),
   * e(exa)
   * @param name         property name.
   * @param defaultValue default value.
   * @return property value as a <code>long</code>, or <code>defaultValue</code>.
   * @throws NumberFormatException - when the value is invalid
   */
  public long getLongBytes(String name, long defaultValue) {
    String valueString = get(name);
    if (valueString == null) {
      return defaultValue;
    }
    return StringUtils.TraditionalBinaryPrefix.string2long(valueString);
  }

  /**
   * Get the value of the <code>name</code> property as a <code>float</code>. If no such property is
   * specified, or if the specified value is not a valid <code>float</code>, then
   * <code>defaultValue</code> is returned.
   * @param name         property name.
   * @param defaultValue default value.
   * @return property value as a <code>float</code>, or <code>defaultValue</code>.
   */
  public float getFloat(String name, float defaultValue) {
    String valueString = get(name);
    if (valueString == null) return defaultValue;
    try {
      return Float.parseFloat(valueString);
    } catch (NumberFormatException e) {
      return defaultValue;
    }
  }

  /**
   * Get the properties as a {@code Map<String,String>}
   * @return {@code Map<String,String>}
   */
  public Map<String, String> asMap() {
    return props;
  }

  @Override
  public Iterator<Entry<String, String>> iterator() {
    return props.entrySet().iterator();
  }

  public boolean isEmpty() {
    return props.isEmpty();
  }

  /**
   * Constructs new map only if necessary for adding the override properties.
   * @param overrides Map of properties to override current properties.
   * @return new ReadOnlyProps if in applying the overrides there are modifications to the current
   *         underlying Map, otherwise returns this.
   */
  public ReadOnlyProps addAll(Properties overrides) {
    for (Entry<Object, Object> entry : overrides.entrySet()) {
      String key = entry.getKey().toString();
      String value = entry.getValue().toString();
      String oldValue = props.get(key);
      if (!Objects.equal(oldValue, value)) {
        if (LOGGER.isDebugEnabled()) LOGGER
          .debug("Creating new ReadOnlyProps due to " + key + " with " + oldValue + "!=" + value);
        return new ReadOnlyProps(this, overrides);
      }
    }
    return this;
  }
}
