/*
 * Burn.java
 *
 * Copyright (c) 2007  Vreixo Formoso
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * See COPYING file for details.
 */

package org.pykix.libburnia.libburn;

/**
 * Library initialization/shutdown and miscellaneous functions.
 * 
 * 
 * @author Vreixo Formoso
 * @since 0.1
 */
public class Burn {

	static final String JNI_LIBRARY_NAME = "java-libburn-0.1";
	
	/** Maximum number of particularly permissible drive addresses. */
	public static final int WHITELIST_LEN = 255;
	
	/**
	 * Initialize the library.
	 * 
	 * <p>
	 * This must be called before using any other functions in the library. It
	 * may be called more than once with no effect.
	 * 
	 * <p>
	 * It is possible to 'restart' the library by shutting it down and
	 * re-initializing it. This is necessary if you follow the older and
	 * more general way of accessing a drive via burn_drive_scan() and
	 * burn_drive_grab(). See burn_drive_scan_and_grab() with its strong
	 * urges and its explanations. TODO fix this comment
	 * 
	 * @throws BurnException
	 * 		If library initialization fails.
	 */
	public static void initialize() throws BurnException {
		
		if ( burn_initialize() == 0 ) {
			
			throw new BurnException("Can't initialize libburnia library");
		}
	}
	
	/**
	 * Shutdown the library.
	 * 
	 * <p>
	 * This should be called before exiting your application. Make sure that all
	 * drives you have grabbed are released <i>before</i> calling this.
	 */
	public static void finish() {
		burn_finish();
	}
	
	/**
	 * Set parameters for behavior on opening device files. To be called early
	 * after {@link #initialize()} and before any bus scan. But not mandatory at 
	 * all.
	 * 
	 * <p>
	 * Parameter value 1 enables a feature, 0 disables. Default is (1,0,0). 
	 * Have a good reason before you change it.
	 * 
	 * @param exclusive
	 * 		<ul>
	 * 		<li>1 = Try to open only devices which are not marked as busy
	 *              and try to mark them busy if opened sucessfully. (O_EXCL)
	 *              There are kernels which simply don't care about O_EXCL.
	 *              Some have it off, some have it on, some are switchable.
	 *      <li>2 = in case of a SCSI device, also try to open exclusively
	 *              the matching /dev/sr, /dev/scd and /dev/st .
	 *      <li>0 = no attempt to make drive access exclusive.
	 *      </ul>
	 * @param blocking
	 * 		Try to wait for drives which do not open immediately but
	 * 		also do not return an error as well. (O_NONBLOCK)
	 *      This might stall indefinitely with /dev/hdX hard disks.
	 * @param abortOnBusy
	 * 		Unconditionally abort process when a non blocking
	 *      exclusive opening attempt indicates a busy drive.
	 *      Use this only after thorough tests with your app.
	 */
	public static void presetDeviceOpen(int exclusive, int blocking, 
			int abortOnBusy) {
		
		burn_preset_device_open(exclusive, blocking, abortOnBusy);
	}
	
	/**
	 * Allows the use of media types which are implemented in libburn but not 
	 * yet tested. The list of those untested profiles is subject to change.
	 * Currently it contains: 0x15 "DVD-R/DL Sequential".
	 * If you really test such media, then please report the outcome on
	 * libburn-hackers@pykix.org
	 * If ever then this call should be done soon after {@link #initialize()} 
	 * before any drive scanning.
	 * 
	 * @param yes
	 * 		<code>true</code> to allow all implemented profiles, 
	 * 		<code>false</code> only tested media (default)
	 */
	public static void allowUntestedProfiles(boolean yes) {
		burn_allow_untested_profiles(yes);
	}
	
