package nxm.sys.inc;

import static java.util.Collections.enumeration;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

/** An extension of {@link KeyableByType} that includes all of the methods from
 *  {@code Map<String,Object>} which is the most common base-implementation of
 *  {@link KeyableByType}. <br>
 *  <br>
 *  Note: Since the functionality of the various {@code get?(key,def)} methods (which derives from
 *  the NeXtMidas {@code Table} class) was defined prior to the introduction of
 *  {@link Map#getOrDefault(Object,Object)} and when null values were uncommon (since
 *  {@code Hashtable} was still dominant) they were defined as returning the default value
 *  ({@code def}) any time {@link #get(Object)} returns null. This differs from the more modern
 *  {@link Map#getOrDefault(Object,Object)} which will return a null value if such is present
 *  in the map.
 *  @author  J. Czechowski
 *  @since   NeXtMidas 3.9.0
 */
@SpecialLoggingRequired
@InternalUseOnly
public interface MapKBT extends Map<String,Object>, KeyableByType {
  /** An unmodifiable instance with no content. */
  public static final MapKBT EMPTY_MAP = new AbstractMapKBT(Collections.emptyMap());

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Implement the Keyable and KeyableByType methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  @Override default Object   getKey  (String k)           { return get(k); }
  @Override default Object   setKey  (String k, Object v) { return put(k,v); }
  @Override default Object   getO    (String k)           { return get(k); }
  @Override default String[] getKeys ()                   { return getKeys(null); }

