/*
 * Copyright 2007-2011 Rickard Öberg.
 * Copyright 2007-2010 Niclas Hedhman.
 * Copyright 2008 Alin Dreghiciu.
 * Copyright 2012 Stanislav Muhametsin.
 * Copyright 2012-2014 Paul Merlin.
 *
 * Licensed 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
 * ied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.qi4j.api.query;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import org.qi4j.api.association.Association;
import org.qi4j.api.association.GenericAssociationInfo;
import org.qi4j.api.association.ManyAssociation;
import org.qi4j.api.association.NamedAssociation;
import org.qi4j.api.composite.Composite;
import org.qi4j.api.entity.Identity;
import org.qi4j.api.injection.scope.State;
import org.qi4j.api.property.GenericPropertyInfo;
import org.qi4j.api.property.Property;
import org.qi4j.api.query.grammar.AndSpecification;
import org.qi4j.api.query.grammar.AssociationFunction;
import org.qi4j.api.query.grammar.AssociationNotNullSpecification;
import org.qi4j.api.query.grammar.AssociationNullSpecification;
import org.qi4j.api.query.grammar.ContainsAllSpecification;
import org.qi4j.api.query.grammar.ContainsSpecification;
import org.qi4j.api.query.grammar.EqSpecification;
import org.qi4j.api.query.grammar.GeSpecification;
import org.qi4j.api.query.grammar.GtSpecification;
import org.qi4j.api.query.grammar.LeSpecification;
import org.qi4j.api.query.grammar.LtSpecification;
import org.qi4j.api.query.grammar.ManyAssociationContainsSpecification;
import org.qi4j.api.query.grammar.ManyAssociationFunction;
import org.qi4j.api.query.grammar.MatchesSpecification;
import org.qi4j.api.query.grammar.NamedAssociationContainsNameSpecification;
import org.qi4j.api.query.grammar.NamedAssociationContainsSpecification;
import org.qi4j.api.query.grammar.NamedAssociationFunction;
import org.qi4j.api.query.grammar.NeSpecification;
import org.qi4j.api.query.grammar.NotSpecification;
import org.qi4j.api.query.grammar.OrSpecification;
import org.qi4j.api.query.grammar.OrderBy;
import org.qi4j.api.query.grammar.PropertyFunction;
import org.qi4j.api.query.grammar.PropertyNotNullSpecification;
import org.qi4j.api.query.grammar.PropertyNullSpecification;
import org.qi4j.api.query.grammar.PropertyReference;
import org.qi4j.api.query.grammar.Variable;
import org.qi4j.api.util.NullArgumentException;
import org.qi4j.functional.Specification;

import static org.qi4j.functional.Iterables.first;
import static org.qi4j.functional.Iterables.prepend;

/**
 * Static factory methods for query expressions and operators.
 */
public final class QueryExpressions
{
    // This is used for eq(Association,Composite)
    private static final Method IDENTITY_METHOD;

    static
    {
        try
        {
            IDENTITY_METHOD = Identity.class.getMethod( "identity" );
        }
        catch( NoSuchMethodException e )
        {
            throw new InternalError( "Zest Core API codebase is corrupted. Contact Zest team: QueryExpressions" );
        }
    }

    // Templates and variables -----------------------------------------------|