	/**
	 * Scan for drives. 
	 * 
	 * <p>
	 * No drives may be in use when this is called. All drive objects are 
	 * invalidated by using this function. Do <b>NOT</b> store drive
	 * objects across calls to this function or death AND pain will ensue.
	 * 
	 * <p>
	 * After this call all drives depicted by the returned array are subject
	 * to eventual (O_EXCL) locking. See burn_preset_device_open(). This state
	 * ends either with burn_drive_info_forget() or with burn_drive_release().
	 * 
	 * <p>
	 * It is unfriendly to other processes on the system to hold drives locked
	 * which one does not definitely plan to use soon.
	 * 
	 * @return
	 * 		an array of drive info items (cdroms/burners).
	 *      The returned array must be freed by burn_drive_info_free()
	 *      before burn_finish(), and also before calling this function
	 *      burn_drive_scan() again.
	 * @throws BurnException
	 * 		If scan fails
	 * 
	 * TODO correct above comment
	 */
	public static DriveInfo[] scan() throws BurnException {
		
		DriveInfo[] infos = burn_drive_scan();
		
		if ( infos == null ) {
			throw new BurnException("No drives found");
		}
		return infos;
	}
	
	/**
	 * Aquire a drive with known persistent address. 
	 * 
	 * <p>
	 * This is the sysadmin friendly way to open one drive and to leave all 
	 * others untouched. It bundles the following API calls to form a 
	 * non-obtrusive way to use libburn:
	 * burn_drive_add_whitelist() , {@link #scan()}, {@link Drive#grab(boolean)}
	 * 
	 * <p>
	 * You are <b>strongly urged</b> to use this call whenever you know the 
	 * drive address in advance.
	 * 
	 * <p>
	 * If not, then you have to use directly above calls. In that case, you are
	 * <b>strongly urged</b> to drop any unintended drive which will be 
	 * exclusively occupied and not closed by {@link #scan()}.
	 * This can be done by shutting down the library including a call to
	 * {@link #finish()}. You may later start a new libburn session and should 
	 * then use the function described here with an address obtained after
	 * {@link #scan()} via {@link DriveInfo#getAdr()}.
	 * 
	 * <p>
	 * Another way is to drop the unwanted drives by {@link DriveInfo#forget(int)}.
	 * 
	 * @param adr
	 * 		The persistent address of the desired drive. Either obtained
	 *      by {@link DriveInfo#getAdr()} or guessed skillfully by application
	 *      resp. its user.
	 * @param load
	 * 		<code>true</code> to make the drive attempt to load a disc (close 
	 * 		its tray door, etc).
	 * @return
	 * 		A DriveInfo object with the requested cdrom/burner.
	 * @throws BurnException
	 * 		If drive not found.
	 */
	public static DriveInfo scanAndGrab(String adr, boolean load) 
	throws BurnException {
		
		DriveInfo info = burn_drive_scan_and_grab(adr, load);
		
		if ( info == null ) {
			throw new BurnException("Drive not found");
		}
		return info;
	}
	
	/**
	 * Add a device to the list of permissible drives. As soon as some entry 
	 * is in the whitelist all non-listed drives are banned from scanning.
	 * 
	 * <p>
	 * The number of devices that can be added to the whitelist is limited
	 * to {@link #WHITELIST_LEN}.
	 * 
	 * @param adr
	 * 		The persistent address of the drive.
	 * @return
	 * 		<code>true</code> on success, <code>false</code> otherwise.
	 */
	public static boolean addWhiteList(String adr) {
		return (burn_drive_add_whitelist(adr) == 1);
	}
	
	/**
	 * Remove all drives from whitelist. This enables all possible drives.
	 */
	public static void clearWhiteList() {
		burn_drive_clear_whitelist();
	}
	
	/**
	 * Set the verbosity level of the library. 
	 * 
	 * <p>
	 * The default value is 0, which means that nothing is output on stderr. 
	 * The more you increase this, the more debug output should be displayed 
	 * on stderr for you.
	 * 
	 * <p>
	 * This is for development only. Not suitable for applications.
	 * 
	 * @param level
	 * 		The verbosity level desired. 0 for nothing, higher positive
	 *		values for more information output.
	 */
	public static void setVerbosity(int level) {
		burn_set_verbosity(level);
	}
	
