/*  Copyright 2008 Edward Yakop.
 *  Copyright 2009 Niclas Hedhman.
 *
 * 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
 * implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.qi4j.api.unitofwork.concern;

import java.lang.reflect.Method;
import org.qi4j.api.common.AppliesTo;
import org.qi4j.api.concern.GenericConcern;
import org.qi4j.api.injection.scope.Invocation;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.injection.scope.Uses;
import org.qi4j.api.structure.Module;
import org.qi4j.api.unitofwork.ConcurrentEntityModificationException;
import org.qi4j.api.unitofwork.UnitOfWork;
import org.qi4j.api.usecase.Usecase;
import org.qi4j.api.usecase.UsecaseBuilder;

/**
 * {@code UnitOfWorkConcern} manages the unit of work complete, discard and retry policy.
 *
 * @see UnitOfWorkPropagation
 * @see UnitOfWorkDiscardOn
 */
@AppliesTo( UnitOfWorkPropagation.class )
public class UnitOfWorkConcern
    extends GenericConcern
{
    private static final Class<?>[] DEFAULT_DISCARD_CLASSES = new Class[]{ Throwable.class };

    @Structure
    Module module;

    @Invocation
    UnitOfWorkPropagation propagation;

    /**
     * Handles method with {@code UnitOfWorkPropagation} annotation.
     *
     * @param proxy  The object.
     * @param method The invoked method.
     * @param args   The method arguments.
     *
     * @return The returned value of method invocation.
     *
     * @throws Throwable Thrown if the method invocation throw exception.
     */
    @Override
    public Object invoke( Object proxy, Method method, Object[] args )
        throws Throwable
    {
        UnitOfWorkPropagation.Propagation propagationPolicy = propagation.value();
        if( propagationPolicy == UnitOfWorkPropagation.Propagation.REQUIRED )
        {
            if( module.isUnitOfWorkActive() )
            {
                return next.invoke( proxy, method, args );
            }
            else
            {
                Usecase usecase = usecase();
                return invokeWithCommit( proxy, method, args, module.newUnitOfWork( usecase ) );
            }
        }
        else if( propagationPolicy == UnitOfWorkPropagation.Propagation.MANDATORY )
        {
            if( !module.isUnitOfWorkActive() )
            {
                throw new IllegalStateException( "UnitOfWork was required but there is no available unit of work." );
            }
        }
        else if( propagationPolicy == UnitOfWorkPropagation.Propagation.REQUIRES_NEW )
        {
            Usecase usecase = usecase();
            return invokeWithCommit( proxy, method, args, module.newUnitOfWork( usecase ) );
        }
        return next.invoke( proxy, method, args );
    }

    private Usecase usecase()
    {
        String usecaseName = propagation.usecase();
        Usecase usecase;
        if( usecaseName == null )
        {
            usecase = Usecase.DEFAULT;
        }
        else
        {
            usecase = UsecaseBuilder.newUsecase( usecaseName );
        }
        return usecase;
    }

    protected Object invokeWithCommit( Object proxy, Method method, Object[] args, UnitOfWork currentUnitOfWork )
        throws Throwable
    {
        try
        {
            UnitOfWorkRetry retryAnnot = method.getAnnotation( UnitOfWorkRetry.class );
            int maxTries = 0;
            long delayFactor = 0;
            long initialDelay = 0;
            if( retryAnnot != null )
            {
                maxTries = retryAnnot.retries();
                initialDelay = retryAnnot.initialDelay();
                delayFactor = retryAnnot.delayFactory();
            }
            int retry = 0;
            while( true )
            {
                Object result = next.invoke( proxy, method, args );
                try
                {
                    currentUnitOfWork.complete();
                    return result;
                }
                catch( ConcurrentEntityModificationException e )
                {
                    if( retry >= maxTries )
                    {
                        throw e;
                    }
                    module.currentUnitOfWork().discard();
                    Thread.sleep( initialDelay + retry * delayFactor );
                    retry++;
                    currentUnitOfWork = module.newUnitOfWork( usecase() );
                }
            }
        }
        catch( Throwable throwable )
        {
            // Discard only if this concern create a unit of work
            discardIfRequired( method, currentUnitOfWork, throwable );
            throw throwable;
        }
    }

    /**
     * Discard unit of work if the discard policy match.
     *
     * @param aMethod     The invoked method. This argument must not be {@code null}.
     * @param aUnitOfWork The current unit of work. This argument must not be {@code null}.
     * @param aThrowable  The exception thrown. This argument must not be {@code null}.
     */
    protected void discardIfRequired( Method aMethod, UnitOfWork aUnitOfWork, Throwable aThrowable )
    {
        UnitOfWorkDiscardOn discardPolicy = aMethod.getAnnotation( UnitOfWorkDiscardOn.class );
        Class<?>[] discardClasses;
        if( discardPolicy != null )
        {
            discardClasses = discardPolicy.value();
        }
        else
        {
            discardClasses = DEFAULT_DISCARD_CLASSES;
        }

        Class<? extends Throwable> aThrowableClass = aThrowable.getClass();
        for( Class<?> discardClass : discardClasses )
        {
            if( discardClass.isAssignableFrom( aThrowableClass ) )
            {
                aUnitOfWork.discard();
            }
        }
    }
}