    /**
     * Create a Query Template using the given type.
     *
     * @param <T> the type of the template
     * @param clazz a class declaring the type of the template
     *
     * @return a new Query Template
     */
    public static <T> T templateFor( Class<T> clazz )
    {
        NullArgumentException.validateNotNull( "Template class", clazz );

        if( clazz.isInterface() )
        {
            return clazz.cast( Proxy.newProxyInstance( clazz.getClassLoader(),
                                                       array( clazz ),
                                                       new TemplateHandler<T>( null, null, null, null ) ) );
        }
        else
        {
            try
            {
                T mixin = clazz.newInstance();
                for( Field field : clazz.getFields() )
                {
                    if( field.getAnnotation( State.class ) != null )
                    {
                        if( field.getType().equals( Property.class ) )
                        {
                            field.set( mixin,
                                       Proxy.newProxyInstance( field.getType().getClassLoader(),
                                                               array( field.getType() ),
                                                               new PropertyReferenceHandler<>( new PropertyFunction<T>( null, null, null, null, field ) ) ) );
                        }
                        else if( field.getType().equals( Association.class ) )
                        {
                            field.set( mixin,
                                       Proxy.newProxyInstance( field.getType().getClassLoader(),
                                                               array( field.getType() ),
                                                               new AssociationReferenceHandler<>( new AssociationFunction<T>( null, null, null, field ) ) ) );
                        }
                        else if( field.getType().equals( ManyAssociation.class ) )
                        {
                            field.set( mixin,
                                       Proxy.newProxyInstance( field.getType().getClassLoader(),
                                                               array( field.getType() ),
                                                               new ManyAssociationReferenceHandler<>( new ManyAssociationFunction<T>( null, null, null, field ) ) ) );
                        }
                        else if( field.getType().equals( NamedAssociation.class ) )
                        {
                            field.set( mixin,
                                       Proxy.newProxyInstance( field.getType().getClassLoader(),
                                                               array( field.getType() ),
                                                               new NamedAssociationReferenceHandler<>( new NamedAssociationFunction<T>( null, null, null, field ) ) ) );
                        }
                    }
                }
                return mixin;
            }
            catch( IllegalAccessException | IllegalArgumentException | InstantiationException | SecurityException e )
            {
                throw new IllegalArgumentException( "Cannot use class as template", e );
            }
        }
    }

    /**
     * Create a Query Template using the given mixin class and association.
     *
     * @param <T> the type of the template
     * @param mixinType  a class declaring the type of the template
     * @param association an association
     *
     * @return a new Query Template
     */
    public static <T> T templateFor( final Class<T> mixinType, Association<?> association )
    {
        NullArgumentException.validateNotNull( "Mixin class", mixinType );
        NullArgumentException.validateNotNull( "Association", association );
        return mixinType.cast( Proxy.newProxyInstance( mixinType.getClassLoader(),
                                                       array( mixinType ),
                                                       new TemplateHandler<T>( null,
                                                                               association( association ),
                                                                               null,
                                                                               null ) ) );
    }

    public static <T> T oneOf( final ManyAssociation<T> association )
    {
        NullArgumentException.validateNotNull( "Association", association );
        return association.get( 0 );
    }

    public static <T> T oneOf( final NamedAssociation<T> association )
    {
        NullArgumentException.validateNotNull( "Association", association );
        return association.get( first( association ) );
    }

    /**
     * Create a new Query Variable.
     *
     * @param name a name for the Variable
     *
     * @return a new Query Variable.
     */
    public static Variable variable( String name )
    {
        NullArgumentException.validateNotNull( "Variable name", name );
        return new Variable( name );
    }

    /**
     * Create a new Query Template PropertyFunction.
     *
     * @param <T> type of the Property
     * @param property a Property
     *
     * @return a new Query Template PropertyFunction
     */
    @SuppressWarnings( "unchecked" )
    public static <T> PropertyFunction<T> property( Property<T> property )
    {
        return ( (PropertyReferenceHandler<T>) Proxy.getInvocationHandler( property ) ).property();
    }

    /**
     * Create a new Query Property instance.
     *
     * @param <T> type of the Property
     * @param mixinClass mixin of the Property
     * @param fieldName name of the Property field
     *
     * @return a new Query Property instance for the given mixin and property name.
     */
    @SuppressWarnings( "unchecked" )
    public static <T> Property<T> property( Class<?> mixinClass, String fieldName )
    {
        try
        {
            Field field = mixinClass.getField( fieldName );
            if( !Property.class.isAssignableFrom( field.getType() ) )
            {
                throw new IllegalArgumentException( "Field must be of type Property<?>" );
            }
            return (Property<T>) Proxy.newProxyInstance(
                mixinClass.getClassLoader(),
                array( field.getType() ),
                new PropertyReferenceHandler<>( new PropertyFunction<T>( null, null, null, null, field ) ) );
        }
        catch( NoSuchFieldException e )
        {
            throw new IllegalArgumentException( "No such field '" + fieldName + "' in mixin " + mixinClass.getName() );
        }
    }