	/**
	 * Evaluate wether the given address would be a possible persistent 
	 * drive address of libburn.
	 */
	public static boolean isEnumerableAdr(String adr) {
		return burn_drive_is_enumerable_adr(adr);
	}
	
	/**
	 * Try to convert a given existing filesystem address into a persistent 
	 * drive address. This succeeds with symbolic links or if a hint about 
	 * the drive's system address can be read from the filesystem object and 
	 * a matching drive is found.
	 * 
	 * @param path
	 * 		The address of an existing file system object
	 * @return
	 * 		The persistent address or <code>null</code> on failure
	 */
	public static String convertFsAdr(String path) {
		return burn_drive_convert_fs_adr(path);
	}
	
	/**
	 * 
	 */
	public interface AbortHandler {
		
		/**
		 * Function suitable to produce appeasing messages while
		 * burn is been canceled.
		 * 
		 * @param patience
		 * 		Maximum number of seconds to wait.
		 * @param elapsed
		 * 		Elapsed number of seconds.
		 * @return
		 */
		public int abortPacifier(int patience, int elapsed);
	}
	
	/**
	 * Abort any running drive operation and finally call {@link #finish()}.
	 * 
	 * <p>
	 * You MUST calm down the busy drive if an aborting event occurs during a
	 * burn run. For that you may call this function either from your own signal
	 * handling code or indirectly by activating the builtin signal handling:
	 * burn_set_signal_handling("my_app_name : ", NULL, 0);
	 * 
	 * <p>
	 * Else you may eventually call {@link Drive#cancel()} on the active drive 
	 * and wait for it to assume state {@link DriveStatus#IDLE}.
	 * 
	 * @param patience
	 * 		Maximum number of seconds to wait for drives to finish
	 * @param pacifier
	 * 		A function to produce appeasing messages. You can pass 
	 * 		<code>null</code> to not produce those messages.
	 * @return
	 * 		<code>true</code> if all went well, <code>false</code> if had
	 * 		to leave a drive in unclean state
	 * @throws BurnException
	 * 		On severe error, do no use libburn again
	 * @see #abort(int, String)
	 */
	public static boolean abort(int patience, AbortHandler pacifier) 
	throws BurnException {
		
		return burn_abort_java(patience, pacifier);
	}
	
	/**
	 * Abort any running drive operation and finally call {@link #finish()}.
	 * 
	 * <p>
	 * You MUST calm down the busy drive if an aborting event occurs during a
	 * burn run. For that you may call this function either from your own signal
	 * handling code or indirectly by activating the builtin signal handling:
	 * burn_set_signal_handling("my_app_name : ", NULL, 0);
	 * 
	 * <p>
	 * Else you may eventually call {@link Drive#cancel()} on the active drive 
	 * and wait for it to assume state {@link DriveStatus#IDLE}.
	 * 
	 * <p>
	 * This version of abort uses the default pacifier function that print
	 * messages to stdout.
	 * 
	 * @param patience
	 * 		Maximum number of seconds to wait for drives to finish
	 * @param msg
	 * 		The message to be printed.
	 * @return
	 * 		<code>true</code> if all went well, <code>false</code> if had
	 * 		to leave a drive in unclean state
	 * @throws BurnException
	 * 		On severe error, do no use libburn again
	 * @see #abort(int, org.pykix.libburnia.libburn.Burn.AbortHandler)
	 */
	public static boolean abort(int patience, String msg) throws BurnException {
		return burn_abort_def(patience, msg);
	}
	
	/**
	 * Interface suitable for {@link Burn#setSignalHandler(org.pykix.libburnia.libburn.Burn.SignalHandler, int)} 
	 */
	public interface SignalHandler {
		
