package ca.ucalgary.util;

import java.io.*;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.*;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.concurrent.Semaphore;

/**
 * Utility to launch a Java application if not running, or pass a new file for 
 * opening if the app is already open (per user).  To use this class, call it as your 
 * main class, and pass the real main class name as the first argument.  Subsequent 
 * arguments are passed to the real main class's main(argv) method.
 */
public class SingletonApplication{
    private static ListenerThread listenerThread;

    public static void main(String[] args){
        if(args.length == 0 || args[0].matches("-{1,2}(h(elp)?)?")){
	    System.err.println("Usage: java "+SingletonApplication.class.getName()+
			       " <main class name> [file name or url to open #1] ...");
	    System.exit(1);
        }

	// Get the main class of the target program
	Class mainClass = null;
        try{
            mainClass = Class.forName(args[0]);
        } catch(Exception e){
	    System.err.println("Could not find main class "+args[0]);
	    System.exit(2);
        }
       	Class mainArgs[] = {String[].class};
	Method main = null;
	try{
	    main = mainClass.getMethod("main", mainArgs);
	} catch(Exception e){
	    System.err.println("Could not find static main method in class "+args[0]);
	    System.exit(11);
	}

        // Make sure its a valid public static main, returning void 
        main.setAccessible(true);
        int mods = main.getModifiers();
        if(main.getReturnType() != void.class || !Modifier.isStatic(mods) || !Modifier.isPublic(mods)) {
	    System.err.println("Specified main class ("+mainClass.getName()+
			       " does not have a method with signature 'public void static main(String[])'");
	    System.exit(3);
        }

	// Create a lock file for the app in its own directory
        String userHome = System.getProperty("user.home") ;
        String cfgDir = File.separator + mainClass.getName() + File.separator;
        File myappDir = new File(userHome + cfgDir);
        myappDir.mkdirs();
	String lockFileAbsolutePath = userHome + cfgDir + "lock";
        RandomAccessFile lockFile = null;
 	try{
	    lockFile = new RandomAccessFile(lockFileAbsolutePath, "rw");
 	} catch(Exception e){
 	    System.err.println("Failed to access/create file " + lockFileAbsolutePath+": "+e.getMessage());
 	    e.printStackTrace();
 	    System.exit(10);
 	}
 
	// Pass along all args except the first, the main class name
	String programArgs[] = new String[args.length-1];
	System.arraycopy(args, 1, programArgs, 0, programArgs.length);

//         FileOutputStream fos = null;
// 	try{
// 	    fos = new FileOutputStream(lockFile);
// 	} catch(Exception e){
// 	    System.err.println("Failed to get output stream for file " + lockFile.getAbsolutePath()+": "+e.getMessage());
// 	    e.printStackTrace();
// 	    System.exit(12);
// 	}

	FileLock lock = null;
	try{
	    lock = lockFile.getChannel().tryLock();
	} catch(Exception e){
	    System.err.println("Failed to check lock on " + lockFileAbsolutePath+": "+e.getMessage());
	    e.printStackTrace();
	    System.exit(9);
	}

        if (lock == null) {
	    // Already running, find out the port to send instructions through
	    try{
		//LineNumberReader lnr = new LineNumberReader(new FileReader(lockFile));
		for(String portString = lockFile.readLine(); portString != null; portString = lockFile.readLine()){
		    if(portString.matches("\\s*#.*")){
			continue; // ignore comment lines, starting with '#'
		    }
		    int port = 0;
		    try{
			port = Integer.parseInt(portString);
		    } catch(Exception e){
			System.err.println("Lock file "+lockFileAbsolutePath+
					   " should contain an integer, but '"+
					   portString+"' was found. Please delete the " +
					   "corrupt lock file and relaunch this application.");
			System.exit(4);
		    }
		    try{
			callOpen(port, programArgs);
		    } catch(Exception e){
			System.err.println("While calling open on existing "+main.getName()+" process: " + e.getMessage());
			e.printStackTrace();
			System.exit(7);
		    }
		    System.exit(0);
		}
	    } catch(Exception re){
		System.err.println("Failure while reading " + lockFileAbsolutePath+": "+re.getMessage());
		re.printStackTrace();
		System.exit(13);
	    }
	    System.err.println("Lock file "+lockFileAbsolutePath+
			       " was unexpectedly blank, launching new " +
			       "instance for lack of a better option.");
	}

	// first instance, or the app doesn't implement the right interface (why are you using this, then?)
	(new File(lockFileAbsolutePath)).deleteOnExit();
	try{
	    callMain(main, programArgs, lock);
	} catch(Exception e){
	    System.err.println("While calling main method of "+main.getName()+": " + e.getMessage());
	    e.printStackTrace();
	    System.exit(7);
	}
    }