    /**
     * Create a new Query Template AssociationFunction.
     *
     * @param <T> type of the Association
     * @param association an Association
     *
     * @return a new Query Template AssociationFunction
     */
    @SuppressWarnings( "unchecked" )
    public static <T> AssociationFunction<T> association( Association<T> association )
    {
        return ( (AssociationReferenceHandler<T>) Proxy.getInvocationHandler( association ) ).association();
    }

    /**
     * Create a new Query Template ManyAssociationFunction.
     *
     * @param <T> type of the ManyAssociation
     * @param association a ManyAssociation
     *
     * @return a new Query Template ManyAssociationFunction
     */
    @SuppressWarnings( "unchecked" )
    public static <T> ManyAssociationFunction<T> manyAssociation( ManyAssociation<T> association )
    {
        return ( (ManyAssociationReferenceHandler<T>) Proxy.getInvocationHandler( association ) ).manyAssociation();
    }

    /**
     * Create a new Query Template NamedAssociationFunction.
     *
     * @param <T> type of the NamedAssociation
     * @param association a NamedAssociation
     *
     * @return a new Query Template NamedAssociationFunction
     */
    @SuppressWarnings( "unchecked" )
    public static <T> NamedAssociationFunction<T> namedAssociation( NamedAssociation<T> association )
    {
        return ( (NamedAssociationReferenceHandler<T>) Proxy.getInvocationHandler( association ) ).namedAssociation();
    }

    // And/Or/Not ------------------------------------------------------------|
    /**
     * Create a new AND specification.
     *
     * @param left first operand
     * @param right second operand
     * @param optionalRight optional operands
     *
     * @return a new AND specification
     */
    @SafeVarargs
    public static AndSpecification and( Specification<Composite> left,
                                        Specification<Composite> right,
                                        Specification<Composite>... optionalRight
    )
    {
        return new AndSpecification( prepend( left, prepend( right, Arrays.asList( optionalRight ) ) ) );
    }

    /**
     * Create a new OR specification.
     *
     * @param specs operands
     *
     * @return a new OR specification
     */
    @SafeVarargs
    public static OrSpecification or( Specification<Composite>... specs )
    {
        return new OrSpecification( Arrays.asList( specs ) );
    }

    /**
     * Create a new NOT specification.
     *
     * @param operand specification to be negated
     *
     * @return a new NOT specification
     */
    public static NotSpecification not( Specification<Composite> operand )
    {
        return new NotSpecification( operand );
    }

    // Comparisons -----------------------------------------------------------|

    /**
     * Create a new EQUALS specification for a Property.
     *
     * @param property a Property
     * @param value its value
     *
     * @return a new EQUALS specification for a Property.
     */
    public static <T> EqSpecification<T> eq( Property<T> property, T value )
    {
        return new EqSpecification<>( property( property ), value );
    }

    /**
     * Create a new EQUALS specification for a Property using a named Variable.
     *
     * @param property a Property
     * @param variable a Query Variable
     *
     * @return a new EQUALS specification for a Property using a named Variable.
     */
    @SuppressWarnings( {"raw", "unchecked"} )
    public static <T> EqSpecification<T> eq( Property<T> property, Variable variable )
    {
        return new EqSpecification( property( property ), variable );
    }

    /**
     * Create a new EQUALS specification for an Association.
     *
     * @param association an Association
     * @param value its value
     *
     * @return a new EQUALS specification for an Association.
     */
    public static <T> EqSpecification<String> eq( Association<T> association, T value )
    {
        return new EqSpecification<>( new PropertyFunction<String>( null,
                                                                    association( association ),
                                                                    null,
                                                                    null,
                                                                    IDENTITY_METHOD ),
                                      value.toString() );
    }

    /**
     * Create a new GREATER OR EQUALS specification for a Property.
     *
     * @param property a Property
     * @param value its value
     *
     * @return a new GREATER OR EQUALS specification for a Property.
     */
    public static <T> GeSpecification<T> ge( Property<T> property, T value )
    {
        return new GeSpecification<>( property( property ), value );
    }

