#parse("main/Header.vm")
package com.nativelibs4java.opencl;

import static com.nativelibs4java.opencl.CLException.error;
import com.nativelibs4java.opencl.CLPlatform.DeviceFeature;

import java.io.File;
import java.io.IOException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.logging.*;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import com.nativelibs4java.opencl.library.OpenCLLibrary;
import com.nativelibs4java.opencl.library.IOpenCLLibrary;
import com.nativelibs4java.opencl.library.IOpenCLLibrary.cl_platform_id;
import org.bridj.*;
import org.bridj.ann.Ptr;

import org.bridj.util.ProcessUtils;
import org.bridj.util.StringUtils;
import static org.bridj.Pointer.*;

/**
 * Entry point class for the OpenCL4Java Object-oriented wrappers around the OpenCL API.<br>
 * @author Olivier Chafik
 */
public class JavaCL {

	static final boolean debug = "true".equals(System.getProperty("javacl.debug")) || "1".equals(System.getenv("JAVACL_DEBUG"));
    static final boolean verbose = debug || "true".equals(System.getProperty("javacl.verbose")) || "1".equals(System.getenv("JAVACL_VERBOSE"));
    static final boolean logCalls = "true".equals(System.getProperty("javacl.logCalls")) || "1".equals(System.getenv("JAVACL_LOG_CALLS"));
    static final int minLogLevel = Level.WARNING.intValue();
    
    static final String JAVACL_DEBUG_COMPILER_FLAGS_PROP = "JAVACL_DEBUG_COMPILER_FLAGS";
    static List<String> DEBUG_COMPILER_FLAGS;
    
	static boolean shouldLog(Level level) {
        return verbose || level.intValue() >= minLogLevel;
    }
	static boolean log(Level level, String message, Throwable ex) {
        if (!shouldLog(level))
            return true;
		Logger.getLogger(JavaCL.class.getSimpleName()).log(level, message, ex);
        return true;
	}
	
	static boolean log(Level level, String message) {
		log(level, message, null);
		return true;
	}
	
	static void check(boolean test, String message, Object... formatArgs) {
		if (!test) throw new RuntimeException(String.format(message, formatArgs));
	}

	private static int getPlatformIDs(int count, Pointer<cl_platform_id> out, Pointer<Integer> pCount) {
        assert (count == 0) ^ (pCount == null);  
        assert (count == 0) == (out == null);
        if (hasIcd == null || hasIcd.booleanValue()) {
    		try {
    			int ret = CL.clIcdGetPlatformIDsKHR(count, getPeer(out), getPeer(pCount));
                if (hasIcd == null)
                    hasIcd = true;
                return ret;
    		} catch (Throwable th) {
                hasIcd = false;
    			return CL.clGetPlatformIDs(count, getPeer(out), getPeer(pCount));
    		}
        } else {
            return CL.clGetPlatformIDs(count, getPeer(out), getPeer(pCount));
        }
	}
	
	@org.bridj.ann.Library("OpenCLProbe") 
	@org.bridj.ann.Convention(org.bridj.ann.Convention.Style.StdCall)
	public static class OpenCLProbeLibrary {
		@org.bridj.ann.Optional
		public native static synchronized int clGetPlatformIDs(int cl_uint1, Pointer<cl_platform_id > cl_platform_idPtr1, Pointer<Integer > cl_uintPtr1);
		@org.bridj.ann.Optional
		public native static synchronized int clIcdGetPlatformIDsKHR(int cl_uint1, Pointer<cl_platform_id > cl_platform_idPtr1, Pointer<Integer > cl_uintPtr1);
		@org.bridj.ann.Optional
		public native static int clGetPlatformInfo(@Ptr long cl_platform_id1, int cl_platform_info1, @Ptr long size_t1, @Ptr long voidPtr1, @Ptr long size_tPtr1);
	
        #declareInfosGetter("infos", "clGetPlatformInfo")
        
