/*
 * Copyright 2008 Alin Dreghiciu.
 *
 * 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.lang.groovy;

import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyShell;
import groovy.lang.MissingPropertyException;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import org.qi4j.api.common.AppliesTo;
import org.qi4j.api.common.AppliesToFilter;
import org.qi4j.api.composite.Composite;
import org.qi4j.api.injection.scope.This;
import org.qi4j.io.Inputs;
import org.qi4j.io.Outputs;

/**
 * Generic mixin that implements interfaces by delegating to Groovy functions
 * using Groovy. Each method in an interface is declared by a Groovy method
 * in a file located in classpath with the name "&lt;interface&gt;.groovy",
 * where the interface name includes the package, and has "." replaced with "/".
 * <p>
 * Example:
 * </p>
 * <pre><code>
 * org/qi4j/samples/hello/domain/HelloWorldSpeaker.groovy
 * org/qi4j/samples/hello/domain/HelloWorldSpeaker.sayAgain.groovy
 * </code></pre>
 *
 */
@AppliesTo( GroovyMixin.AppliesTo.class )
public class GroovyMixin
    implements InvocationHandler
{

    private @This
    Composite me;
    private final Map<Class, GroovyObject> groovyObjects;

    public static class AppliesTo
        implements AppliesToFilter
    {

        @Override
        public boolean appliesTo( Method method, Class compositeType, Class mixin, Class modelClass )
        {
            return getFunctionResource( method ) != null;
        }

    }

    public GroovyMixin()
    {

        groovyObjects = new HashMap<Class, GroovyObject>();
    }

    @Override
    public Object invoke( Object proxy, Method method, Object[] args )
        throws Throwable
    {
        final FunctionResource groovySource = getFunctionResource( method );
        if( groovySource != null )
        {
            if( groovySource.script )
            {
                return invokeAsObject( method, args, groovySource.url );
            }
            return invokeAsScript( method, args, groovySource.url );
        }
        throw new RuntimeException( "Internal error: Mixin invoked even if it does not apply" );
    }

    private Object invokeAsObject( Method method, Object[] args, URL groovySource )
        throws Throwable
    {
        try
        {
            Class declaringClass = method.getDeclaringClass();
            GroovyObject groovyObject = groovyObjects.get( declaringClass );
            if( groovyObject == null )
            {
                InputStream is = null;
                final Class groovyClass;
                try
                {
                    is = groovySource.openStream();
                    StringBuilder sourceBuilder = new StringBuilder();
                    Inputs.text( groovySource ).transferTo( Outputs.text( sourceBuilder ) );
                    GroovyClassLoader groovyClassLoader = new GroovyClassLoader( declaringClass.getClassLoader() );
                    groovyClass = groovyClassLoader.parseClass( sourceBuilder.toString() );
                }
                finally
                {
                    if( is != null )
                    {
                        is.close();
                    }
                }
                groovyObject = (GroovyObject) groovyClass.newInstance();
                if( hasProperty( groovyObject, "This" ) )
                {
                    groovyObject.setProperty( "This", me );
                }
                groovyObjects.put( declaringClass, groovyObject );
            }
            return groovyObject.invokeMethod( method.getName(), args );
        }
        catch( Exception e )
        {
            e.printStackTrace();
            throw e;
        }
    }

    private boolean hasProperty( GroovyObject groovyObject, String propertyName )
    {
        try
        {
            groovyObject.getProperty( propertyName );
            return true;
        }
        catch( MissingPropertyException ex )
        {
            return false;
        }
    }

    private Object invokeAsScript( Method method, Object[] args, URL groovySource )
        throws Throwable
    {
        try
        {
            Binding binding = new Binding();
            binding.setVariable( "This", me );
            binding.setVariable( "args", args );
            GroovyShell shell = new GroovyShell( binding );
            InputStream is = null;
            try
            {
                is = groovySource.openStream();
                return shell.evaluate( new InputStreamReader( is ) );
            }
            finally
            {
                if( is != null )
                {
                    is.close();
                }
            }
        }
        catch( Exception e )
        {
            e.printStackTrace();
            throw e;
        }
    }

    private static FunctionResource getFunctionResource( final Method method )
    {
        boolean script = false;
        final String scriptPath = method.getDeclaringClass().getName().replace( '.', File.separatorChar );
        String scriptFile = scriptPath + "." + method.getName() + ".groovy";
        URL scriptUrl = method.getDeclaringClass().getClassLoader().getResource( scriptFile );
        if( scriptUrl == null )
        {
            script = true;
            scriptFile = scriptPath + ".groovy";
            scriptUrl = method.getDeclaringClass().getClassLoader().getResource( scriptFile );
        }
        if( scriptUrl != null )
        {
            return new FunctionResource( script, scriptUrl );
        }
        return null;
    }

    private static class FunctionResource
    {

        URL url;
        boolean script;

        private FunctionResource( final boolean script, final URL url )
        {
            this.script = script;
            this.url = url;
        }

    }

}