    /**
     * Create a new GREATER OR EQUALS specification for a Property using a named Variable.
     *
     * @param property a Property
     * @param variable a Query Variable
     *
     * @return a new GREATER OR EQUALS specification for a Property using a named Variable.
     */
    @SuppressWarnings( {"raw", "unchecked"} )
    public static <T> GeSpecification<T> ge( Property<T> property, Variable variable )
    {
        return new GeSpecification( property( property ), variable );
    }

    /**
     * Create a new GREATER THAN specification for a Property.
     *
     * @param property a Property
     * @param value its value
     *
     * @return a new GREATER THAN specification for a Property.
     */
    public static <T> GtSpecification<T> gt( Property<T> property, T value )
    {
        return new GtSpecification<>( property( property ), value );
    }

    /**
     * Create a new GREATER THAN specification for a Property using a named Variable.
     *
     * @param property a Property
     * @param variable a Query Variable
     *
     * @return a new GREATER THAN specification for a Property using a named Variable.
     */
    @SuppressWarnings( {"raw", "unchecked"} )
    public static <T> GtSpecification<T> gt( Property<T> property, Variable variable )
    {
        return new GtSpecification( property( property ), variable );
    }

    /**
     * Create a new LESS OR EQUALS specification for a Property.
     *
     * @param property a Property
     * @param value its value
     *
     * @return a new LESS OR EQUALS specification for a Property.
     */
    public static <T> LeSpecification<T> le( Property<T> property, T value )
    {
        return new LeSpecification<>( property( property ), value );
    }

    /**
     * Create a new LESS OR EQUALS specification for a Property using a named Variable.
     *
     * @param property a Property
     * @param variable a Query Variable
     *
     * @return a new LESS OR EQUALS specification for a Property using a named Variable.
     */
    @SuppressWarnings( {"raw", "unchecked"} )
    public static <T> LeSpecification<T> le( Property<T> property, Variable variable )
    {
        return new LeSpecification( property( property ), variable );
    }

    /**
     * Create a new LESSER THAN specification for a Property.
     *
     * @param property a Property
     * @param value its value
     *
     * @return a new LESSER THAN specification for a Property.
     */
    public static <T> LtSpecification<T> lt( Property<T> property, T value )
    {
        return new LtSpecification<>( property( property ), value );
    }

    /**
     * Create a new LESSER THAN specification for a Property using a named Variable.
     *
     * @param property a Property
     * @param variable a Query Variable
     *
     * @return a new LESSER THAN specification for a Property using a named Variable.
     */
    @SuppressWarnings( {"raw", "unchecked"} )
    public static <T> LtSpecification<T> lt( Property<T> property, Variable variable )
    {
        return new LtSpecification( property( property ), variable );
    }

    /**
     * Create a new NOT EQUALS specification for a Property.
     *
     * @param property a Property
     * @param value its value
     *
     * @return a new NOT EQUALS specification for a Property.
     */
    public static <T> NeSpecification<T> ne( Property<T> property, T value )
    {
        return new NeSpecification<>( property( property ), value );
    }

    /**
     * Create a new NOT EQUALS specification for a Property using a named Variable.
     *
     * @param property a Property
     * @param variable a Query Variable
     *
     * @return a new NOT EQUALS specification for a Property using a named Variable.
     */
    @SuppressWarnings( {"raw", "unchecked"} )
    public static <T> NeSpecification<T> ne( Property<T> property, Variable variable )
    {
        return new NeSpecification( property( property ), variable );
    }

    /**
     * Create a new REGULAR EXPRESSION specification for a Property.
     *
     * @param property a Property
     * @param regexp its value
     *
     * @return a new REGULAR EXPRESSION specification for a Property.
     */
    public static MatchesSpecification matches( Property<String> property, String regexp )
    {
        return new MatchesSpecification( property( property ), regexp );
    }

    /**
     * Create a new REGULAR EXPRESSION specification for a Property using a named Variable.
     *
     * @param property a Property
     * @param variable a Query Variable
     *
     * @return a new REGULAR EXPRESSION specification for a Property using a named Variable.
     */
    public static MatchesSpecification matches( Property<String> property, Variable variable )
    {
        return new MatchesSpecification( property( property ), variable );
    }

    // Null checks -----------------------------------------------------------|

