/*
 *  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 groovy.util;

import groovy.lang.Binding;
import groovy.lang.GroovyObject;
import groovy.lang.MetaClass;
import groovy.lang.MissingMethodException;
import groovy.lang.MissingPropertyException;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.InvokerHelper;

/**
 * {@link Script} that performs method invocations and property access like {@link groovy.lang.Closure} does.
 *
 * <p>
 * {@link DelegatingScript} is a convenient basis for loading a custom-defined DSL as a {@link Script}, then execute it.
 * The following sample code illustrates how to do it:
 *
 * <pre>
 * class MyDSL {
 *     public void foo(int x, int y, Closure z) { ... }
 *     public void setBar(String a) { ... }
 * }
 *
 * CompilerConfiguration cc = new CompilerConfiguration();
 * cc.setScriptBaseClass(DelegatingScript.class.getName());
 * GroovyShell sh = new GroovyShell(cl,new Binding(),cc);
 * DelegatingScript script = (DelegatingScript)sh.parse(new File("my.dsl"))
 * script.setDelegate(new MyDSL());
 * script.run();
 * </pre>
 *
 * <p>
 * <tt>my.dsl</tt> can look like this:
 *
 * <pre>
 * foo(1,2) {
 *     ....
 * }
 * bar = ...;
 * </pre>
 *
 * <p>
 * {@link DelegatingScript} does this by delegating property access and method invocation to the <tt>delegate</tt> object.
 *
 * <p>
 * More formally speaking, given the following script:
 *
 * <pre>
 * a = 1;
 * b(2);
 * </pre>
 *
 * <p>
 * Using {@link DelegatingScript} as the base class, the code will run as:
 *
 * <pre>
 * delegate.a = 1;
 * delegate.b(2);
 * </pre>
 *
 * ... whereas in plain {@link Script}, this will be run as:
 *
 * <pre>
 * binding.setProperty("a",1);
 * ((Closure)binding.getProperty("b")).call(2);
 * </pre>
 */
public abstract class DelegatingScript extends Script {
    private Object delegate;
    private MetaClass metaClass;

    /**
     * Creates a delegating script with an empty binding.
     */
    protected DelegatingScript() {
        super();
    }

    /**
     * Creates a delegating script using the supplied binding.
     *
     * @param binding the binding to use for the script
     */
    protected DelegatingScript(Binding binding) {
        super(binding);
    }

    /**
     * Sets the delegation target.
     */
    public void setDelegate(Object delegate) {
        this.delegate = delegate;
        this.metaClass = InvokerHelper.getMetaClass(delegate.getClass());
    }

    /**
     * Delegates method invocation before falling back to normal script dispatch.
     *
     * @param name the method name
     * @param args the invocation arguments
     * @return the invocation result
     */
    @Override
    public Object invokeMethod(String name, Object args) {
        try {
            if (delegate instanceof GroovyObject) {
                return ((GroovyObject) delegate).invokeMethod(name, args);
            }
            return metaClass.invokeMethod(delegate, name, args);
        } catch (MissingMethodException mme) {
            return super.invokeMethod(name, args);
        }
    }

    /**
     * Resolves properties against the delegate before the script binding.
     *
     * @param property the property name
     * @return the property value
     */
    @Override
    public Object getProperty(String property) {
        try {
            return metaClass.getProperty(delegate,property);
        } catch (MissingPropertyException e) {
            return super.getProperty(property);
        }
    }

    /**
     * Sets a property on the delegate before falling back to the script binding.
     *
     * @param property the property name
     * @param newValue the new property value
     */
    @Override
    public void setProperty(String property, Object newValue) {
        try {
            metaClass.setProperty(delegate,property,newValue);
        } catch (MissingPropertyException e) {
            super.setProperty(property,newValue);
        }
    }

    /**
     * Returns the current delegation target.
     *
     * @return the delegate object
     */
    public Object getDelegate() {
        return delegate;
    }
}