package nxm.sys.lib;

import static nxm.sys.lib.FileUtil.getInputStreamFor;

import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Properties;

import nxm.sys.inc.MapKBT;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.inc.SpecialLoggingRequired;

/** Various utility functions for dealing with {@link Properties}.
 *  @author J. Czechowski
 *  @since  NeXtMidas 3.9.0
 */
@SpecialLoggingRequired
public class PropsUtilities {
  private PropsUtilities () { } // prevent instantiation

  /** The full host name. If the FQDN is "myserver.example.com" this will be "myserver.example.com".
   *  Note that this may fallback to an IP address.
   */
  public static final String HOSTNAME;

  /** The short host name. If the FQDN is "myserver.example.com" this will be "myserver".
   *  Note that this may fallback to an IP address.
   */
  public static final String HOST;

  /** The process ID. This will be "0000" if not available. */
  public static final String PID;

  static {
    String mxName      = ManagementFactory.getRuntimeMXBean().getName();
    int    i           = mxName.indexOf('.');
    int    j           = mxName.indexOf('@');
    String name        = (i < 0)? mxName : mxName.substring(0,i);
    String altHostname = (j < 0)? null   : mxName.substring(j+1);
    String pid         = (j < 0)? "0000" : name.substring(0,j);
    String hostname;
    String host;

    try {
      InetAddress ia = InetAddress.getLocalHost();
      hostname = ia.getHostName();
    }
    catch (UnknownHostException e) {
      hostname = null;
    }

    // Pick the best host name (we prefer a non-IP address)
    if ((hostname == null) || hostname.isEmpty()) {
      hostname = altHostname;
    }
    if ((altHostname != null) && !altHostname.isEmpty() && !isIP(altHostname) && isIP(hostname)) {
      hostname = altHostname;
    }
    if ((hostname == null) || hostname.isEmpty() || hostname.equals("127.0.0.1")) {
      hostname = "localhost";
    }

    // Convert hostname into host
    i = hostname.indexOf('.');
    if (i > 0) {
      host = hostname.substring(0,i);
    }
    else {
      host = hostname;
    }

    HOSTNAME = hostname;
    HOST     = host;
    PID      = pid;
  }

  /** Indicates that a copy of the system properties (see {@link System#getProperties()} should be
   *  used.
   *  <pre>
   *    SYSTEM_PROPS = "-- SYSTEM_PROPS --";
   *  </pre>
   */
  public static final String SYSTEM_PROPS = "-- SYSTEM_PROPS --";

  /** Flag for <code>getPropValue(..)</code> indicating the value is required. */
  @SuppressWarnings("RedundantStringConstructorCall")
  public static final String   REQUIRED_STRING  = String.valueOf("-- REQUIRED_STRING --");
  /** Flag for <code>getPropValue(..)</code> indicating the value is required. */
  @SuppressWarnings("UnnecessaryBoxing")
  public static final Long     REQUIRED_LONG    = Long.valueOf(Long.MIN_VALUE);
  /** Flag for <code>getPropValue(..)</code> indicating the value is required. */
  @SuppressWarnings("UnnecessaryBoxing")
  public static final Integer  REQUIRED_INTEGER = Integer.valueOf(Integer.MIN_VALUE);
  /** Flag for <code>getPropValue(..)</code> indicating the value is required. */
  @SuppressWarnings("UnnecessaryBoxing")
  public static final Double   REQUIRED_DOUBLE  = Double.valueOf(Double.NaN);
  /** Flag for <code>getPropValue(..)</code> indicating the value is required. */
  @SuppressWarnings({"UnnecessaryBoxing", "BooleanConstructorCall"})
  public static final Boolean  REQUIRED_BOOLEAN = Boolean.valueOf(false);

  /** Loads the properties.
   *  @param propsFile The file to load the properties from.
   *  @return The run-time properties.
   */
  public static Properties loadProps (String propsFile) {
    return loadProps(propsFile, null, null);
  }

  /** Loads the properties.
   *  @param propsFile The file to load the properties from.
   *  @param defProps  The default properties (by file name or {@link #SYSTEM_PROPS}).
   *  @return The run-time properties.
   */
  public static Properties loadProps (String propsFile, String defProps) {
    return loadProps(propsFile, null, defProps);
  }

  /** Loads the properties.
   *  @param propsFile The file to load the properties from.
   *  @param defProps  The default properties.
   *  @return The run-time properties.
   */
  public static Properties loadProps (String propsFile, Properties defProps) {
    return loadProps(propsFile, defProps, null);
  }

