package kindle.test.jni;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

/**
 * 
 * @author Zuljin
 *
 * Singleton class that unpack *.so jni lib from azw2/jar to safe place and 
 * provide this location to native static class when it is initialized by
 * our custom classloader    
 *
 */
public class JniLibrariesManager {
	private static Map<String, String> jnis = new HashMap<String, String>();
	private static JniLibrariesManager singletonObj = new JniLibrariesManager();

	private JniLibrariesManager() {
	}

	public static JniLibrariesManager getInstance() {
		return singletonObj;
	}

	public String getLibLocation(String key) {
        if (jnis.containsKey(key)) {
            return jnis.get(key);
        }
		return null;
	}

	public void addLibInSystem(String key, String short_lib_name) {
		jnis.put(key, short_lib_name);
	}

	public void addLibInDir(String key, String full_path_to_lib) {
		jnis.put(key, full_path_to_lib);
	}

	public void addLibInJar(String key, String path_to_lib_in_jar, String cache_dir) {
		
		URL url = Thread.currentThread().getContextClassLoader().getResource(path_to_lib_in_jar);

		try {
			JarURLConnection jarConn = (JarURLConnection) url.openConnection();
			URL jarURL = jarConn.getJarFileURL();
        
			JarInputStream jis = new JarInputStream(jarURL.openConnection().getInputStream());
			JarEntry je = null;
		
			while ((je = jis.getNextJarEntry()) != null) {
				if (path_to_lib_in_jar.equals(je.getName())) {
					//lets make here simple verification so if library exist in cache_dir then we will  
					//update it only it is different than one from AZW2 (to safe Kindle flash memory unnecessary write operations)
					//Unfortunately azw2 files created with eclipse doesn't provide correct values for functions
					//getSize, getCrc. Only getTime is provided so we will use it as a verification value

					String lib_name = new File(path_to_lib_in_jar).getName();
					
					//lets try to open file in safe location and check its date
					File f = new File(cache_dir+lib_name);
					
					if (false == f.exists() || f.lastModified() != je.getTime())
					{
						BufferedInputStream jarBuf = new BufferedInputStream(jis);
						FileOutputStream fStream = new FileOutputStream(f);

						int b;
						while ((b = jarBuf.read()) != -1)
							fStream.write(b);
						
						jarBuf.close();
						fStream.close();
						
						f.setLastModified(je.getTime());
					}
					
					jis.closeEntry();
				
					jnis.put(key, cache_dir+lib_name);
					break;
				}
				jis.closeEntry();
			}
		} catch (FileNotFoundException e) {
		} catch (IOException e) {
		}
	}
	
	public Object createLibInstance(String class_name) throws ClassNotFoundException {
        if (!jnis.containsKey(class_name)) {
            throw new ClassNotFoundException("Class not found " + class_name);
        }

    	JniClassLoader cl = null;
    	Class<?> ca = null;
    	Object lib = null;

    	for(int i = 0; i < 10; i++) {
	        cl = new JniClassLoader();

			ca = cl.findClass(class_name);

			try {
				lib = ca.newInstance();
			} catch (InstantiationException e) {
				throw new ClassNotFoundException("Instantiation of class failed " + class_name, e);
			} catch (IllegalAccessException e) {
				throw new ClassNotFoundException("IllegalAccess of class " + class_name, e);
			} catch (UnsatisfiedLinkError e) {
				//Ok we get exception that another classloader already loaded our jni library
				//But this classloader should be already destroyed so we call gc once more to be sure.
				//On Kindle Touch 2-3 such loops and gc should finally release jni library and we can continue
				lib = null; ca = null; cl = null;
		        System.gc();
		        try { //lets give some time to gc thread
					Thread.sleep(10);
				} catch (InterruptedException e1) {}
		        continue;
			}
			break;
        }

        if (null == lib)
        	throw new ClassNotFoundException("UnsatisfiedLinkError for class " + class_name);

		return lib;
	}
	
    public void destroyLibInstances() {
    	//it takes 2-3 times until gc finally release our classloader
    	//but even if not then in initSampleLib we are also ready to call gc few more times 
    	//when app is launched once again and classloader wasn't release here.
    	for(int i = 0; i < 5; i++) {
    		System.gc();
	        try { //lets give some time to gc thread
				Thread.sleep(10);
			} catch (InterruptedException e1) {}
    	}
    }

}
