package net.bytebuddy.matcher;

import lombok.EqualsAndHashCode;

/**
 * An element matcher is used as a predicate for identifying code elements such as types, methods, fields or
 * annotations. They are similar to Java 8's {@code Predicate}s but compatible to Java 6 and Java 7 and represent
 * a functional interface. They can be chained by using instances of
 * {@link net.bytebuddy.matcher.ElementMatcher.Junction}.
 *
 * @param <T> The type of the object that is being matched.
 */
public interface ElementMatcher<T> {

    /**
     * Matches a target against this element matcher.
     *
     * @param target The instance to be matched.
     * @return {@code true} if the given element is matched by this matcher or {@code false} otherwise.
     */
    boolean matches(T target);

    /**
     * A junctions allows to chain different {@link net.bytebuddy.matcher.ElementMatcher}s in a readable manner.
     *
     * @param <S> The type of the object that is being matched.
     */
    interface Junction<S> extends ElementMatcher<S> {

        /**
         * Creates a conjunction where this matcher and the {@code other} matcher must both be matched in order
         * to constitute a successful match. The other matcher is only invoked if this matcher constitutes a successful
         * match.
         *
         * @param other The second matcher to consult.
         * @param <U>   The type of the object that is being matched. Note that Java's type inference might not
         *              be able to infer the common subtype of this instance and the {@code other} matcher such that
         *              this type must need to be named explicitly.
         * @return A conjunction of this matcher and the other matcher.
         */
        <U extends S> Junction<U> and(ElementMatcher<? super U> other);

        /**
         * Creates a disjunction where either this matcher or the {@code other} matcher must be matched in order
         * to constitute a successful match. The other matcher is only invoked if this matcher constitutes an
         * unsuccessful match.
         *
         * @param other The second matcher to consult.
         * @param <U>   The type of the object that is being matched. Note that Java's type inference might not
         *              be able to infer the common subtype of this instance and the {@code other} matcher such that
         *              this type must need to be named explicitly.
         * @return A disjunction of this matcher and the other matcher.
         */
        <U extends S> Junction<U> or(ElementMatcher<? super U> other);

        /**
         * A base implementation of {@link net.bytebuddy.matcher.ElementMatcher.Junction}.
         *
         * @param <V> The type of the object that is being matched.
         */
        abstract class AbstractBase<V> implements Junction<V> {

            @Override
            public <U extends V> Junction<U> and(ElementMatcher<? super U> other) {
                return new Conjunction<U>(this, other);
            }

            @Override
            public <U extends V> Junction<U> or(ElementMatcher<? super U> other) {
                return new Disjunction<U>(this, other);
            }
        }

        /**
         * A conjunction matcher which only matches an element if both represented matchers constitute a match.
         *
         * @param <W> The type of the object that is being matched.
         */
        @EqualsAndHashCode(callSuper = false)
        class Conjunction<W> extends AbstractBase<W> {

            /**
             * The element matchers that constitute this conjunction.
             */
            private final ElementMatcher<? super W> left, right;

            /**
             * Creates a new conjunction matcher.
             *
             * @param left  The first matcher to consult for a match.
             * @param right The second matcher to consult for a match. This matcher is only consulted
             *              if the {@code first} matcher constituted a match.
             */
            public Conjunction(ElementMatcher<? super W> left, ElementMatcher<? super W> right) {
                this.left = left;
                this.right = right;
            }

            @Override
            public boolean matches(W target) {
                return left.matches(target) && right.matches(target);
            }

            @Override
            public String toString() {
                return "(" + left + " and " + right + ')';
            }
        }

        /**
         * A disjunction matcher which only matches an element if both represented matchers constitute a match.
         *
         * @param <W> The type of the object that is being matched.
         */
        @EqualsAndHashCode(callSuper = false)
        class Disjunction<W> extends AbstractBase<W> {

            /**
             * The element matchers that constitute this disjunction.
             */
            private final ElementMatcher<? super W> left, right;

            /**
             * Creates a new disjunction matcher.
             *
             * @param left  The first matcher to consult for a match.
             * @param right The second matcher to consult for a match. This matcher is only consulted
             *              if the {@code first} matcher did not already constitute a match.
             */
            public Disjunction(ElementMatcher<? super W> left, ElementMatcher<? super W> right) {
                this.left = left;
                this.right = right;
            }

            @Override
            public boolean matches(W target) {
                return left.matches(target) || right.matches(target);
            }

            @Override
            public String toString() {
                return "(" + left + " or " + right + ')';
            }
        }
    }
}
