content: package com.airbnb.lottie.model;
  
  import androidx.annotation.CheckResult;
  import androidx.annotation.Nullable;
  import androidx.annotation.RestrictTo;
  
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  
  /**
   * Defines which content to target.
   * The keypath can contain wildcards ('*') with match exactly 1 item.
   * or globstars ('**') which match 0 or more items. or KeyPath.COMPOSITION
   * to represent the root composition layer.
   * <p>
   * For example, if your content were arranged like this:
   * Gabriel (Shape Layer)
   * Body (Shape Group)
   * Left Hand (Shape)
   * Fill (Fill)
   * Transform (Transform)
   * ...
   * Brandon (Shape Layer)
   * Body (Shape Group)
   * Left Hand (Shape)
   * Fill (Fill)
   * Transform (Transform)
   * ...
   * <p>
   * <p>
   * You could:
   * Match Gabriel left hand fill:
   * new KeyPath("Gabriel", "Body", "Left Hand", "Fill");
   * Match Gabriel and Brandon's left hand fill:
   * new KeyPath("*", "Body", Left Hand", "Fill");
   * Match anything with the name Fill:
   * new KeyPath("**", "Fill");
   * Target the the root composition layer:
   * KeyPath.COMPOSITION
   * <p>
   * <p>
   * NOTE: Content that are part of merge paths or repeaters cannot currently be resolved with
   * a {@link KeyPath}. This may be fixed in the future.
   */
  public class KeyPath {
    /**
     * A singleton KeyPath that targets on the root composition layer.
     * This is useful if you want to apply transformer to the animation as a whole.
     */
    public final static KeyPath COMPOSITION = new KeyPath("COMPOSITION");
  
    private final List<String> keys;
    @Nullable private KeyPathElement resolvedElement;
  
    public KeyPath(String... keys) {
      this.keys = Arrays.asList(keys);
    }
  
    /**
     * Copy constructor. Copies keys as well.
     */
    private KeyPath(KeyPath keyPath) {
      keys = new ArrayList<>(keyPath.keys);
      resolvedElement = keyPath.resolvedElement;
    }
  
    /**
     * Returns a new KeyPath with the key added.
     * This is used during keypath resolution. Children normally don't know about all of their parent
     * elements so this is used to keep track of the fully qualified keypath.
     * This returns a key keypath because during resolution, the full keypath element tree is walked
     * and if this modified the original copy, it would remain after popping back up the element tree.
     */
    @CheckResult
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public KeyPath addKey(String key) {
      KeyPath newKeyPath = new KeyPath(this);
      newKeyPath.keys.add(key);
      return newKeyPath;
    }
  
    /**
     * Return a new KeyPath with the element resolved to the specified {@link KeyPathElement}.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public KeyPath resolve(KeyPathElement element) {
      KeyPath keyPath = new KeyPath(this);
      keyPath.resolvedElement = element;
      return keyPath;
    }
  
    /**
     * Returns a {@link KeyPathElement} that this has been resolved to. KeyPaths get resolved with
     * resolveKeyPath on LottieDrawable or LottieAnimationView.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    @Nullable
    public KeyPathElement getResolvedElement() {
      return resolvedElement;
    }
  
    /**
     * Returns whether they key matches at the specified depth.
     */
    @SuppressWarnings("RedundantIfStatement")
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public boolean matches(String key, int depth) {
      if (isContainer(key)) {
        // This is an artificial layer we programatically create.
        return true;
      }
      if (depth >= keys.size()) {
        return false;
      }
      if (keys.get(depth).equals(key) ||
          keys.get(depth).equals("**") ||
          keys.get(depth).equals("*")) {
        return true;
      }
      return false;
    }
  
    /**
     * For a given key and depth, returns how much the depth should be incremented by when
     * resolving a keypath to children.
     * <p>
     * This can be 0 or 2 when there is a globstar and the next key either matches or doesn't match
     * the current key.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public int incrementDepthBy(String key, int depth) {
      if (isContainer(key)) {
        // If it's a container then we added programatically and it isn't a part of the keypath.
        return 0;
      }
      if (!keys.get(depth).equals("**")) {
        // If it's not a globstar then it is part of the keypath.
        return 1;
      }
      if (depth == keys.size() - 1) {
        // The last key is a globstar.
        return 0;
      }
      if (keys.get(depth + 1).equals(key)) {
        // We are a globstar and the next key is our current key so consume both.
        return 2;
      }
      return 0;
    }
  
    /**
     * Returns whether the key at specified depth is fully specific enough to match the full set of
     * keys in this keypath.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public boolean fullyResolvesTo(String key, int depth) {
      if (depth >= keys.size()) {
        return false;
      }
      boolean isLastDepth = depth == keys.size() - 1;
      String keyAtDepth = keys.get(depth);
      boolean isGlobstar = keyAtDepth.equals("**");
  
      if (!isGlobstar) {
        boolean matches = keyAtDepth.equals(key) || keyAtDepth.equals("*");
        return (isLastDepth || (depth == keys.size() - 2 && endsWithGlobstar())) && matches;
      }
  
      boolean isGlobstarButNextKeyMatches = !isLastDepth && keys.get(depth + 1).equals(key);
      if (isGlobstarButNextKeyMatches) {
        return depth == keys.size() - 2 ||
            (depth == keys.size() - 3 && endsWithGlobstar());
      }
  
      if (isLastDepth) {
        return true;
      }
      if (depth + 1 < keys.size() - 1) {
        // We are a globstar but there is more than 1 key after the globstar we we can't fully match.
        return false;
      }
      // Return whether the next key (which we now know is the last one) is the same as the current
      // key.
      return keys.get(depth + 1).equals(key);
    }
  
    /**
     * Returns whether the keypath resolution should propagate to children. Some keypaths resolve
     * to content other than leaf contents (such as a layer or content group transform) so sometimes
     * this will return false.
     */
    @SuppressWarnings("SimplifiableIfStatement")
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public boolean propagateToChildren(String key, int depth) {
      if ("__container".equals(key)) {
        return true;
      }
      return depth < keys.size() - 1 || keys.get(depth).equals("**");
    }
  
    /**
     * We artificially create some container groups (like a root ContentGroup for the entire animation
     * and for the contents of a ShapeLayer).
     */
    private boolean isContainer(String key) {
      return "__container".equals(key);
    }
  
    private boolean endsWithGlobstar() {
      return keys.get(keys.size() - 1).equals("**");
    }
  
    public String keysToString() {
      return keys.toString();
    }
  
    @Override public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
  
      KeyPath keyPath = (KeyPath) o;
  
      if (!keys.equals(keyPath.keys)) {
        return false;
      }
      return resolvedElement != null ? resolvedElement.equals(keyPath.resolvedElement) : keyPath.resolvedElement == null;
    }
  
    @Override public int hashCode() {
      int result = keys.hashCode();
      result = 31 * result + (resolvedElement != null ? resolvedElement.hashCode() : 0);
      return result;
    }
  
    @Override public String toString() {
      return "KeyPath{" + "keys=" + keys + ",resolved=" + (resolvedElement != null) + '}';
    }
  }
  