/* The contents of this file is dual-licensed under 2 
 * alternative Open Source/Free licenses: LGPL 2.1 or later and 
 * Apache License 2.0. (starting with JNA version 4.0.0).
 * 
 * You can freely decide which license you want to apply to 
 * the project.
 * 
 * You may obtain a copy of the LGPL License at:
 * 
 * http://www.gnu.org/licenses/licenses.html
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 * 
 * You may obtain a copy of the Apache License at:
 * 
 * http://www.apache.org/licenses/
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

/** Derive from this interface for all native library definitions.
 *
 * Define an instance of your library like this:
 * <pre><code>
 * MyNativeLibrary INSTANCE = (MyNativeLibrary)
 *     Native.loadLibrary("mylib", MyNativeLibrary.class);
 * </code></pre>
 * <p>
 * By convention, method names are identical to the native names, although you
 * can map java names to different native names by providing a
 * {@link FunctionMapper} as a value for key {@link #OPTION_FUNCTION_MAPPER}
 * in the options map passed to the
 * {@link Native#loadLibrary(String, Class, Map)} call.
 * <p>
 * Although the names for structures and structure fields may be chosen
 * arbitrarily, they should correspond as closely as possible to the native
 * definitions.  The same is true for parameter names.
 * <p>
 * This interface supports multiple, concurrent invocations of any library
 * methods on the Java side.  Check your library documentation for its
 * multi-threading requirements on the native side.  If a library is not safe
 * for simultaneous multi-threaded access, consider using
 * {@link Native#synchronizedLibrary} to prevent simultaneous multi-threaded
 * access to the native code.
 * <p>
 * <b>Optional fields</b><br>
 * Interface options will be automatically propagated to structures defined
 * within the library provided a call to
 * {@link Native#loadLibrary(String,Class,Map)} is made prior to instantiating
 * any of those structures.  One common way of ensuring this is to declare
 * an <b>INSTANCE</b> field in the interface which holds the
 * <code>loadLibrary</code> result.
 * <p>
 * <b>OPTIONS</b> (an instance of {@link Map}),
 * <b>TYPE_MAPPER</b> (an instance of {@link TypeMapper}),
 * <b>STRUCTURE_ALIGNMENT</b> (one of the alignment types defined in
 * {@link Structure}), and <b>STRING_ENCODING</b> (a {@link String}) may also
 * be defined.  If no instance of the interface has been instantiated, these
 * fields will be used to determine customization settings for structures and
 * methods defined within the interface.
 * <p>
 *
 * @author  Todd Fast, todd.fast@sun.com
 * @author  Timothy Wall, twalljava@dev.java.net
 */
public interface Library {
    /** Option key for a {@link TypeMapper} for the library. */
    String OPTION_TYPE_MAPPER = "type-mapper";
    /** Option key for a {@link FunctionMapper} for the library. */
    String OPTION_FUNCTION_MAPPER = "function-mapper";
    /** Option key for an {@link InvocationMapper} for the library. */
    String OPTION_INVOCATION_MAPPER = "invocation-mapper";
    /** Option key for structure alignment type ({@link Integer}), which should
     * be one of the predefined alignment types in {@link Structure}.
     */
    String OPTION_STRUCTURE_ALIGNMENT = "structure-alignment";
    /** <p>Option key for per-library String encoding.  This affects conversions
     * between Java unicode and native (<code>const char*</code>) strings (as
     * arguments or Structure fields).
     * </p>
     * Defaults to {@link Native#getDefaultStringEncoding()}.
     */
    String OPTION_STRING_ENCODING = "string-encoding";
    /** Option key for a boolean flag to allow any Java class instance as a
        parameter.  If no type mapper is found, the object is passed as a
        pointer.
        <em>NOTE:</em> This is for use with raw JNI interactions via the
        JNIEnv data structure.
    */
    String OPTION_ALLOW_OBJECTS = "allow-objects";
    /** Calling convention for the entire library. */
    String OPTION_CALLING_CONVENTION = "calling-convention";
    /** Flags to use when opening the native library (see {@link Native#open(String,int)}) */
    String OPTION_OPEN_FLAGS = "open-flags";
    /** <p>Class loader to use when searching for native libraries on the
     * resource path (classpath).  If not provided the current thread's
     * context class loader is used.</p>
     * If extracted from the resource path (i.e. bundled in a jar file), the
     * loaded library's lifespan will mirror that of the class loader, which
     * means you can use the same library in isolated contexts without
     * conflict.
     */
    String OPTION_CLASSLOADER = "classloader";

    static class Handler implements InvocationHandler {

        static final Method OBJECT_TOSTRING;
        static final Method OBJECT_HASHCODE;
        static final Method OBJECT_EQUALS;