        private static int getPlatformIDs(int count, Pointer<cl_platform_id> out, Pointer<Integer> pCount) {
            try {
                return clIcdGetPlatformIDsKHR(count, out, pCount);
            } catch (Throwable th) {
                return clGetPlatformIDs(count, out, pCount);
            }
        }
        private static Pointer<cl_platform_id> getPlatformIDs() {
            Pointer<Integer> pCount = allocateInt();
            error(getPlatformIDs(0, null, pCount));

            int nPlats = pCount.getInt();
            if (nPlats == 0)
                return null;

            Pointer<cl_platform_id> ids = allocateTypedPointers(cl_platform_id.class, nPlats);
            error(getPlatformIDs(nPlats, ids, null));
            return ids;
        }
        public static boolean hasOpenCL1_0() {
            Pointer<cl_platform_id> ids = getPlatformIDs();
            if (ids == null)
                return false;
            
            for (cl_platform_id id : ids)
                if (isOpenCL1_0(id))
                    return true;
            return false;
        }
        public static boolean isOpenCL1_0(cl_platform_id platform) {
            String version = infos.getString(getPeer(platform), OpenCLLibrary.CL_PLATFORM_VERSION);
            return version.matches("OpenCL 1\\.0.*");
        }
		public static boolean isValid() {
			try {
                Pointer<cl_platform_id> ids = getPlatformIDs();
                return ids != null;
			} catch (Throwable th) {
                return false;
			}
		}
	}	

    static final IOpenCLLibrary CL;
    static Boolean hasIcd;
	static {
		if (Platform.isLinux()) {
			String amdAppBase = "/opt/AMDAPP/lib";
			BridJ.addLibraryPath(amdAppBase + "/" + (Platform.is64Bits() ? "x86_64" : "x86"));
			BridJ.addLibraryPath(amdAppBase);
		}
        	boolean needsAdditionalSynchronization = false;
		{
			OpenCLProbeLibrary probe = null;
			try {
				try {
					BridJ.setNativeLibraryActualName("OpenCLProbe", "OpenCL");
					BridJ.register();
				} catch (Throwable th) {}
				
				probe = new OpenCLProbeLibrary();
				
				if (!probe.isValid()) {
					BridJ.unregister(OpenCLProbeLibrary.class);
					//BridJ.setNativeLibraryActualName("OpenCLProbe", "OpenCL");
                   			String alt;
					if (Platform.is64Bits() && (BridJ.getNativeLibraryFile(alt = "atiocl64") != null || BridJ.getNativeLibraryFile(alt = "amdocl64") != null) ||
						BridJ.getNativeLibraryFile(alt = "atiocl32") != null ||
						BridJ.getNativeLibraryFile(alt = "atiocl") != null ||
						BridJ.getNativeLibraryFile(alt = "amdocl32") != null ||
						BridJ.getNativeLibraryFile(alt = "amdocl") != null) 
					{
						log(Level.INFO, "Hacking around ATI's weird driver bugs (using atiocl/amdocl library instead of OpenCL)", null); 
						BridJ.setNativeLibraryActualName("OpenCL", alt);
					}
                    BridJ.register(OpenCLProbeLibrary.class);
				}
                
                
                if (probe.hasOpenCL1_0()) {
                    needsAdditionalSynchronization = true;
                    log(Level.WARNING, "At least one OpenCL platform uses OpenCL 1.0, which is not thread-safe: will use (slower) synchronized low-level bindings.");
                }
			} finally {
				if (probe != null)
					BridJ.unregister(OpenCLProbeLibrary.class);
				probe = null;
			}
		}
		
        if (debug) {
            String debugArgs = System.getenv(JAVACL_DEBUG_COMPILER_FLAGS_PROP);
            if (debugArgs != null)
                DEBUG_COMPILER_FLAGS = Arrays.asList(debugArgs.split(" "));
            else if (Platform.isMacOSX())
                DEBUG_COMPILER_FLAGS = Arrays.asList("-g");
            else
                DEBUG_COMPILER_FLAGS = Arrays.asList("-O0", "-g");
            
            int pid = ProcessUtils.getCurrentProcessId();
            log(Level.INFO, "Debug mode enabled with compiler flags \"" + StringUtils.implode(DEBUG_COMPILER_FLAGS, " ") + "\" (can be overridden with env. var. JAVACL_DEBUG_COMPILER_FLAGS_PROP)");
            log(Level.INFO, "You can debug your kernels with GDB using one of the following commands :\n"
                    + "\tsudo gdb --tui --pid=" + pid + "\n"
                    + "\tsudo ddd --debugger \"gdb --pid=" + pid + "\"\n"
                    + "More info here :\n"
                    + "\thttp://code.google.com/p/javacl/wiki/DebuggingKernels");
            
            
        }
        Class<? extends OpenCLLibrary> libraryClass = OpenCLLibrary.class;
        if (needsAdditionalSynchronization) {
            try {
                libraryClass = BridJ.subclassWithSynchronizedNativeMethods(libraryClass);
            } catch (Throwable ex) {
                throw new RuntimeException("Failed to create a synchronized version of the OpenCL API bindings: " + ex, ex);
            }
        }
        BridJ.register(libraryClass);
        try {
            IOpenCLLibrary cl = libraryClass.newInstance();
            CL = logCalls ? wrapWithLogs(cl) : cl;
        } catch (Throwable ex) {
            throw new RuntimeException("Failed to instantiate library " + libraryClass.getName() + ": " + ex, ex);
        }
	}
    
