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

import org.apache.ratis.security.TlsConf;
import org.apache.ratis.util.NetUtils;
import org.apache.ratis.util.SizeInBytes;
import org.apache.ratis.util.TimeDuration;
import org.apache.ratis.util.function.CheckedBiConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;

public interface ConfUtils {
  Logger LOG = LoggerFactory.getLogger(ConfUtils.class);

  class Utils {
    private static final ConcurrentMap<String, Object> CACHE = new ConcurrentHashMap<>();

    private static <T> boolean isNew(String key, T value) {
      if (value == null) {
        final Object previous = CACHE.remove(key);
        return previous != null;
      } else {
        final Object previous = CACHE.put(key, value);
        return !value.equals(previous);
      }
    }
  }

  static <T> void logGet(String key, T value, T defaultValue, Consumer<String> logger) {
    if (logger != null && Utils.isNew(key, value)) {
      logger.accept(String.format("%s = %s (%s)", key, value,
          Objects.equals(value, defaultValue)? "default": "custom"));
    }
  }

  static <T> void logFallback(String key, String fallbackKey, T fallbackValue, Consumer<String> logger) {
    if (logger != null) {
      logger.accept(String.format("%s = %s (fallback to %s)", key, fallbackValue, fallbackKey));
    }
  }

  static void logSet(String key, Object value) {
    LOG.debug("set {} = {}", key, value);
  }

  static BiConsumer<String, Integer> requireMin(int min) {
    return (key, value) -> {
      if (value < min) {
        throw new IllegalArgumentException(
            key + " = " + value + " < min = " + min);
      }
    };
  }

  static BiConsumer<String, Integer> requireMax(int max) {
    return (key, value) -> {
      if (value > max) {
        throw new IllegalArgumentException(
            key + " = " + value + " > max = " + max);
      }
    };
  }

  static BiConsumer<String, Double> requireMin(double min) {
    return (key, value) -> {
      if (value < min) {
        throw new IllegalArgumentException(
            key + " = " + value + " < min = " + min);
      }
    };
  }

  static BiConsumer<String, Double> requireMax(double max) {
    return (key, value) -> {
      if (value > max) {
        throw new IllegalArgumentException(
            key + " = " + value + " > max = " + max);
      }
    };
  }

  static BiConsumer<String, Long> requireMin(SizeInBytes min) {
    return requireMin(min.getSize());
  }

  static BiConsumer<String, Long> requireMin(long min) {
    return (key, value) -> {
      if (value < min) {
        throw new IllegalArgumentException(
            key + " = " + value + " < min = " + min);
      }
    };
  }

  static BiConsumer<String, SizeInBytes> requireMinSizeInByte(SizeInBytes min) {
    return (key, value) -> {
      if (value.getSize() < min.getSize()) {
        throw new IllegalArgumentException(
            key + " = " + value + " < min = " + min);
      }
    };
  }

  static BiConsumer<String, Long> requireMax(long max) {
    return (key, value) -> {
      if (value > max) {
        throw new IllegalArgumentException(
            key + " = " + value + " > max = " + max);
      }
    };
  }

  static BiConsumer<String, TimeDuration> requireNonNegativeTimeDuration() {
    return (key, value) -> {
      if (value.isNegative()) {
        throw new IllegalArgumentException(
            key + " = " + value + " is negative.");
      }
    };
  }

  static BiConsumer<String, TimeDuration> requirePositive() {
    return (key, value) -> {
      if (value.getDuration() <= 0) {
        throw new IllegalArgumentException(
            key + " = " + value + " is non-positive.");
      }
    };
  }

  static BiFunction<String, Long, Integer> requireInt() {
    return (key, value) -> {
      try {
        return Math.toIntExact(value);
      } catch (ArithmeticException ae) {
        throw new IllegalArgumentException(
            "Failed to cast " + key + " = " + value + " to int.", ae);
      }
    };
  }