        static {
            try {
                OBJECT_TOSTRING = Object.class.getMethod("toString");
                OBJECT_HASHCODE= Object.class.getMethod("hashCode");
                OBJECT_EQUALS = Object.class.getMethod("equals", Object.class);
            } catch (Exception e) {
                throw new Error("Error retrieving Object.toString() method");
            }
        }

        /**
         * FunctionInfo has to be immutable to to make the object visible
         * to other threads fully initialized. This is a prerequisite for
         * using the class in the double checked locking scenario of {@link Handler#invoke(Object, Method, Object[])}
         */
        private static final class FunctionInfo {
            final InvocationHandler handler;
            final Function function;
            final boolean isVarArgs;
            final Map<String, ?> options;
            final Class<?>[] parameterTypes;

            FunctionInfo(InvocationHandler handler, Function function, Class<?>[] parameterTypes, boolean isVarArgs, Map<String, ?> options) {
                this.handler = handler;
                this.function = function;
                this.isVarArgs = isVarArgs;
                this.options = options;
                this.parameterTypes = parameterTypes;
            }
        }

        private final NativeLibrary nativeLibrary;
        private final Class<?> interfaceClass;
        // Library invocation options
        private final Map<String, Object> options;
        private final InvocationMapper invocationMapper;
        private final Map<Method, FunctionInfo> functions = new WeakHashMap<Method, FunctionInfo>();
        public Handler(String libname, Class<?> interfaceClass, Map<String, ?> options) {

            if (libname != null && "".equals(libname.trim())) {
                throw new IllegalArgumentException("Invalid library name \"" + libname + "\"");
            }

            if (!interfaceClass.isInterface()) {
                throw new IllegalArgumentException(libname + " does not implement an interface: " + interfaceClass.getName());
            }

            this.interfaceClass = interfaceClass;
            this.options = new HashMap<String, Object>(options);
            int callingConvention = AltCallingConvention.class.isAssignableFrom(interfaceClass)
                                  ? Function.ALT_CONVENTION
                                  : Function.C_CONVENTION;
            if (this.options.get(OPTION_CALLING_CONVENTION) == null) {
                this.options.put(OPTION_CALLING_CONVENTION, Integer.valueOf(callingConvention));
            }
            if (this.options.get(OPTION_CLASSLOADER) == null) {
                this.options.put(OPTION_CLASSLOADER, interfaceClass.getClassLoader());
            }
            this.nativeLibrary = NativeLibrary.getInstance(libname, this.options);
            invocationMapper = (InvocationMapper)this.options.get(OPTION_INVOCATION_MAPPER);
        }

        public NativeLibrary getNativeLibrary() {
            return nativeLibrary;
        }

        public String getLibraryName() {
            return nativeLibrary.getName();
        }

        public Class<?> getInterfaceClass() {
            return interfaceClass;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] inArgs)
            throws Throwable {

            // Intercept Object methods
            if (OBJECT_TOSTRING.equals(method)) {
                return "Proxy interface to " + nativeLibrary;
            } else if (OBJECT_HASHCODE.equals(method)) {
                return Integer.valueOf(hashCode());
            } else if (OBJECT_EQUALS.equals(method)) {
                Object o = inArgs[0];
                if (o != null && Proxy.isProxyClass(o.getClass())) {
                    return Function.valueOf(Proxy.getInvocationHandler(o) == this);
                }
                return Boolean.FALSE;
            }

            // Using the double-checked locking pattern to speed up function calls
            FunctionInfo f = functions.get(method);
            if(f == null) {
                synchronized(functions) {
                    f = functions.get(method);
                    if (f == null) {
                        boolean isVarArgs = Function.isVarArgs(method);
                        InvocationHandler handler = null;
                        if (invocationMapper != null) {
                            handler = invocationMapper.getInvocationHandler(nativeLibrary, method);
                        }
                        Function function = null;
                        Class<?>[] parameterTypes = null;
                        Map<String, Object> options = null;
                        if (handler == null) {
                            // Find the function to invoke
                            function = nativeLibrary.getFunction(method.getName(), method);
                            parameterTypes = method.getParameterTypes();
                            options = new HashMap<String, Object>(this.options);
                            options.put(Function.OPTION_INVOKING_METHOD, method);
                        }
                        f = new FunctionInfo(handler, function, parameterTypes, isVarArgs, options);
                        functions.put(method, f);
                    }
                }
            }
            if (f.isVarArgs) {
                inArgs = Function.concatenateVarArgs(inArgs);
            }
            if (f.handler != null) {
                return f.handler.invoke(proxy, method, inArgs);
            }
            return f.function.invoke(method, f.parameterTypes, method.getReturnType(), inArgs, f.options);
        }
    }
}