    private static IOpenCLLibrary wrapWithLogs(final IOpenCLLibrary cl) {
        return (IOpenCLLibrary) Proxy.newProxyInstance(JavaCL.class.getClassLoader(), new Class[] { IOpenCLLibrary.class }, new InvocationHandler() {
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                log(Level.INFO, method.getName() + "(" + StringUtils.implode(args, ", ") + ")");
                Object ret = method.invoke(cl, args);
                log(Level.INFO, "\t" + method.getName() + " -> " + ret);
                return ret;
            }
        });
    }
	
    /**
     * List the OpenCL implementations that contain at least one GPU device.
     */
    public static CLPlatform[] listGPUPoweredPlatforms() {
        CLPlatform[] platforms = listPlatforms();
        List<CLPlatform> out = new ArrayList<CLPlatform>(platforms.length);
        for (CLPlatform platform : platforms) {
            if (platform.listGPUDevices(true).length > 0)
                out.add(platform);
        }
        return out.toArray(new CLPlatform[out.size()]);
    }
	/**
	 * Lists all available OpenCL implementations.
	 */
    public static CLPlatform[] listPlatforms() {
        Pointer<Integer> pCount = allocateInt();
        error(getPlatformIDs(0, null, pCount));

        int nPlats = pCount.getInt();
        if (nPlats == 0)
            return new CLPlatform[0];

        Pointer<cl_platform_id> ids = allocateTypedPointers(cl_platform_id.class, nPlats);

        error(getPlatformIDs(nPlats, ids, null));
        CLPlatform[] platforms = new CLPlatform[nPlats];

        for (int i = 0; i < nPlats; i++) {
            platforms[i] = new CLPlatform(ids.getSizeTAtIndex(i));
        }
        return platforms;
    }

	/**
	 * Creates an OpenCL context formed of the provided devices.<br>
	 * It is generally not a good idea to create a context with more than one device,
	 * because much data is shared between all the devices in the same context.
	 * @param devices devices that are to form the new context
	 * @return new OpenCL context
	 */
    public static CLContext createContext(Map<CLPlatform.ContextProperties, Object> contextProperties, CLDevice... devices) {
		return devices[0].getPlatform().createContext(contextProperties, devices);
    }

    /**
	 * Allows the implementation to release the resources allocated by the OpenCL compiler. <br>
	 * This is a hint from the application and does not guarantee that the compiler will not be used in the future or that the compiler will actually be unloaded by the implementation. <br>
	 * Calls to Program.build() after unloadCompiler() will reload the compiler, if necessary, to build the appropriate program executable.
	 */
	public static void unloadCompiler() {
		error(CL.clUnloadCompiler());
	}

	/**
	 * Returns the "best" OpenCL device (currently, the one that has the largest amount of compute units).<br>
	 * For more control on what is to be considered a better device, please use the {@link JavaCL#getBestDevice(CLPlatform.DeviceFeature[]) } variant.<br>
	 * This is currently equivalent to <code>getBestDevice(MaxComputeUnits)</code>
	 */
    public static CLDevice getBestDevice() {
        return getBestDevice(CLPlatform.DeviceFeature.MaxComputeUnits);
    }
	/**
	 * Returns the "best" OpenCL device based on the comparison of the provided prioritized device feature.<br>
	 * The returned device does not necessarily exhibit the features listed in preferredFeatures, but it has the best ordered composition of them.<br>
	 * For instance on a system with a GPU and a CPU device, <code>JavaCL.getBestDevice(CPU, MaxComputeUnits)</code> will return the CPU device, but on another system with two GPUs and no CPU device it will return the GPU that has the most compute units.
	 */
    public static CLDevice getBestDevice(CLPlatform.DeviceFeature... preferredFeatures) {
        List<CLDevice> devices = new ArrayList<CLDevice>();
		for (CLPlatform platform : listPlatforms())
			devices.addAll(Arrays.asList(platform.listAllDevices(true)));
        return CLPlatform.getBestDevice(Arrays.asList(preferredFeatures), devices);
    }
    /**
     * Creates an OpenCL context with the "best" device (see {@link JavaCL#getBestDevice() })
     */
	public static CLContext createBestContext() {
        return createBestContext(DeviceFeature.MaxComputeUnits);
	}

    /**
     * Creates an OpenCL context with the "best" device based on the comparison of the provided 
	 * prioritized device feature (see {@link JavaCL#getBestDevice(CLPlatform.DeviceFeature...) })
     */
	public static CLContext createBestContext(CLPlatform.DeviceFeature... preferredFeatures) {
		CLDevice device = getBestDevice(preferredFeatures);
		return device.getPlatform().createContext(null, device);
	}

    /**
     * Creates an OpenCL context able to share entities with the current OpenGL context.
     * @throws RuntimeException if JavaCL is unable to create an OpenGL-shared OpenCL context.
     */
	public static CLContext createContextFromCurrentGL() {
        RuntimeException first = null;
        for (CLPlatform platform : listPlatforms()) {
            try {
                CLContext ctx = platform.createContextFromCurrentGL();
                if (ctx != null)
                    return ctx;
            } catch (RuntimeException ex) {
                if (first == null)
                    first = ex;
                
            }
        }
        throw new RuntimeException("Failed to create an OpenCL context based on the current OpenGL context", first);
    }
    
    static File userJavaCLDir = new File(new File(System.getProperty("user.home")), ".javacl");
    static File userCacheDir = new File(userJavaCLDir, "cache");
    
    static synchronized File createTempFile(String prefix, String suffix, String category) {
    		File dir = new File(userJavaCLDir, category);
    		dir.mkdirs();
    		try {
    			return File.createTempFile(prefix, suffix, dir);
    		} catch (IOException ex) {
    			throw new RuntimeException("Failed to create a temporary directory for category '" + category + "' in " + userJavaCLDir + ": " + ex.getMessage(), ex);
    		}
    }
    static synchronized File createTempDirectory(String prefix, String suffix, String category) {
    		File file = createTempFile(prefix, suffix, category);
		file.delete();
		file.mkdir();
    		return file;
    }
}