  @SafeVarargs
  static boolean getBoolean(
      BiFunction<String, Boolean, Boolean> booleanGetter,
      String key, boolean defaultValue, Consumer<String> logger, BiConsumer<String, Boolean>... assertions) {
    return get(booleanGetter, key, defaultValue, logger, assertions);
  }

  @SafeVarargs
  static int getInt(
      BiFunction<String, Integer, Integer> integerGetter,
      String key, int defaultValue, Consumer<String> logger, BiConsumer<String, Integer>... assertions) {
    return get(integerGetter, key, defaultValue, logger, assertions);
  }

  @SafeVarargs
  static int getInt(
      BiFunction<String, Integer, Integer> integerGetter,
      String key, int defaultValue, String fallbackKey, int fallbackValue,
      Consumer<String> logger, BiConsumer<String, Integer>... assertions) {
    return get(integerGetter, key, defaultValue, fallbackKey, fallbackValue, logger, assertions);
  }

  @SafeVarargs
  static long getLong(
      BiFunction<String, Long, Long> longGetter,
      String key, long defaultValue, Consumer<String> logger, BiConsumer<String, Long>... assertions) {
    return get(longGetter, key, defaultValue, logger, assertions);
  }

  @SafeVarargs
  static double getDouble(
      BiFunction<String, Double, Double> doubleGetter,
      String key, double defaultValue, Consumer<String> logger, BiConsumer<String, Double>... assertions) {
    return get(doubleGetter, key, defaultValue, logger, assertions);
  }

  @SafeVarargs
  static File getFile(
      BiFunction<String, File, File> fileGetter,
      String key, File defaultValue, Consumer<String> logger, BiConsumer<String, File>... assertions) {
    return get(fileGetter, key, defaultValue, logger, assertions);
  }

  @SafeVarargs
  static List<File> getFiles(
      BiFunction<String, List<File>, List<File>> fileGetter,
      String key, List<File> defaultValue, Consumer<String> logger, BiConsumer<String, List<File>>... assertions) {
    return get(fileGetter, key, defaultValue, logger, assertions);
  }


  @SafeVarargs
  static SizeInBytes getSizeInBytes(
      BiFunction<String, SizeInBytes, SizeInBytes> getter,
      String key, SizeInBytes defaultValue, Consumer<String> logger, BiConsumer<String, SizeInBytes>... assertions) {
    final SizeInBytes value = get(getter, key, defaultValue, logger, assertions);
    requireMin(0L).accept(key, value.getSize());
    return value;
  }

  @SafeVarargs
  static TimeDuration getTimeDuration(
      BiFunction<String, TimeDuration, TimeDuration> getter,
      String key, TimeDuration defaultValue, Consumer<String> logger, BiConsumer<String, TimeDuration>... assertions) {
    final TimeDuration value = get(getter, key, defaultValue, logger, assertions);
    requireNonNegativeTimeDuration().accept(key, value);
    return value;
  }

  @SafeVarargs
  static TimeDuration getTimeDuration(
        BiFunction<String, TimeDuration, TimeDuration> getter,
        String key, TimeDuration defaultValue, String fallbackKey, TimeDuration fallbackValue,
        Consumer<String> logger, BiConsumer<String, TimeDuration>... assertions) {
    final TimeDuration value = get(getter, key, defaultValue, fallbackKey, fallbackValue, logger, assertions);
    requireNonNegativeTimeDuration().accept(key, value);
    return value;
  }


  static TlsConf getTlsConf(
      Function<String, TlsConf> tlsConfGetter,
      String key, Consumer<String> logger) {
    return get((k, d) -> tlsConfGetter.apply(k), key, null, logger);
  }

  @SafeVarargs
  static <T> T get(BiFunction<String, T, T> getter,
      String key, T defaultValue, Consumer<String> logger, BiConsumer<String, T>... assertions) {
    final T value = getter.apply(key, defaultValue);
    logGet(key, value, defaultValue, logger);
    Arrays.asList(assertions).forEach(a -> a.accept(key, value));
    return value;
  }