		/**
		 * Function to be called when a signal is received.
		 * 
		 * @param signum
		 * @param flag
		 * @return
		 * 		has to return -2 if it does not want the process to
		 * 		exit with value 1.
		 */
		public int handleSignal(int signum, int flag);
	}
	
	/**
	 * Control builtin signal handling. See also 
	 * {@link #abort(int, org.pykix.libburnia.libburn.Burn.AbortHandler)}.
	 * 
	 * @param handler
	 * 		A handler to be called on signals. It's 
	 * 		{@link SignalHandler#handleSignal(int, int) handleSignal} method
	 * 		will be called when a signal is received. It should finally call 
	 * 		{@link #abort(int, org.pykix.libburnia.libburn.Burn.AbortHandler)}.
	 * @param mode
	 * 		0 call handler(signum, 0) on nearly all signals
	 * 		1 enable system default reaction on all signals
	 * 		2 try to ignore nearly all signals
	 * 		10 like mode 2 but handle SIGABRT like with mode 0
	 */
	public static void setSignalHandler(SignalHandler handler, int mode) {
		
		if ( handler == null ) {
			throw new NullPointerException("Handler cannot be null");
		}
		
		burn_set_signal_handling(handler, mode);
	}
	
	/**
	 * Control builtin signal handling.
	 * 
	 * <p>
	 * This forces the usage of the default signal handler, that will
	 * eventually call {@link #abort(int, String) abort} and then perform 
	 * exit(1). 
	 * 
	 * <p>
	 * If you want to provide a custom signal handler you can use
	 * {@link #setSignalHandler(org.pykix.libburnia.libburn.Burn.SignalHandler, int)}
	 * 
	 * @param msg
	 * 		If not <code>null</code> then it is used as prefix for pacifier 
	 * 		messages.
	 * @param mode
	 * 		0 call handler(signum, 0) on nearly all signals
	 * 		1 enable system default reaction on all signals
	 * 		2 try to ignore nearly all signals
	 * 		10 like mode 2 but handle SIGABRT like with mode 0
	 */
	public static void setDefaultSignalHandler(String msg, int mode) {
		burn_set_default_signal_handling(msg, mode);
	}
	
	public static class Version {
		
		public int major;
		public int minor;
		public int micro;
		
		public Version(int major, int minor, int micro) {
			super();
			this.major = major;
			this.minor = minor;
			this.micro = micro;
		}
		
		@Override
		public String toString() {
			return major + "." + minor + "." + micro;
		}
	}
	
	/**
	 * Returns the library's version.
	 */
	public static Version getVersion() {
		return burn_version();
	}
	
	private static native int burn_initialize();
	
	private static native void burn_finish();

	private static native void burn_preset_device_open(int exclusive, 
			int blocking, int abort_on_busy);
	
	private static native void burn_allow_untested_profiles(boolean yes);
	
	private static native DriveInfo[] burn_drive_scan();
	
	private static native DriveInfo burn_drive_scan_and_grab(String adr, 
			boolean load);
	
	private static native int burn_drive_add_whitelist(String adr);

	private static native void burn_drive_clear_whitelist();
	
	private static native void burn_set_verbosity(int level);

	private static native boolean burn_drive_is_enumerable_adr(String adr);

	private static native String burn_drive_convert_fs_adr(String path);
	
	private static native boolean burn_abort_def(int patience, String msg) 
		throws BurnException;
	
	//TODO not tested at all!!
	private static native boolean burn_abort_java(int patience, AbortHandler h) 
		throws BurnException;
	
	private static native void burn_set_signal_handling(SignalHandler handler, 
		     int mode);
	
	private static native void burn_set_default_signal_handling(String msg, 
		     int mode);
	
	private static native Version burn_version();
	
	static {
		System.loadLibrary(JNI_LIBRARY_NAME);
	}
}