  /** Loads the properties.
   *  @param propsFile     The file to load the properties from.
   *  @param defProps      The default properties (by object).
   *  @param defPropsName  The default properties (by file name or {@link #SYSTEM_PROPS}).
   *  @return The run-time properties.
   */
  private static Properties loadProps (String propsFile, Properties defProps, String defPropsName) {
    if (defPropsName != null) {
      defProps = loadProps(defPropsName, null, null);
    }

    Properties properties = new Props(defProps);
    if ((propsFile == null) || propsFile.isEmpty()) {
      // No new properties, just wrap the defaults
    }
    else if (propsFile.equals(SYSTEM_PROPS)) { // same as SYSTEM_PROPS
      // Use system properties
      System.getProperties().stringPropertyNames().forEach((key) -> {
        properties.setProperty(key, System.getProperty(key));
      });
    }
    else {
      try (InputStream in = getInputStreamFor(propsFile)) {
        properties.load(in);
      }
      catch (IOException e) {
        throw new RuntimeException("Unable to load properties from '"+propsFile+"'", e);
      }
    }
    return properties;
  }

  /** Loads Properties from a MapKBT
   *
   *  @param propMap MapKBT containing properties
   *  @return Properties created from MapKBT
   */
  @ProvisionalUseOnly("API could change")
  public static Properties loadProps (MapKBT propMap) {
    Properties properties = new Props();
    for (String key : propMap.getKeys()) {
      String value = propMap.get(key).toString();
      properties.setProperty(key, value);
    }
    return properties;
  }

  /** Creates a new properties that duplicates the properties with the given prefix. Example:
   *  <pre>
   *    Input:
   *      { foo=123,
   *        bar=456,
   *        PRE-foo=abc,
   *        PRE-baz=def,
   *      }
   *    Call:
   *      extractProps(props, "PRE-", false)
   *    Output:
   *      { foo=abc,
   *        baz=def,
   *      }
   *  </pre>
   *  @param props  The current properties. (This will not be modified.)
   *  @param prefix The prefix on the properties to extract.
   *  @param keep   Keep the prefix (true) or remove it (false)?
   *  @return The extracted properties.
   */
  public static Properties extractProps (Properties props, String prefix, boolean keep) {
    Properties p = new Props();
    int len = (keep)? 0 : prefix.length();
    props.stringPropertyNames().forEach((name) -> {
      if (name.startsWith(prefix)) {
        p.setProperty(name.substring(len), props.getProperty(name));
      }
    });
    return p;
  }

  /** Gets a property of the given type.
   *  @param <T>   This should be {@link String}, {@link Long}, {@link Integer}, {@link Short},
   *               {@link Byte}, {@link Boolean}, or {@link Character}.
   *  @param props The properties.
   *  @param name  The property name.
   *  @param def   The default value (not null).
   *  @return The value or def if not found.
   *  @throws NullPointerException     If not found and def is set to one of the <code>REQUIRED_*</code> values.
   *  @throws NumberFormatException    If the value can not convert to the proper type.
   *  @throws IllegalArgumentException If the type is invalid.
   */
  @SuppressWarnings("unchecked")
  public static <T> T getPropValue (Properties props, String name, T def) {
    return getPropValue(props, name, def, (Class<T>)def.getClass(), false);
  }


  /** Gets a property of the given type.
   *  @param <T>   This should be {@link String}, {@link Long}, {@link Integer}, {@link Short},
   *               {@link Byte}, {@link Boolean}, or {@link Character}.
   *  @param props The properties.
   *  @param name  The property name.
   *  @param type  The value type.
   *  @return The value or null if not found.
   *  @throws NumberFormatException    If the value can not convert to the proper type.
   *  @throws IllegalArgumentException If the type is invalid.
   */
  public static <T> T getPropValue (Properties props, String name, Class<T> type) {
    return getPropValue(props, name, null, type, false);
  }


  /** Gets a property of the given type and sets it if not already set.
   *  @param <T>   This should be {@link String}, {@link Long}, {@link Integer}, {@link Short},
   *               {@link Byte}, {@link Boolean}, or {@link Character}.
   *  @param props The properties.
   *  @param name  The property name.
   *  @param def   The default value (not null).
   *  @return The value or def if not found.
   *  @throws NullPointerException     If not found and def is set to one of the <code>REQUIRED_*</code> values.
   *  @throws IllegalArgumentException If the value can not convert to the proper type or if type is invalid.
   */
  @SuppressWarnings("unchecked")
  public static <T> T usePropValue (Properties props, String name, T def) {
    return getPropValue(props, name, def, (Class<T>)def.getClass(), true);
  }

  /** Gets a property of the given type and sets it if not already set.
   *  @param <T>   This should be {@link String}, {@link Long}, {@link Integer}, {@link Short},
   *               {@link Byte}, {@link Boolean}, or {@link Character}.
   *  @param props The properties.
   *  @param name  The property name.
   *  @param type  The value type.
   *  @return The value or null if not found.
   *  @throws IllegalArgumentException If the value can not convert to the proper type or if type is invalid.
   */
  public static <T> T usePropValue (Properties props, String name, Class<T> type) {
    return getPropValue(props, name, null, type, true);
  }

