/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *
 */

package org.apache.polygene.library.rest.server.restlet;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.polygene.api.composite.TransientComposite;
import org.apache.polygene.api.constraint.Constraint;
import org.apache.polygene.api.constraint.ConstraintDeclaration;
import org.apache.polygene.api.constraint.Constraints;
import org.apache.polygene.api.injection.scope.Structure;
import org.apache.polygene.api.object.NoSuchObjectTypeException;
import org.apache.polygene.api.structure.Module;
import org.apache.polygene.library.rest.server.api.ObjectSelection;
import org.apache.polygene.library.rest.server.api.constraint.InteractionConstraint;
import org.apache.polygene.library.rest.server.api.constraint.InteractionConstraintDeclaration;
import org.apache.polygene.library.rest.server.api.constraint.InteractionValidation;
import org.apache.polygene.library.rest.server.api.constraint.RequiresValid;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * JAVADOC
 */
public class InteractionConstraintsService
    implements InteractionConstraints
{
    @Structure
    Module module;

    Logger logger = LoggerFactory.getLogger( InteractionConstraintsService.class );

    private Map<Method, InteractionConstraintsBinding> methodsConstraints = new ConcurrentHashMap<>();
    private Map<Class, InteractionConstraintsBinding> classConstraints = new ConcurrentHashMap<>();

    @Override
    public boolean isValid( Method method, ObjectSelection objectSelection, Module module )
    {
        return getConstraints( method, module ).isValid( objectSelection );
    }

    @Override
    public boolean isValid( Class resourceClass, ObjectSelection objectSelection, Module module )
    {
        return getConstraints( resourceClass, module ).isValid( objectSelection );
    }

    private InteractionConstraintsBinding getConstraints( Method method, Module module )
    {
        InteractionConstraintsBinding constraintBindings = methodsConstraints.get( method );
        if( constraintBindings == null )
        {
            constraintBindings = findConstraints( method, module );
            methodsConstraints.put( method, constraintBindings );
        }
        return constraintBindings;
    }

    private InteractionConstraintsBinding getConstraints( Class aClass, Module module )
    {
        InteractionConstraintsBinding constraintBindings = classConstraints.get( aClass );
        if( constraintBindings == null )
        {
            constraintBindings = findConstraints( aClass, module );
            classConstraints.put( aClass, constraintBindings );
        }
        return constraintBindings;
    }

    private InteractionConstraintsBinding findConstraints( Method method, Module module )
    {
        List<Binding> methodConstraintBindings = new ArrayList<Binding>();

        for( Annotation annotation : method.getAnnotations() )
        {
            if( annotation.annotationType().equals( RequiresValid.class ) )
            {
                RequiresValid requiresValid = (RequiresValid) annotation;

                Class contextClass = method.getDeclaringClass();
                if( InteractionValidation.class.isAssignableFrom( contextClass ) )
                {
                    InteractionValidation validation = null;
                    if( TransientComposite.class.isAssignableFrom( contextClass ) )
                    {
                        validation = (InteractionValidation) module.newTransient( contextClass );
                    }
                    else
                    {
                        validation = (InteractionValidation) module.newObject( contextClass );
                    }
                    methodConstraintBindings.add( new RequiresValidBinding( requiresValid, validation ) );
                }
            }
            else if( annotation.annotationType().getAnnotation( ConstraintDeclaration.class ) != null )
            {
                Constraints constraints = annotation.annotationType().getAnnotation( Constraints.class );

                for( Class<? extends Constraint<?, ?>> aClass : constraints.value() )
                {
                    try
                    {
                        Constraint<Annotation, Object> constraint = (Constraint<Annotation, Object>) aClass.newInstance();
                        Class roleClass = (Class) ( (ParameterizedType) aClass.getGenericInterfaces()[ 0 ] ).getActualTypeArguments()[ 1 ];
                        ConstraintBinding constraintBinding = new ConstraintBinding( constraint, annotation, roleClass );
                        methodConstraintBindings.add( constraintBinding );
                    }
                    catch( InstantiationException e )
                    {
                        e.printStackTrace();
                    }
                    catch( IllegalAccessException e )
                    {
                        e.printStackTrace();
                    }
                }
            }
            else if( annotation.annotationType().getAnnotation( InteractionConstraintDeclaration.class ) != null )
            {
                Class<? extends InteractionConstraint> constraintClass = annotation.annotationType()
                    .getAnnotation( InteractionConstraintDeclaration.class )
                    .value();
                InteractionConstraint<Annotation> constraint = null;
                try
                {
                    try
                    {
                        constraint = module.newObject( constraintClass );
                    }
                    catch( NoSuchObjectTypeException e )
                    {
                        constraint = constraintClass.newInstance();
                    }
                }
                catch( Exception e )
                {
                    continue; // Skip this constraint
                }
                InteractionConstraintBinding constraintBinding = new InteractionConstraintBinding( constraint, annotation );
                methodConstraintBindings.add( constraintBinding );
            }
        }

        if( methodConstraintBindings.isEmpty() )
        {
            methodConstraintBindings = null;
        }

        return new InteractionConstraintsBinding( methodConstraintBindings );
    }

    private InteractionConstraintsBinding findConstraints( Class aClass, Module module )
    {
        List<Binding> classConstraintBindings = new ArrayList<Binding>();

        for( Annotation annotation : aClass.getAnnotations() )
        {
            if( annotation.annotationType().getAnnotation( ConstraintDeclaration.class ) != null )
            {
                Constraints constraints = annotation.annotationType().getAnnotation( Constraints.class );

                for( Class<? extends Constraint<?, ?>> constraintClass : constraints.value() )
                {
                    try
                    {
                        Constraint<Annotation, Object> constraint = (Constraint<Annotation, Object>) constraintClass.newInstance();
                        Class roleClass = (Class) ( (ParameterizedType) constraint.getClass()
                            .getGenericInterfaces()[ 0 ] ).getActualTypeArguments()[ 1 ];
                        ConstraintBinding constraintBinding = new ConstraintBinding( constraint, annotation, roleClass );
                        classConstraintBindings.add( constraintBinding );
                    }
                    catch( InstantiationException e )
                    {
                        e.printStackTrace();
                    }
                    catch( IllegalAccessException e )
                    {
                        e.printStackTrace();
                    }
                }
            }
            else if( annotation.annotationType().getAnnotation( InteractionConstraintDeclaration.class ) != null )
            {
                Class<? extends InteractionConstraint> constraintClass = annotation.annotationType()
                    .getAnnotation( InteractionConstraintDeclaration.class )
                    .value();
                InteractionConstraint<Annotation> constraint = null;
                try
                {
                    try
                    {
                        constraint = module.newObject( constraintClass );
                    }
                    catch( NoSuchObjectTypeException e )
                    {
                        constraint = constraintClass.newInstance();
                    }
                }
                catch( Exception e )
                {
                    continue; // Skip this constraint
                }
                InteractionConstraintBinding constraintBinding = new InteractionConstraintBinding( constraint, annotation );
                classConstraintBindings.add( constraintBinding );
            }
        }

        if( classConstraintBindings.isEmpty() )
        {
            classConstraintBindings = null;
        }

        return new InteractionConstraintsBinding( classConstraintBindings );
    }

    interface Binding
    {
        boolean isValid( ObjectSelection objectSelection );
    }

    public static class InteractionConstraintsBinding
    {
        List<Binding> bindings;

        public InteractionConstraintsBinding( List<Binding> bindings )
        {
            this.bindings = bindings;
        }

        public boolean isValid( ObjectSelection objectSelection )
        {
            if( bindings != null )
            {
                for( Binding constraintBinding : bindings )
                {
                    if( !constraintBinding.isValid( objectSelection ) )
                    {
                        return false;
                    }
                }
            }

            return true;
        }
    }

    public class RequiresValidBinding
        implements Binding
    {
        RequiresValid annotation;
        private final InteractionValidation validation;

        public RequiresValidBinding( RequiresValid annotation, InteractionValidation validation )
        {
            this.validation = validation;
            this.annotation = annotation;
        }

        @Override
        public boolean isValid( ObjectSelection objectSelection )
        {
            try
            {
                return validation.isValid( annotation.value() );
            }
            catch( IllegalArgumentException e )
            {
                return false;
            }
            catch( Throwable e )
            {
                logger.warn( "Could not check validation constraint for '" + annotation.value() + "'", e );
                return false;
            }
        }
    }

    public class ConstraintBinding
        implements Binding
    {
        Constraint<Annotation, Object> constraint;
        Annotation annotation;
        Class roleClass;

        public ConstraintBinding( Constraint<Annotation, Object> constraint, Annotation annotation, Class roleClass )
        {
            this.constraint = constraint;
            this.annotation = annotation;
            this.roleClass = roleClass;
        }

        @Override
        public boolean isValid( ObjectSelection objectSelection )
        {
            try
            {
                Object checkedObject = roleClass.equals( ObjectSelection.class ) ? objectSelection : objectSelection.get( roleClass );

                return constraint.isValid( annotation, checkedObject );
            }
            catch( IllegalArgumentException e )
            {
                return false;
            }
            catch( Throwable e )
            {
                logger.warn( "Could not check constraint " + constraint.getClass().getName(), e );
                return false;
            }
        }
    }

    public class InteractionConstraintBinding
        implements Binding
    {
        InteractionConstraint<Annotation> constraint;
        Annotation annotation;

        public InteractionConstraintBinding( InteractionConstraint<Annotation> constraint, Annotation annotation )
        {
            this.constraint = constraint;
            this.annotation = annotation;
        }

        @Override
        public boolean isValid( ObjectSelection objectSelection )
        {
            try
            {
                return constraint.isValid( annotation, objectSelection );
            }
            catch( Throwable e )
            {
                logger.warn( "Could not check constraint " + constraint.getClass().getName(), e );
                return false;
            }
        }
    }
}