    /**
     * Create a new NOT NULL specification for a Property.
     *
     * @param property a Property
     *
     * @return a new NOT NULL specification for a Property.
     */
    public static <T> PropertyNotNullSpecification<T> isNotNull( Property<T> property )
    {
        return new PropertyNotNullSpecification<>( property( property ) );
    }

    /**
     * Create a new NULL specification for a Property.
     *
     * @param property a Property
     *
     * @return a new NULL specification for a Property.
     */
    public static <T> PropertyNullSpecification<T> isNull( Property<T> property )
    {
        return new PropertyNullSpecification<>( property( property ) );
    }

    /**
     * Create a new NOT NULL specification for an Association.
     *
     * @param association an Association
     *
     * @return a new NOT NULL specification for an Association.
     */
    public static <T> AssociationNotNullSpecification<T> isNotNull( Association<T> association )
    {
        return new AssociationNotNullSpecification<>( association( association ) );
    }

    /**
     * Create a new NULL specification for an Association.
     *
     * @param association an Association
     *
     * @return a new NULL specification for an Association.
     */
    public static <T> AssociationNullSpecification<T> isNull( Association<T> association )
    {
        return new AssociationNullSpecification<>( association( association ) );
    }

    // Collections -----------------------------------------------------------|

    /**
     * Create a new CONTAINS ALL specification for a Collection Property.
     *
     * @param collectionProperty a Collection Property
     * @param values its values
     *
     * @return a new CONTAINS ALL specification for a Collection Property.
     */
    public static <T> ContainsAllSpecification<T> containsAll( Property<? extends Collection<T>> collectionProperty,
                                                               Iterable<T> values )
    {
        NullArgumentException.validateNotNull( "Values", values );
        return new ContainsAllSpecification<>( property( collectionProperty ), values );
    }

    /**
     * Create a new CONTAINS ALL specification for a Collection Property using named Variables.
     *
     * @param collectionProperty a Collection Property
     * @param variables named Variables
     *
     * @return a new CONTAINS ALL specification for a Collection Property using named Variables.
     */
    @SuppressWarnings( {"raw", "unchecked"} )
    public static <T> ContainsAllSpecification<T> containsAllVariables(
        Property<? extends Collection<T>> collectionProperty,
        Iterable<Variable> variables )
    {
        NullArgumentException.validateNotNull( "Variables", variables );
        return new ContainsAllSpecification( property( collectionProperty ), variables );
    }

    /**
     * Create a new CONTAINS specification for a Collection Property.
     *
     * @param collectionProperty a Collection Property
     * @param value the value
     *
     * @return a new CONTAINS specification for a Collection Property.
     */
    public static <T> ContainsSpecification<T> contains( Property<? extends Collection<T>> collectionProperty,
                                                         T value )
    {
        NullArgumentException.validateNotNull( "Value", value );
        return new ContainsSpecification<>( property( collectionProperty ), value );
    }

    /**
     * Create a new CONTAINS specification for a Collection Property using named Variables.
     *
     * @param collectionProperty a Collection Property
     * @param variable named Variable
     *
     * @return a new CONTAINS specification for a Collection Property using named Variables.
     */
    @SuppressWarnings( {"raw", "unchecked"} )
    public static <T> ContainsSpecification<T> contains( Property<? extends Collection<T>> collectionProperty,
                                                         Variable variable )
    {
        NullArgumentException.validateNotNull( "Variable", variable );
        return new ContainsSpecification( property( collectionProperty ), variable );
    }

    /**
     * Create a new CONTAINS specification for a ManyAssociation.
     *
     * @param manyAssoc  a ManyAssociation
     * @param value the value
     *
     * @return a new CONTAINS specification for a ManyAssociation.
     */
    public static <T> ManyAssociationContainsSpecification<T> contains( ManyAssociation<T> manyAssoc, T value )
    {
        return new ManyAssociationContainsSpecification<>( manyAssociation( manyAssoc ), value );
    }

    /**
     * Create a new CONTAINS specification for a NamedAssociation.
     *
     * @param namedAssoc  a NamedAssociation
     * @param value the value
     *
     * @return a new CONTAINS specification for a NamedAssociation.
     */
    public static <T> NamedAssociationContainsSpecification<T> contains( NamedAssociation<T> namedAssoc, T value )
    {
        return new NamedAssociationContainsSpecification<>( namedAssociation( namedAssoc ), value );
    }