  /** Gets an optional property as a String.
   *  @param <T>   This should be {@link String}, {@link Long}, {@link Integer}, {@link Short},
   *               {@link Byte}, {@link Boolean}, or {@link Character}.
   *  @param props The properties.
   *  @param name  The property name.
   *  @param def   The default value.
   *  @param type  The value type.
   *  @param set   Set the value is needed?
   *  @return The value.
   *  @throws NullPointerException     If not found and def is set to one of the <code>REQUIRED_*</code> values.
   *  @throws IllegalArgumentException If the value can not convert to the proper type or if type is invalid.
   */
  @SuppressWarnings("unchecked")
  private static <T> T getPropValue (Properties props, String name, T def, Class<T> type, boolean set) {
    String str = props.getProperty(name, null);

    if ((str == null) || str.isEmpty()) {
      if ((def == REQUIRED_STRING) || (def == REQUIRED_LONG) || (def == REQUIRED_INTEGER) ||
          (def == REQUIRED_DOUBLE) || (def == REQUIRED_BOOLEAN)) {
        throw new NullPointerException("Missing required property '"+name+"'");
      }
      if (set) {
        // Set the property back to the default value
        props.setProperty(name, (def == null)? "" : def.toString());
      }
      return def;
    }

    try {
      Object val = null;
      if (type == String.class   ) val = str;
      if (type == Double.class   ) val = Double.valueOf(str);
      if (type == Float.class    ) val = Float.valueOf(str);
      if (type == Long.class     ) val = Long.valueOf(str);
      if (type == Integer.class  ) val = Integer.valueOf(str);
      if (type == Short.class    ) val = Short.valueOf(str);
      if (type == Byte.class     ) val = Byte.valueOf(str);
      if (type == Boolean.class  ) val = Boolean.valueOf(str);
      if (type == Character.class) val = str.charAt(0);

      if (Enum.class.isAssignableFrom(type)) {
        val = Enum.valueOf((Class)type, str);
      }

      if (val != null) {
        if (set) {
          // Set the property back to what we read it in as
          props.setProperty(name, val.toString());
        }
        return type.cast(val);
      }
    }
    catch (IllegalArgumentException e) {
      IllegalArgumentException ex = new NumberFormatException("Invalid value for property '"+name+"', "
                                                             +"expected value of type "+type.getName()
                                                             +" but given: "+str);
      ex.setStackTrace(e.getStackTrace());
      throw ex;
    }

    throw new IllegalArgumentException("Invalid type: "+type);
  }

  /** Is the given string an IPv4 or IPv6 address rather than a host name? */
  private static boolean isIP (String addr) {
    // Implementation Note: This is only used internally. It is a pretty sloppy
    //   check, but sufficient for what we need. Before making this public (if
    //   ever) the algorigthm would need to be substantially revised.
    if (addr.startsWith("[") && addr.endsWith("]")) return true; // IPv6

    for (int i = 0; i < addr.length(); i++) {
      char c = addr.charAt(0);
      if ((c != '.') && ((c < '0') || (c > '9'))) return false;
    }
    return true;
  }

  /** An extension on {@link Properties} that supports "${key}" translations and 5 special keys.
   *  The special keys are:
   *  <pre>
   *         HOME     - User home directory
   *         HOST     - Host name (short form)
   *         HOSTNAME - Host name (FQDN)
   *         PID      - Process ID
   *         USER     - User name
   *  </pre>
   */
  public static class Props extends Properties {
    private static final long serialVersionUID = 4112578634029874840L;

    /** Creates a new instance without defaults. */
    public Props () {
      super();
    }

    /** Creates a new instance with defaults.
     *  @param defaults The defaults.
     */
    public Props (Properties defaults) {
      super(defaults);
    }

    @Override
    public final String getProperty (String key, String def) {
      // This parent calls getProperty(key), so we put the functionality in there...
      return super.getProperty(key, def);
    }

    @Override
    public String getProperty (String key) {
      String value = super.getProperty(key);

      if (value == null) {
        switch (key) {
          case "HOME":     return getProperty("user.home");
          case "HOST":     return HOST;
          case "HOSTNAME": return HOSTNAME;
          case "PID":      return PID;
          case "USER":     return getProperty("user.name");
          default:         return null;
        }
      }

      // Handle "${key}" translations
      return resolve(value);
    }

    /** Resolves any "${key}" translations.
     *  @param value The given value.
     *  @return The value following resolution.
     */
    private String resolve (String value) {
      int i = value.indexOf("${");
      if (i < 0) return value;

      int j = -1;
      int n = 1;
      for (int ii = i+2; (j < 0) && (ii < value.length()); ii++) {
        switch (value.charAt(ii)) {
          case '{': n++;                     break;
          case '}': n--; if (n == 0) j = ii; break;
        }
      }

      if (j < 0) return value;

      String tag = value.substring(i+2,j);
      String val = getProperty(tag, "${"+tag+"}");

      return value.substring(0,i) + val + resolve(value.substring(j+1));
    }
  }
}