  @SafeVarargs
  static <T> T get(BiFunction<String, T, T> getter,
      String key, T defaultValue, String fallbackKey, T fallbackValue,
      Consumer<String> logger, BiConsumer<String, T>... assertions) {
    T value = get(getter, key, defaultValue, null, assertions);
    if (value != defaultValue) {
      logGet(key, value, defaultValue, logger);
      return value;
    } else {
      logFallback(key, fallbackKey, fallbackValue, logger);
      return fallbackValue;
    }
  }

  static InetSocketAddress getInetSocketAddress(
      BiFunction<String, String, String> stringGetter,
      String key, String defaultValue, Consumer<String> logger) {
    return NetUtils.createSocketAddr(get(stringGetter, key, defaultValue, logger));
  }

  @SafeVarargs
  static void setBoolean(
      BiConsumer<String, Boolean> booleanSetter, String key, boolean value,
      BiConsumer<String, Boolean>... assertions) {
    set(booleanSetter, key, value, assertions);
  }

  @SafeVarargs
  static void setInt(
      BiConsumer<String, Integer> integerSetter, String key, int value,
      BiConsumer<String, Integer>... assertions) {
    set(integerSetter, key, value, assertions);
  }

  @SafeVarargs
  static void setLong(
      BiConsumer<String, Long> longSetter, String key, long value,
      BiConsumer<String, Long>... assertions) {
    set(longSetter, key, value, assertions);
  }

  @SafeVarargs
  static void setDouble(
      BiConsumer<String, Double> doubleSetter, String key, double value,
      BiConsumer<String, Double>... assertions) {
    set(doubleSetter, key, value, assertions);
  }

  @SafeVarargs
  static void setFile(
      BiConsumer<String, File> fileSetter, String key, File value,
      BiConsumer<String, File>... assertions) {
    set(fileSetter, key, value, assertions);
  }

  @SafeVarargs
  static void setFiles(
      BiConsumer<String, List<File>> fileSetter, String key, List<File> value,
      BiConsumer<String, List<File>>... assertions) {
    set(fileSetter, key, value, assertions);
  }

  @SafeVarargs
  static void setSizeInBytes(
      BiConsumer<String, String> stringSetter, String key, SizeInBytes value,
      BiConsumer<String, Long>... assertions) {
    final long v = value.getSize();
    Arrays.asList(assertions).forEach(a -> a.accept(key, v));
    set(stringSetter, key, value.getInput());
  }

  @SafeVarargs
  static void setTimeDuration(
      BiConsumer<String, TimeDuration> timeDurationSetter, String key, TimeDuration value,
      BiConsumer<String, TimeDuration>... assertions) {
    set(timeDurationSetter, key, value, assertions);
  }

  static void setTlsConf(
      BiConsumer<String, TlsConf> tlsConfSetter, String key, TlsConf value) {
    set(tlsConfSetter, key, value);
  }

  @SafeVarargs
  static <T> void set(
      BiConsumer<String, T> setter, String key, T value,
      BiConsumer<String, T>... assertions) {
    Arrays.asList(assertions).forEach(a -> a.accept(key, value));
    setter.accept(key, value);
    logSet(key, value);
  }

  static void printAll(Class<?> confClass) {
    ConfUtils.printAll(confClass, System.out::println);
  }

  static void printAll(Class<?> confClass, Consumer<Object> out) {
    if (confClass.isEnum()) {
      return;
    }
    out.accept("");
    out.accept("******* " + confClass + " *******");
    Arrays.asList(confClass.getDeclaredFields())
        .forEach(f -> printField(confClass, out, f));
    Arrays.asList(confClass.getClasses())
        .forEach(c -> printAll(c, s -> out.accept("  " + s)));
  }