    /**
     * Create a new CONTAINS NAME specification for a NamedAssociation.
     *
     * @param namedAssoc  a NamedAssociation
     * @param name the name
     *
     * @return a new CONTAINS NAME specification for a NamedAssociation.
     */
    public static <T> NamedAssociationContainsNameSpecification<T> containsName( NamedAssociation<T> namedAssoc,
                                                                                 String name )
    {
        return new NamedAssociationContainsNameSpecification<>( namedAssociation( namedAssoc ), name );
    }

    // Ordering --------------------------------------------------------------|
    /**
     * Create a new Query ascending order segment for a Property.
     *
     * @param <T> type of the Property
     * @param property a Property
     *
     * @return a new Query ascending order segment for a Property.
     */
    public static <T> OrderBy orderBy( final Property<T> property )
    {
        return orderBy( property, OrderBy.Order.ASCENDING );
    }

    /**
     * Create a new Query ordering segment for a Property.
     *
     * @param <T> type of the Property
     * @param property a Property
     * @param order ascending or descending
     *
     * @return a new Query ordering segment for a Property.
     */
    public static <T> OrderBy orderBy( final Property<T> property, final OrderBy.Order order )
    {
        return new OrderBy( property( property ), order );
    }

    // Query Templates InvocationHandlers ------------------------------------|

    private static class TemplateHandler<T>
        implements InvocationHandler
    {
        private final PropertyFunction<?> compositeProperty;
        private final AssociationFunction<?> compositeAssociation;
        private final ManyAssociationFunction<?> compositeManyAssociation;
        private final NamedAssociationFunction<?> compositeNamedAssociation;

        private TemplateHandler( PropertyFunction<?> compositeProperty,
                                 AssociationFunction<?> compositeAssociation,
                                 ManyAssociationFunction<?> compositeManyAssociation,
                                 NamedAssociationFunction<?> compositeNamedAssociation
        )
        {
            this.compositeProperty = compositeProperty;
            this.compositeAssociation = compositeAssociation;
            this.compositeManyAssociation = compositeManyAssociation;
            this.compositeNamedAssociation = compositeNamedAssociation;
        }

        @Override
        public Object invoke( Object o, Method method, Object[] objects )
            throws Throwable
        {
            if( Property.class.isAssignableFrom( method.getReturnType() ) )
            {
                return Proxy.newProxyInstance(
                    method.getReturnType().getClassLoader(),
                    array( method.getReturnType() ),
                    new PropertyReferenceHandler<>( new PropertyFunction<T>( compositeProperty,
                                                                             compositeAssociation,
                                                                             compositeManyAssociation,
                                                                             compositeNamedAssociation,
                                                                             method ) ) );
            }
            else if( Association.class.isAssignableFrom( method.getReturnType() ) )
            {
                return Proxy.newProxyInstance(
                    method.getReturnType().getClassLoader(),
                    array( method.getReturnType() ),
                    new AssociationReferenceHandler<>( new AssociationFunction<T>( compositeAssociation,
                                                                                   compositeManyAssociation,
                                                                                   compositeNamedAssociation,
                                                                                   method ) ) );
            }
            else if( ManyAssociation.class.isAssignableFrom( method.getReturnType() ) )
            {
                return Proxy.newProxyInstance(
                    method.getReturnType().getClassLoader(),
                    array( method.getReturnType() ),
                    new ManyAssociationReferenceHandler<>( new ManyAssociationFunction<T>( compositeAssociation,
                                                                                           compositeManyAssociation,
                                                                                           compositeNamedAssociation,
                                                                                           method ) ) );
            }
            else if( NamedAssociation.class.isAssignableFrom( method.getReturnType() ) )
            {
                return Proxy.newProxyInstance(
                    method.getReturnType().getClassLoader(),
                    array( method.getReturnType() ),
                    new NamedAssociationReferenceHandler<>( new NamedAssociationFunction<T>( compositeAssociation,
                                                                                             compositeManyAssociation,
                                                                                             compositeNamedAssociation,
                                                                                             method ) ) );
            }

            return null;
        }
    }