  @Override
  default Object getO (String k, Object d) {
    // Note: We can't use getOrDefault(..) due to the way null is handled (see note at top of class)
    Object v = get(k);
    return (v == null)? d : v;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Other methods to match Table
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Gets the size of the map.
   *  @return The size of the map.
   */
  default int getSize () {
    return size();
  }

  /** Gets an enumeration of the keys in this table.
   *  @return An enumeration of the keys in this table.
   */
  default Enumeration<String> keys () {
    return enumeration(keySet());
  }

  /** Gets the list of keys matching a filter (on values) as a comma separated list.
   *  @param valueFilter The filter to use (on values) to determine which keys to list.
   *                     (If this is <code>null</code> all keys are listed.)
   *  @return The list of keys (comma separated).
   */
  default String[] getKeys (Filter valueFilter) {
    if (isEmpty()) return new String[0];

    Collection<String> keys;
    if (valueFilter == null) {
      keys = keySet().stream().collect(toList());
    }
    else {
      keys = keySet().stream().filter((k) -> valueFilter.accept(get(k))).collect(toList());
    }
    return keys.toArray(new String[keys.size()]);
  }

  /** Gets the list of keys as a comma separated list. This is the same as P@code getKeyList(null)}.
   *  @return The list of keys.
   */
  default String getKeyList () {
    return getKeyList(null);
  }

  /** Gets the list of keys matching a filter (on values) as a comma separated list.
   *  @param valueFilter The filter to use (on values) to determine which keys to list.
   *                     (If this is <code>null</code> all keys are listed.)
   *  @return The list of keys (comma separated).
   */
  default String getKeyList (Filter valueFilter) {
    if (isEmpty()) return "";

    if (valueFilter == null) {
      return keySet().stream().collect(joining(","));
    }
    else {
      return keySet().stream().filter((k) -> valueFilter.accept(get(k))).collect(joining(","));
    }
  }


  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Static utility methods
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Builds a map out of an array of {@link KeyValuePair}.
   *  @param kvPairs The array of {@link KeyValuePair}.
   *  @return The map.
   */
  public static MapKBT toMap (KeyValuePair<?>... kvPairs) {
    return KeyValuePair.toMap(kvPairs);
  }

  /** Builds a map out of an collection of {@link KeyValuePair}.
   *  @param kvPairs The collection of {@link KeyValuePair}.
   *  @return The map.
   */
  public static MapKBT toMap (Iterable<KeyValuePair<?>> kvPairs) {
    return KeyValuePair.toMap(kvPairs);
  }

  /** Wraps a generic {@code Map<String,Object>} implementation as an instance of {@link MapKBT}.
   *  @param map The map to wrap.
   *  @return The wrapped map, or {@code map} if it already implements {@link MapKBT}. Returns null
   *          if the input parameter is null.
   */
  public static MapKBT wrapMap (Map<String,Object> map) {
    if (map == null) return null;
    return (map instanceof MapKBT)? (MapKBT)map : new AbstractMapKBT(map);
  }

  /** Wraps a generic {@link Map} implementation as an instance of {@link MapKBT}. This takes in an
   *  {@link Object}, casts it to a {@link Map} and then verifies that each of the keys is a
   *  {@link String}. If the value passed in is null or already an instance of {@link MapKBT}, it is
   *  returned as-is with no additional checks. This allows easy use when a {@code Map} is present
   *  in a non-generic form, and facilitates code like this:
   *  <pre>
   *    if (obj instanceof Map) {
   *      MapKBT map = wrapMap(obj);
   *      ...
   *    }
   *  </pre>
   *  Note that this is not fool-proof. If a {@code Map<K,V>} is passed in, this will just
   *  assume (subject to an initial check of the current keys) that {@code K} is {@link String}
   *  and {@code V} is {@link Object}. It will permit any type of value to be inserted, and will
   *  have undefined behavior if the underlying structure ever has a non-string added as a key.
   *  @param <T> The map type, which should be a {@code Map<String,Object>}.
   *  @param map The map to wrap.
   *  @return The wrapped map, or {@code map} if it already implements {@link MapKBT}. Returns null
   *          if the input parameter is null.
   *  @throws ClassCastException If the input parameter is not a {@link Map} or if it is a
   *                             {@link Map} but has a key that is not a {@link String}.
   */
  @SuppressWarnings("unchecked")
  public static <T> MapKBT wrapMap (T map) {
    if ((map == null) || (map instanceof MapKBT)) {
      return (MapKBT)map;
    }

    Map<?,?> m;
    try {
      m = (Map<?,?>)map;
    }
    catch (ClassCastException e) {
      throw new ClassCastException("Expected Map<String,Object> but given "+map.getClass());
    }

    m.keySet().forEach((k) -> {
      if ((!(k instanceof String)) && (k != null)) {
        throw new ClassCastException("Expected Map<String,Object> but given Map<K super "+k.getClass()+",V>");
      }
    });

    return new AbstractMapKBT((Map)m);
  }

  /** Creates a new instance backed by a {@link TreeMap}.
   *  @return A new instance backed by a {@link TreeMap}.
   */
  @InternalUseOnly
  public static MapKBT newMap () {
    return new AbstractMapKBT();
  }

  /** Returns an unmodifiable, empty map.
   *  @return An unmodifiable, empty map.
   */
  @InternalUseOnly
  public static MapKBT emptyMap () {
    return EMPTY_MAP;
  }

  /** Wraps a generic {@link Map} implementation as an unmodifiable instance of {@link MapKBT}.
   *  @param map The map to wrap.
   *  @return An unmodifiable view of the given map.
   */
  @InternalUseOnly
  public static MapKBT unmodifiableMap (Map<String,Object> map) {
    return new AbstractMapKBT(Collections.unmodifiableMap(map));
  }

  /** Wraps a generic {@link Map} implementation as a synchronized instance of {@link MapKBT}.
   *  @param map The map to wrap.
   *  @return An unmodifiable view of the given map.
   */
  @InternalUseOnly
  public static MapKBT synchronizedMap (Map<String,Object> map) {
    return new AbstractMapKBT(Collections.synchronizedMap(map));
  }

  /** Creates a singleton {@link Map}.
   *  @param key The key.
   *  @param val The value.
   *  @return An unmodifiable view of the given map.
   */
  @InternalUseOnly
  public static MapKBT singletonMap (String key, Object val) {
    return new AbstractMapKBT(Collections.singletonMap(key,val));
  }

  /** Wraps a {@link Properties} implementation as an instance of {@link MapKBT}.
   *  @param props The properties to wrap.
   *  @return The wrapped map. Returns null if the input parameter is null.
   */
  @InternalUseOnly
  public static MapKBT wrapProperties (Properties props) {
    return (props == null)? null : new PropsImpl(props);
  }

  /** Find all Maps (including inner) that have a specified key (outer entry)
   *
   *  @param map      Map to search
   *  @param keyName  key that we want in outer level of each map selected
   *  @return ArrayList with Maps that have that key as an outer key
   */
  @SuppressWarnings("rawtypes")
  @InternalUseOnly
  public static ArrayList<Map> getSubMapsWithKey (Map map, String keyName) {
    ArrayList<Map> mapsWithEntry = new ArrayList<>();
    if (map.containsKey(keyName)) mapsWithEntry.add(map);
    for (Object key : map.keySet()) {
      if (map.get(key) instanceof Map) {
        Map subMap = (Map)map.get(key);
        if (subMap.containsKey(keyName)) mapsWithEntry.add(subMap);
        else {
          ArrayList<Map> subMapsWithEntry = getSubMapsWithKey(subMap, keyName);
          for (Map subMapWithEntry : subMapsWithEntry) {
            mapsWithEntry.add(subMapWithEntry);
          }
        }
      }
    }
    return mapsWithEntry;
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Provider interface
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** Marks an object that can provide a {@link MapKBT} that reflects the values in the object. */
  @FunctionalInterface
  @InternalUseOnly
  public static interface ToMapKBT {
    /** Returns a {@link MapKBT} instance that reflects the values in this object. The
     *  return value should be treated as if it was read-only.
     *  @return A {@link MapKBT} that matches this object.
     */
    MapKBT toMapKBT ();
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Implementation class
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** A basic implementation of {@link MapKBT}. */
  @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
  public static class AbstractMapKBT implements MapKBT {
    protected final Map<String,Object> map;

    /** Creates a new instance backed by a {@link TreeMap}. */
    @InternalUseOnly
    public AbstractMapKBT () {
      this(new TreeMap<>());
    }

    /** Creates a new instance.
     *  @param map The map to wrap (not null).
     */
    @InternalUseOnly
    public AbstractMapKBT (Map<String,Object> map) {
      this.map = requireNonNull(map);
    }

    /** Creates a new instance.
     *  @param cons The constructor for the underlying map type to use. This is usually
     *              {@code TreeMap::new} (the default) or {@code LinkedHashMap::new},
     *              but can be {@code ConcurrentSkipListMap::new} where concurrency is
     *              required.
     */
    public AbstractMapKBT (Supplier<Map<String,Object>> cons) {
      this((cons != null)? cons.get() : new TreeMap<>());
    }

    /** Creates a new instance backed by a {@link Map} created using the provided constructor.
     *  @param cons The constructor for the underlying map type to use. This is usually
     *              {@code TreeMap::new} (the default) or {@code LinkedHashMap::new},
     *              but can be {@code ConcurrentSkipListMap::new} where concurrency is
     *              required.
     *  @return MapKBT instance
     */
    public MapKBT newMap (Supplier<Map<String,Object>> cons) {
      return new AbstractMapKBT(cons);
    }

    @Override
    public void putAll (Map<? extends String, ? extends Object> m) {
      map.putAll(m);
    }
    @Override
    public void forEach (BiConsumer<? super String, ? super Object> a) {
      map.forEach(a);
    }
    @Override
    public void replaceAll (BiFunction<? super String, ? super Object, ? extends Object> f) {
      map.replaceAll(f);
    }
    @Override
    public Object computeIfAbsent (String k, Function<? super String, ? extends Object> f) {
      return map.computeIfAbsent(k, f);
    }
    @Override
    public Object computeIfPresent (String k, BiFunction<? super String, ? super Object, ? extends Object> f) {
      return map.computeIfPresent(k,f);
    }
    @Override
    public Object compute (String k, BiFunction<? super String, ? super Object, ? extends Object> f) {
      return map.compute(k,f);
    }
    @Override
    public Object merge (String k, Object v, BiFunction<? super Object, ? super Object, ? extends Object> f) {
      return map.merge(k,v,f);
    }

    @Override public boolean     equals        (Object o)           { return map.equals(o);        }
    @Override public int         hashCode      ()                   { return map.hashCode();       }
    @Override public String      toString      ()                   { return map.toString();       }
    @Override public int         size          ()                   { return map.size();           }
    @Override public boolean     isEmpty       ()                   { return map.isEmpty();        }
    @Override public boolean     containsKey   (Object k)           { return map.containsKey(k);   }
    @Override public boolean     containsValue (Object v)           { return map.containsValue(v); }
    @Override public Object      get           (Object k)           { return map.get(k);           }
    @Override public Object      put           (String k, Object v) { return map.put(k,v);         }
    @Override public Object      remove        (Object k)           { return map.remove(k);        }
    @Override public void        clear         ()                   {        map.clear();          }
    @Override public Set<String> keySet        ()                   { return map.keySet();         }
    @Override public Collection<Object>        values   ()          { return map.values();         }
    @Override public Set<Entry<String,Object>> entrySet ()          { return map.entrySet();       }

    @Override public Object  getOrDefault (Object k, Object d)           { return map.getOrDefault(k,d); }
    @Override public boolean remove       (Object k, Object v)           { return map.remove(k,v); }
    @Override public boolean replace      (String k, Object o, Object n) { return map.replace(k,o,n); }
    @Override public Object  replace      (String k, Object v)           { return map.replace(k,v); }
    @Override public Object  putIfAbsent  (String k, Object v)           { return map.putIfAbsent(k,v); }
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Implementation class
  //////////////////////////////////////////////////////////////////////////////////////////////////
  /** An basic implementation of {@link MapKBT} that wraps a {@link Properties} taking into
   *  consideration the special methods in {@link Properties} that handle chaining and the
   *  expectation that all keys are inherently (though not explicitly) strings.
   */
  @SuppressWarnings("element-type-mismatch")
  @InternalUseOnly
  public static class PropsImpl implements MapKBT {
    private final Properties props;

    /** Creates a new instance.
     *  @param props properties
     */
    @InternalUseOnly
    public PropsImpl (Properties props) {
      this.props = requireNonNull(props);
    }

    @Override
    public void putAll (Map<? extends String, ? extends Object> m) {
      props.putAll(m);
    }

    @Override
    public boolean equals (Object o) {
      return (o instanceof Map) && entrySet().equals(((Map)o).entrySet());
    }

    @Override
    public Set<Entry<String,Object>> entrySet () {
      Set<Entry<String,Object>> entrySet = new LinkedHashSet<>(props.size());
      keySet().forEach((k) -> entrySet.add(new SimpleImmutableEntry<>(k,props.getProperty(k))));
      return entrySet;
    }

    @Override
    public Collection<Object> values () {
      Collection<Object> values = new ArrayList<>(props.size());
      keySet().forEach((k) -> values.add(props.getProperty(k)));
      return values;
    }

    @Override public int         hashCode      ()                   { return props.hashCode();       }
    @Override public String      toString      ()                   { return props.toString();       }
    @Override public int         size          ()                   { return keySet().size();        }
    @Override public boolean     isEmpty       ()                   { return keySet().isEmpty();     }
    @Override public boolean     containsKey   (Object k)           { return keySet().contains(k);   }
    @Override public boolean     containsValue (Object v)           { return values().contains(v);   }
    @Override public Object      get           (Object k)           { return props.getProperty(k.toString()); }
    @Override public Object      put           (String k, Object v) { return props.put(k,v);         }
    @Override public Object      remove        (Object k)           { return props.remove(k);        }
    @Override public Set<String> keySet        ()                   { return props.stringPropertyNames(); }
    @Override public void        clear         ()                   { keySet().forEach((k) -> remove(k)); }
  }
}
