content: /*
   *  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.causeway.commons.internal.collections;
  
  import java.util.AbstractMap;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.LinkedHashMap;
  import java.util.Map;
  import java.util.Set;
  import java.util.TreeMap;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.function.Function;
  import java.util.function.Predicate;
  import java.util.function.Supplier;
  import java.util.stream.Collectors;
  import java.util.stream.Stream;
  
  import org.jspecify.annotations.NonNull;
  import org.jspecify.annotations.Nullable;
  
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.base._NullSafe;
  import org.apache.causeway.commons.internal.collections._Multimaps.ListMultimap;
  import org.apache.causeway.commons.internal.exceptions._Exceptions;
  
  /**
   * <h1>- internal use only -</h1>
   * <p>
   * Common Map creation idioms.
   * </p>
   * <p>
   * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
   * These may be changed or removed without notice!
   * </p>
   *
   * @since 2.0
   */
  public final class _Maps {
  
      private _Maps(){}
  
      /**
       * A Map that supports value lookup by key and <em>alias</em> keys.
       * <p>
       * example use-case: store values by class type then allow lookup by interface type
       * @param <K>
       * @param <V>
       */
      public static interface AliasMap<K, V> extends Map<K, V> {
  
          /**
           *
           * @param key
           * @param aliases
           * @param value
           * @throws IllegalArgumentException if there is an alias-key collision
           */
          public V put(K key, Can<K> aliases, V value);
  
          /**
           * Like {@link #put(Object, Can, Object)}, but on alias-key collision re-maps existing alias-keys
           * @param key
           * @param aliases
           * @param value
           */
          public V remap(K key, Can<K> aliases, V value);
  
      }
  
      // -- UNMODIFIABLE MAP
  
      public static <K, V> Map<K, V> unmodifiable(final K k1, final V v1) {
          final LinkedHashMap<K, V> mapPreservingOrder = newLinkedHashMap();
          mapPreservingOrder.put(k1, v1);
          return Collections.unmodifiableMap(mapPreservingOrder);
      }
  
      public static <K, V> Map<K, V> unmodifiable(final K k1, final V v1, final K k2, final V v2) {
          final LinkedHashMap<K, V> mapPreservingOrder = newLinkedHashMap();
          mapPreservingOrder.put(k1, v1);
          mapPreservingOrder.put(k2, v2);
          return Collections.unmodifiableMap(mapPreservingOrder);
      }
  
      public static <K, V> Map<K, V> unmodifiable(final K k1, final V v1, final K k2, final V v2, final K k3, final V v3) {
          final LinkedHashMap<K, V> mapPreservingOrder = newLinkedHashMap();
          mapPreservingOrder.put(k1, v1);
          mapPreservingOrder.put(k2, v2);
          mapPreservingOrder.put(k3, v3);
          return Collections.unmodifiableMap(mapPreservingOrder);
      }
  
      public static <K, V> Map<K, V> unmodifiable(final K k1, final V v1, final K k2, final V v2, final K k3, final V v3, final K k4, final V v4) {
          final LinkedHashMap<K, V> mapPreservingOrder = newLinkedHashMap();
          mapPreservingOrder.put(k1, v1);
          mapPreservingOrder.put(k2, v2);
          mapPreservingOrder.put(k3, v3);
          mapPreservingOrder.put(k4, v4);
          return Collections.unmodifiableMap(mapPreservingOrder);
      }
  
      @SafeVarargs
      public static <K, V> Map<K, V> unmodifiableEntries(final Map.@NonNull Entry<? extends K,? extends V>... entries) {
          if(entries.length==0) {
              return Collections.emptyMap();
          }
  
          final LinkedHashMap<K, V> mapPreservingOrder = newLinkedHashMap();
  
          Stream.of(entries)
          .forEach(entry->mapPreservingOrder.put(entry.getKey(), entry.getValue()));
  
          return Collections.unmodifiableMap(mapPreservingOrder);
      }
  
      public static <K, V> Map.Entry<K, V> entry(final K k, final V v){
          return new AbstractMap.SimpleEntry<K, V>(k, v);
      }
  
      // -- TO STRING
  
      public static String toString(
              final @Nullable Map<?, ?> map,
              final @NonNull  CharSequence delimiter) {
  
          return map==null
              ? ""
              : map.entrySet()
                  .stream()
                  .map(Object::toString)
                  .collect(Collectors.joining(delimiter));
          }
  
      // -- MODIFICATIONS
  
      /**
       * For a given {@code map} either adds or removes the specified {@code key} based on whether
       * the map contains the {@code key}.
       * If this is an add operation, then given {@code value} is associated with the {@code key}.
       * @param <K>
       * @param <V>
       * @param map
       * @param key
       * @param value
       * @return whether given map contains the {@code key} after the operation
       */
      public static <K, V> boolean toggleElement(
              final @NonNull Map<K, V> map,
              final @NonNull K key,
              final @NonNull V value) {
  
          var newValue = map.compute(key, (k, v) -> (v==null) ? value : null);
          return newValue!=null;
      }
  
      // -- TRANSFORMATIONS
  
      public static <K, V0, V1> Map<K, V1> mapValues(
              final @Nullable Map<K, V0> input,
              final @NonNull Supplier<Map<K, V1>> mapFactory,
              final @NonNull Function<V0, V1> valueMapper) {
  
          var resultMap = mapFactory.get();
  
          if(input==null
                  || input.isEmpty()) {
              return resultMap;
          }
  
          input.forEach((k, v)->resultMap.put(k, valueMapper.apply(v)));
          return resultMap;
      }
  
      public static <K, V> Map<K, V> filterKeys(
              final @Nullable Map<K, V> input,
              final @NonNull Predicate<K> keyFilter,
              final @NonNull Supplier<Map<K, V>> factory) {
  
          final Map<K, V> result = factory.get();
  
          if(input==null) {
              return result;
          }
  
          input.forEach((k, v)->{
              if(keyFilter.test(k)) {
                  result.put(k, v);
              }
          });
  
          return result;
      }
  
      public static <K, V> ListMultimap<V, K> invertToListMultimap(final Map<K, V> input) {
          final ListMultimap<V, K> result = _Multimaps.newListMultimap();
          if(input==null) {
              return result;
          }
          input.forEach((k, v)->result.putElement(v, k));
          return result;
      }
  
      // -- FACTORIES ...
  
      // -- HASH MAP
  
      public static <K, V> HashMap<K, V> newHashMap() {
          return new HashMap<K, V>();
      }
  
      // -- LINKED HASH MAP
  
      public static <K, V> LinkedHashMap<K, V> newLinkedHashMap() {
          return new LinkedHashMap<K, V>();
      }
  
      // -- CONCURRENT HASH MAP
  
      public static <K, V> ConcurrentHashMap<K, V> newConcurrentHashMap() {
          return new ConcurrentHashMap<K, V>();
      }
  
      // -- TREE MAP
  
      public static <K, V> TreeMap<K, V> newTreeMap() {
          return new TreeMap<K, V>();
      }
  
      public static <K, V> TreeMap<K, V> newTreeMap(final Comparator<? super K> comparator) {
          return new TreeMap<K, V>(comparator);
      }
  
      // -- ALIAS MAP
  
      private record KeyPair<K>(
              K key,
              Can<K> aliasKeys) {
      }
  
      public static <K, V> AliasMap<K, V> newAliasMap(
              final @NonNull Supplier<Map<K, V>> mapFactory){
  
          return new AliasMap<K, V>() {
  
              final Map<K, V> delegate = mapFactory.get();
  
              @Override public int size() { return delegate.size(); }
              @Override public boolean isEmpty() { return delegate.isEmpty(); }
              @Override public boolean containsValue(final Object value) { return delegate.containsValue(value); }
              @Override public Set<K> keySet() { return delegate.keySet(); }
              @Override public Collection<V> values() { return delegate.values();   }
              @Override public Set<Entry<K, V>> entrySet() { return delegate.entrySet(); }
  
              @Override
              public V put(final K key, final V value) {
                  return this.put(key, Can.empty(), value);
              }
  
              @Override
              public void putAll(final Map<? extends K, ? extends V> other) {
                  if(!_NullSafe.isEmpty(other)) {
                      other.forEach((k, v)->this.put(k, v));
                  }
              }
  
              @Override
              public V put(final K key, final Can<K> aliases, final V value) {
                  putAliasKeys(key, aliases, /*re-map*/ false);
                  return delegate.put(key, value);
              }
  
              @Override
              public V remap(final K key, final Can<K> aliases, final V value) {
                  putAliasKeys(key, aliases, /*re-map*/ true);
                  return delegate.put(key, value);
              }
  
              @Override
              public boolean containsKey(final Object keyOrAliasKey) {
                  return delegate.containsKey(keyOrAliasKey) ||
                      containsAliasKey(keyOrAliasKey);
              }
  
              @Override
              public V get(final Object keyOrAliasKey) {
                  var v = delegate.get(keyOrAliasKey);
                  if(v!=null) {
                      return v;
                  }
                  return getByAliasKey(keyOrAliasKey);
              }
  
              @Override
              public V remove(final Object key) {
                  removeAliasKeysOf(key);
                  return delegate.remove(key);
              }
  
              @Override
              public void clear() {
                  delegate.clear();
                  clearAliasKeys();
              }
  
              // -- HELPER
  
              private final Map<K, KeyPair<K>> pairByAliasKey = _Maps.newHashMap();
  
              private void putAliasKeys(final K key, final Can<K> aliasKeys, final boolean remap) {
                  if(aliasKeys.isNotEmpty()) {
                      var keyPair = new KeyPair<>(key, aliasKeys);
                      for(var aliasKey : aliasKeys) {
  
                          var existingKeyPair = pairByAliasKey.put(aliasKey, keyPair);
                          if(existingKeyPair!=null && !remap) {
  
                              throw _Exceptions.illegalArgument(
                                      "alias key collision on alias %s: existing-key=%s, new-key=%s",
                                      aliasKey, existingKeyPair.key, keyPair.key);
                          }
                      }
                  }
              }
  
              private V getByAliasKey(final Object aliasKey) {
                  var keyPair = pairByAliasKey.get(aliasKey);
                  if(keyPair!=null) {
                      return delegate.get(keyPair.key());
                  }
                  return null;
              }
  
              private boolean containsAliasKey(final Object aliasKey) {
                  return pairByAliasKey.containsKey(aliasKey);
              }
  
              private void removeAliasKeysOf(final Object key) {
                  //XXX this implementation is slow for large alias maps, since we traverse the entire map
                  pairByAliasKey.entrySet()
                  .removeIf(entry->{
                      var keyPair = entry.getValue();
                      return keyPair.key().equals(key);
                  });
  
              }
  
              private void clearAliasKeys() {
                  pairByAliasKey.clear();
              }
  
          };
      }
  
      // --
  
  }
  