    // Start the target program, passing the command-line arguments
    private static void callMain(Method main, String[] programArgs, FileLock lock) throws Exception{
	// Write to the lock file the port on which we'll listen for "file open" requests
	if(lock != null){
	    int port = 10000+((int) (1000*Math.random()));
	    ServerSocket serverSocket = null;
	    try{
		serverSocket = new ServerSocket(port);
	    } catch(IOException e){
		System.out.println("Could not listen on port "+port+": "+e.getMessage());		    
		System.exit(5);
	    }
	    // Listener for file open requests.  Note that the actual listener isn't set yet 
	    // (main class should call setArgumentListener())...
	    // but this thread will block on the socket until it is.
	    listenerThread = new ListenerThread(serverSocket);
	    listenerThread.start();

	    FileChannel fc = lock.channel();
	    fc.write(java.nio.ByteBuffer.wrap(("#Port on which app is listening for new open commands\n"+port+"\n").getBytes())); // note port in lock file for other launchers
	    fc.force(true);  //flush
	}

	// Create an argument array containing the array
	// of command-line arguments
	String argsArray[][] = {programArgs}; 
	
	// Call the main method (first arg [target object] is null, because the method is static)
	main.invoke((Object) null, (Object[]) argsArray);
    }

    private static void callOpen(int portNum, String[] programArgs) throws Exception{
	for(String programArg: programArgs){
	    Socket openSocket = new Socket("localhost", portNum);
	    openSocket.getOutputStream().write(programArg.getBytes());
	    openSocket.close();
	}
    }

    /**
     * This method should be called by the main() method of the target application.
     */
    public static void setArgumentListener(ArgumentListener al){
	if(listenerThread != null){
	    listenerThread.setArgumentListener(al);
	}
    }

    static class ListenerThread extends Thread{
	Semaphore openAvailable;
	ServerSocket serverSocket;
	ArgumentListener listener = null;

	public ListenerThread(ServerSocket serverSocket){
	    setDaemon(true);
	    openAvailable = new Semaphore(0, true);  // open not available yet: true = fair
	    this.serverSocket = serverSocket;
	}

	public void setArgumentListener(ArgumentListener al){
	    listener = al;
	    openAvailable.release(); // lets the daemon thread start calling ArgumentListener.processArgument(String)
	}

	public void run(){
	    Socket clientSocket = null;
	    
	    for(;;){
		try{
		    clientSocket = serverSocket.accept();
		} catch(IOException e){
		    System.err.println("Socket accept failed: " + e.getMessage());
		    System.exit(6);
		}
		try{
		    LineNumberReader lnr = new LineNumberReader(new InputStreamReader(clientSocket.getInputStream()));
		    final String arg = lnr.readLine();
		    // block until ArgumentListener available and not opening another file at the moment
		    new Thread(){public void run(){
			try{			
			    openAvailable.acquire(); 
			    listener.processArgument(arg);
			    openAvailable.release();
			} catch(Exception e){
			    System.err.println("Failed to notify argument listener of posted value '"+
					       arg+"': "+e.getMessage());
			}
		    }}.run();
		} catch(Exception re){
		    System.err.println("Failed to read posted argument, aborting launch: "+re.getMessage());
		    re.printStackTrace();
		    System.exit(8);
		}
	    }
	}
    }
}