    private static class PropertyReferenceHandler<T>
        implements InvocationHandler
    {
        private final PropertyFunction<T> property;

        private PropertyReferenceHandler( PropertyFunction<T> property )
        {
            this.property = property;
        }

        private PropertyFunction<T> property()
        {
            return property;
        }

        @Override
        public Object invoke( Object o, final Method method, Object[] objects )
            throws Throwable
        {
            if( method.equals( Property.class.getMethod( "get" ) ) )
            {
                Type propertyType = GenericPropertyInfo.propertyTypeOf( property.accessor() );
                if( propertyType.getClass().equals( Class.class ) )
                {
                    return Proxy.newProxyInstance( method.getDeclaringClass().getClassLoader(),
                                                   array( (Class<?>) propertyType, PropertyReference.class ),
                                                   new TemplateHandler<T>( property, null, null, null ) );
                }
            }

            return null;
        }
    }

    private static class AssociationReferenceHandler<T>
        implements InvocationHandler
    {
        private final AssociationFunction<T> association;

        private AssociationReferenceHandler( AssociationFunction<T> association )
        {
            this.association = association;
        }

        private AssociationFunction<T> association()
        {
            return association;
        }

        @Override
        public Object invoke( Object o, final Method method, Object[] objects )
            throws Throwable
        {
            if( method.equals( Association.class.getMethod( "get" ) ) )
            {
                Type associationType = GenericAssociationInfo.associationTypeOf( association.accessor() );
                if( associationType.getClass().equals( Class.class ) )
                {
                    return Proxy.newProxyInstance( method.getDeclaringClass().getClassLoader(),
                                                   array( (Class) associationType, PropertyReference.class ),
                                                   new TemplateHandler<T>( null, association, null, null ) );
                }
            }

            return null;
        }
    }

    private static class ManyAssociationReferenceHandler<T>
        implements InvocationHandler
    {
        private final ManyAssociationFunction<T> manyAssociation;

        private ManyAssociationReferenceHandler( ManyAssociationFunction<T> manyAssociation )
        {
            this.manyAssociation = manyAssociation;
        }

        public ManyAssociationFunction<T> manyAssociation()
        {
            return manyAssociation;
        }

        @Override
        public Object invoke( Object o, final Method method, Object[] objects )
            throws Throwable
        {
            if( method.equals( ManyAssociation.class.getMethod( "get", Integer.TYPE ) ) )
            {
                Type manyAssociationType = GenericAssociationInfo.associationTypeOf( manyAssociation.accessor() );
                if( manyAssociationType.getClass().equals( Class.class ) )
                {
                    return Proxy.newProxyInstance( method.getDeclaringClass().getClassLoader(),
                                                   array( (Class) manyAssociationType, PropertyReference.class ),
                                                   new TemplateHandler<T>( null, null, manyAssociation, null ) );
                }
            }

            return null;
        }
    }

    private static class NamedAssociationReferenceHandler<T>
        implements InvocationHandler
    {
        private final NamedAssociationFunction<T> namedAssociation;

        private NamedAssociationReferenceHandler( NamedAssociationFunction<T> namedAssociation )
        {
            this.namedAssociation = namedAssociation;
        }

        public NamedAssociationFunction<T> namedAssociation()
        {
            return namedAssociation;
        }

        @Override
        public Object invoke( Object o, final Method method, Object[] objects )
            throws Throwable
        {
            if( method.equals( NamedAssociation.class.getMethod( "get", String.class ) ) )
            {
                Type namedAssociationType = GenericAssociationInfo.associationTypeOf( namedAssociation.accessor() );
                if( namedAssociationType.getClass().equals( Class.class ) )
                {
                    return Proxy.newProxyInstance( method.getDeclaringClass().getClassLoader(),
                                                   array( (Class) namedAssociationType, PropertyReference.class ),
                                                   new TemplateHandler<T>( null, null, null, namedAssociation ) );
                }
            }

            return null;
        }
    }

    @SafeVarargs
    private static <T> T[] array( T... array )
    {
        return array;
    }

    private QueryExpressions()
    {
    }
}
