class TypeSubstitution {

        private List<ResolvedTypeParameterDeclaration> typeParameterDeclarations;
        private List<ResolvedType> types;

        private final static TypeSubstitution EMPTY = new TypeSubstitution();

        public static TypeSubstitution empty() {
            return new TypeSubstitution();
        }

        private TypeSubstitution() {
            this.typeParameterDeclarations = new LinkedList<>();
            this.types = new LinkedList<>();
        }

        public boolean isEmpty() {
            return this == EMPTY;
        }

        public void withPair(ResolvedTypeParameterDeclaration typeParameterDeclaration, ResolvedType type) {
            this.typeParameterDeclarations.add(typeParameterDeclaration);
            this.types.add(type);
        }

        public List<ResolvedTypeParameterDeclaration> typeParameterDeclarations() {
            return typeParameterDeclarations;
        }

        public ResolvedType substitutedType(ResolvedTypeParameterDeclaration typeDecl) {
            int index = typeParameterDeclarations.indexOf(typeDecl);
            return index > -1 ? types.get(index) : typeDecl.object();
        }

    }

    private TypeSubstitution substitution(List<Pair<ResolvedTypeParameterDeclaration, ResolvedType>> pairs) {
        TypeSubstitution substitution = TypeSubstitution.empty();
        pairs.stream().forEach(pair -> substitution.withPair(pair.a, pair.b));
        return substitution;
    }

    /*
     * the least containing type argument, is: (assuming U and V are types)
     * lcta(U, V) = U if U = V, otherwise ? extends lub(U, V)
     * lcta(U, ? extends V) = ? extends lub(U, V)
     * lcta(U, ? super V) = ? super glb(U, V)
     * lcta(? extends U, ? extends V) = ? extends lub(U, V)
     * lcta(? extends U, ? super V) = U if U = V, otherwise ?
     * lcta(? super U, ? super V) = ? super glb(U, V)
     * lcta(U) = ? if U's upper bound is Object, otherwise ? extends lub(U,Object)
     * and where glb() is as defined in §5.1.10.
     */
    private ResolvedType lcta(ResolvedType type1, ResolvedType type2) {
        boolean isWildcard1 = type1.isWildcard();
        boolean isWildcard2 = type2.isWildcard();

        ResolvedType result;
        if (type1.equals(type2)) {
            // lcta(U, V) = U if U = V
            result = type1;
        } else if (isWildcard1 && isWildcard2) {
            result = lctaBothWildcards(type1.asWildcard(), type2.asWildcard());
        } else if (isWildcard1 ^ isWildcard2) {
            ResolvedType rawType = isWildcard1 ? type2 : type1;
            ResolvedWildcard wildcardType = (ResolvedWildcard) (isWildcard1 ? type1 : type2);
            result = lctaOneWildcard(rawType, wildcardType);
        } else {
            // otherwise lcta(U, V) = ? extends lub(U, V)
            result = lctaNoWildcard(type1, type2);
        }
        return result;
    }

    /*
     * lcta(U, V) = U if U = V, otherwise ? extends lub(U, V)
     */
    private ResolvedType lctaNoWildcard(ResolvedType type1, ResolvedType type2) {
        ResolvedType lub = lub(toSet(type1, type2));
        return bound(lub, BoundType.EXTENDS);
    }

    private ResolvedType lctaOneWildcard(ResolvedType rawType, ResolvedWildcard wildcardType) {
        if (wildcardType.isUpperBounded()) {
            ResolvedType glb = TypeHelper.glb(toSet(rawType, wildcardType.getBoundedType()));
            return bound(glb, BoundType.SUPER);
        }
        ResolvedType lub = lub(toSet(rawType, wildcardType.getBoundedType()));
        return bound(lub, BoundType.EXTENDS);
    }

    private ResolvedType lctaBothWildcards(ResolvedWildcard type1, ResolvedWildcard type2) {<FILL_FUNCTION_BODY>

        // lcta(? super U, ? super V) = ? super glb(U, V)
        if (type1.isUpperBounded() && type2.isUpperBounded()) {
            ResolvedType glb = TypeHelper.glb(toSet(type1.getBoundedType(), type2.getBoundedType()));
            return bound(glb, BoundType.SUPER);
        }
        // lcta(? extends U, ? extends V) = ? extends lub(U, V)
        if (type1.isLowerBounded() && type2.isLowerBounded()) {
            ResolvedType lub = lub(toSet(type1.getBoundedType(), type2.getBoundedType()));
            return bound(lub, BoundType.EXTENDS);
        }
        // lcta(? extends U, ? super V) = U if U = V, otherwise ?
        if (type1.getBoundedType().equals(type2.getBoundedType())) {
            return type1.getBoundedType();
        }
        return ResolvedWildcard.UNBOUNDED;
    
<no_super_class>