001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2025–2026 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
006 * the License. You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
012 * specific language governing permissions and limitations under the License.
013 */
014package org.microbean.bean;
015
016import java.util.ArrayList;
017import java.util.Collection;
018import java.util.List;
019import java.util.Map;
020
021import java.util.concurrent.ConcurrentHashMap;
022
023import java.util.function.Predicate;
024import java.util.function.ToIntFunction;
025
026import javax.lang.model.type.TypeMirror;
027
028import org.microbean.assign.Matcher;
029import org.microbean.assign.Selectable;
030
031import static java.util.Objects.requireNonNull;
032
033/**
034 * Utility methods for working with {@link Selectable}s.
035 *
036 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
037 *
038 * @see Selectable
039 *
040 * @see org.microbean.assign.Selectables
041 */
042public final class Selectables {
043
044  private Selectables() {
045    super();
046  }
047
048  /**
049   * Returns a {@link Selectable} that reduces any ambiguity in the results returned by another {@link Selectable},
050   * considering notional alternate status and rank.
051   *
052   * @param <C> the criteria type
053   *
054   * @param <E> the element type
055   *
056   * @param s a {@link Selectable}; must not be {@code null}
057   *
058   * @param p a {@link Predicate} that tests whether an element is an <dfn>alternate</dfn>; may be
059   * {@code null} in which case it will be as if {@code e -> false} were supplied
060   *
061   * @param rankingFunction a {@link ToIntFunction} that returns a <dfn>rank</dfn> for an alternate; a rank of {@code 0}
062   * indicates no particular rank; may be {@code null} in which case it will be as if {@code e -> 0} were supplied
063   *
064   * @return a non-{@code null} {@link Selectable}
065   *
066   * @exception NullPointerException if any argument is {@code null}
067   */
068  public static final <C, E> Selectable<C, E> ambiguityReducing(final Selectable<C, E> s,
069                                                                final Predicate<? super E> p, // are you an alternate?
070                                                                final ToIntFunction<? super E> rankingFunction) { // rank?
071    requireNonNull(s, "s");
072    if (p == null) {
073      // If there's no way to tell if something is an alternate, then it also has no rank
074      return s;
075    }
076    final ToIntFunction<? super E> ranker = rankingFunction == null ? e -> 0 : rankingFunction;
077
078    // Relevant bits:
079    //
080    // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#unsatisfied_and_ambig_dependencies
081    // https://jakarta.ee/specifications/cdi/4.1/jakarta-cdi-spec-4.1#dynamic_lookup (Search for "The iterator() method
082    // must")
083    //
084    // In CDI 5 @Reserve will also enter the chat.
085    return c -> {
086      final List<E> elements = s.select(c); // remember: c could be null; the result could be everything
087      final int size = elements.size();
088      switch (size) {
089      case 0:
090        return List.of();
091      case 1:
092        return List.of(elements.get(0));
093      default:
094        break;
095      }
096
097      int maxRank = Integer.MIN_VALUE;
098      final List<E> reductionList = new ArrayList<>(size); // will never be larger, only smaller
099      boolean reductionListContainsOnlyRankedAlternates = false;
100
101      for (final E element : elements) {
102        if (!p.test(element)) {
103          // The element is not an alternate. We skip it.
104          continue;
105        }
106
107        final int rank = ranker.applyAsInt(element);
108        if (rank == 0) {
109          // The element is an alternate. It has the default rank, so no explicit rank. Headed toward ambiguity. No need
110          // to look at maxRank etc.
111          if (reductionListContainsOnlyRankedAlternates) {
112            reductionListContainsOnlyRankedAlternates = false;
113          }
114          reductionList.add(element);
115          continue;
116        }
117
118        if (reductionList.isEmpty()) {
119          // The element is an alternate with an explicit rank. The reduction list is empty. The element's rank is
120          // therefore by definition the highest one encountered so far. Add the element to the reduction list.
121          assert !reductionListContainsOnlyRankedAlternates : "Unexpected reductionListContainsOnlyRankedAlternates: " + reductionListContainsOnlyRankedAlternates;
122          assert rank > maxRank : "rank <= maxRank: " + rank + " <= " + maxRank; // TODO: I think this is correct
123          maxRank = rank;
124          reductionList.add(element);
125          reductionListContainsOnlyRankedAlternates = true;
126          continue;
127        }
128
129        if (reductionListContainsOnlyRankedAlternates) {
130          // The element is an alternate. It has an explicit rank. The (non-empty) reduction list is known to contain
131          // only ranked alternates (in fact it should contain exactly one).
132          assert reductionList.size() == 1 : "Unexpected reductionList size: " + reductionList;
133          if (rank > maxRank) {
134            // The element's rank is higher than the rank of the (sole) element in the list. Record the new highest rank
135            // and replace the sole element in the list with this element.
136            maxRank = rank;
137            reductionList.set(0, element);
138          }
139          continue;
140        }
141
142        // The element is an alternate. It has an explicit rank (but this does not matter as we'll see). The list we're
143        // using to store alternates does not have a possibility of reducing to size 1, because it already contains
144        // unranked alternates, so we have to add this element to it, regardless of what its rank is. This operation
145        // will not affect the highest rank.
146        reductionList.add(element);
147      }
148
149      assert reductionListContainsOnlyRankedAlternates ? reductionList.size() == 1 : true : "Unexpected reductionList size: " + reductionList;
150
151      return switch (reductionList.size()) {
152        // No reduction at all took place. "If typesafe resolution results in an ambiguous dependency and the set of
153        // candidate beans contains no alternative, the set of resulting beans contains all candidate beans."
154      case 0 -> elements;
155      case 1 -> List.of(reductionList.get(0)); // Optimization for the common case
156      default -> List.copyOf(reductionList);
157      };
158    };
159  }
160
161}