  static void printField(Class<?> confClass, Consumer<Object> out, Field f) {
    final int modifiers = f.getModifiers();
    if (!Modifier.isStatic(modifiers)) {
      throw new IllegalStateException("Found non-static field " + f);
    }
    if (!Modifier.isFinal(modifiers)) {
      throw new IllegalStateException("Found non-final field " + f);
    }
    if (printKey(confClass, out, f, "KEY", "DEFAULT", ConfUtils::append)) {
      return;
    }
    if (printKey(confClass, out, f, "PARAMETER", "CLASS",
        (b, classField) -> b.append(classField.get(null)))) {
      return;
    }
    final String fieldName = f.getName();
    if ("LOG".equals(fieldName) || "$jacocoData".equals(fieldName)) {
      return;
    }
    if (!"PREFIX".equals(fieldName)) {
      throw new IllegalStateException("Unexpected field: " + fieldName);
    }
    try {
      out.accept("constant: " + fieldName + " = " + f.get(null));
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Failed to access " + f, e);
    }
  }

  static void append(StringBuilder b, Field defaultField) throws IllegalAccessException {
    b.append(defaultField.getGenericType().getTypeName());

    final Class<?> type = defaultField.getType();
    if (type.isEnum()) {
      b.append(" enum[");
      for(Object e : defaultField.getType().getEnumConstants()) {
        b.append(e).append(", ");
      }
      b.setLength(b.length() - 2);
      b.append("]");
    }

    b.append(", ").append("default=").append(defaultField.get(null));
  }

  static boolean printKey(
      Class<?> confClass, Consumer<Object> out, Field f, String key, String defaultName,
      CheckedBiConsumer<StringBuilder, Field, IllegalAccessException> processDefault) {
    final String fieldName = f.getName();
    if (fieldName.endsWith("_" + defaultName)) {
      return true;
    }
    if (!fieldName.endsWith("_" + key)) {
      return false;
    }
    final StringBuilder b = new StringBuilder();
    final Object keyName;
    try {
      keyName = f.get(null);
      b.append(key.toLowerCase()).append(": ").append(keyName);
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Failed to access " + fieldName, e);
    }
    assertKey(fieldName, key.length(), keyName, confClass);
    final String defaultFieldName = fieldName.substring(0, fieldName.length() - key.length()) + defaultName;
    b.append(" (");
    try {
      final Field defaultField = confClass.getDeclaredField(defaultFieldName);
      processDefault.accept(b, defaultField);
    } catch (NoSuchFieldException e) {
      throw new IllegalStateException(defaultName + " not found for field " + f, e);
    } catch (IllegalAccessException e) {
      throw new IllegalStateException("Failed to access " + defaultFieldName, e);
    }
    b.append(")");

    out.accept(b);
    return true;
  }

  static String normalizeName(String name) {
    return name.replaceAll("[._-]", "").toLowerCase();
  }

  static void assertKey(String fieldName, int toTruncate, Object keyName, Class<?> confClass) {
    final String normalizedFieldName = normalizeName(fieldName.substring(0, fieldName.length() - toTruncate));
    final String normalizedKeyName = normalizeName("" + keyName);

    if (!normalizedKeyName.endsWith(normalizedFieldName)) {
      throw new IllegalStateException("Field and key mismatched: fieldName = " + fieldName + " (" + normalizedFieldName
          + ") but keyName = " + keyName + " (" + normalizedKeyName + ")");
    }

    // check getter and setter methods
    boolean getter = false;
    boolean setter = false;
    for(Method m : confClass.getMethods()) {
      final String name = m.getName();
      if (name.equalsIgnoreCase(normalizedFieldName)) {
        getter = true;
      }
      if (name.equalsIgnoreCase("set" + normalizedFieldName)) {
        setter = true;
      }
    }
    if (!getter) {
      throw new IllegalStateException("Getter method not found for " + fieldName);
    }
    if (!setter) {
      throw new IllegalStateException("Setter method not found for " + fieldName);
    }
  }
}
