/*
 * Drive.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;

import org.pykix.libburnia.bindings.Proxy;

/**
 * References a physical drive in the system.
 * 
 * @author Vreixo Formoso
 * @since 0.1
 */
public class Drive extends Proxy {

	Drive(long ptr) {
		super(ptr);
	}
	
	/**
	 * Grab the drive. This must be done before the drive can be used 
	 * (for reading, writing, etc).
	 * 
	 * @param load
	 * 		<code>true</code> to make the drive attempt to load a disc 
	 * 		(close its tray door, etc).
	 * @throws BurnException
	 * 		if it wasn't possible to grab the drive
	 */
	public void grab(boolean load) throws BurnException {
		if ( !burn_drive_grab(pointerOf(this), load ? 1 : 0) ) {
			throw new BurnException("Can't grab the drive");
		}
	}
	
	/**
	 * Release the drive. This should not be done until the drive is no longer
	 * busy (see burn_drive_get_status). The drive is (O_EXCL) unlocked.
	 * 
	 * @param eject
	 * 		<code>true</code> to make the drive eject the disc in it.
	 */
	public void release(boolean eject) {
		burn_drive_release( pointerOf(this), eject ? 1 : 0 );
	}
	
	/**
	 * Returns what kind of disc the drive is holding. 
	 * 
	 * <p>
	 * This function may need to be called more than once to get a proper 
	 * status from it. 
	 * 
	 * @return
	 * 		The status of the drive, or what kind of disc is in it.
	 */
	public DiscStatus getDiscStatus() {
		switch ( burn_disc_get_status( pointerOf(this) ) ) {
		case 0:
			return DiscStatus.UNREADY;
		case 1:
			return DiscStatus.BLANK;
		case 2:
			return DiscStatus.EMPTY;
		case 3:
			return DiscStatus.APPENDABLE;
		case 4:
			return DiscStatus.FULL;
		case 5:
			return DiscStatus.UNGRABBED;
		case 6:
			return DiscStatus.UNSUITABLE;
		default:
			throw new RuntimeException("Unexpected value");
		}
	}
	
	/**
	 * Tells the MMC Profile identifier of the loaded media. The drive must be
	 * grabbed in order to get a profile other than {@link Profile#NONE}.
	 * 
	 * <p>
	 * libburn currently writes only to profiles 0x09 "CD-R", 0x0a "CD-RW",
	 * 0x11 "DVD-R", 0x12 "DVD-RAM", 0x13 "DVD-RW restricted overwrite",
	 * 0x14 "DVD-RW Sequential Recording" or 0x1a "DVD+RW".
	 * 
	 * <p>
	 * If enabled by burn_allow_untested_profiles() it also writes to profile
	 * 0x15 "DVD-R/DL Sequential Recording".
	 * 
	 * @return
	 * 		The profile of the loaded media.
	 */
	/*
	 * I've implemented this in a different way that libburn, I prefer returning
	 * profile as an enum. It has a lot of advantages for the programmer, 
	 * and only one problem: this cannot distinguish between different unknown
	 * profiles.
	 */
	public Profile getProfile() {
		return Profile.get( burn_disc_get_profile( pointerOf(this) ) );
	}
	
	/**
	 * Tells whether a disc can be erased or not.
	 * 
	 * @return
	 * 		<code>true</code> is inserted disc is erasable,
	 * 		<code>false</code> if not.
	 */
	public boolean isErasable() {
		return burn_disc_erasable( pointerOf(this) );
	}
	
	/**
	 * Returns the progress and status of a drive.
	 * 
	 * @param progress
	 * 		Will be filled with the progress of the operation, 
	 * 		you can pass <code>null</code> if you don't care
	 * @return
	 * 		the current status of the drive.
	 */
	public DriveStatus getDriveStatus(Progress progress) {
		
		switch ( burn_drive_get_status( pointerOf(this), 
				     progress == null ? 0 : pointerOf(progress) ) ) {
		
		case 0:
			return DriveStatus.IDLE;
		case 1:
			return DriveStatus.SPAWNING;
		case 2:
			return DriveStatus.READING;
		case 3:
			return DriveStatus.WRITING;
		case 4:
			return DriveStatus.WRITING_LEADIN;
		case 5:
			return DriveStatus.WRITING_LEADOUT;
		case 6:
			return DriveStatus.ERASING;
		case 7:
			return DriveStatus.GRABBING;
		case 8:
			return DriveStatus.WRITING_PREGAP;
		case 9:
			return DriveStatus.CLOSING_TRACK;
		case 10:
			return DriveStatus.CLOSING_SESSION;
		case 11:
			return DriveStatus.FORMATTING;
		default:
			throw new RuntimeException("Unexpected drive status");
		}
	}
	
	/**
	 * Erase a disc in the drive. 
	 * 
	 * <p>
	 * The drive must be grabbed successfully <b>BEFORE</b> calling this 
	 * function. Always ensure that the drive reports a status of
	 * {@link DiscStatus#FULL} or {@link DiscStatus#APPENDABLE} before 
	 * calling this function. An erase operation is not cancellable, as 
	 * control of the operation is passed wholly to the drive and
	 * there is no way to interrupt it safely.
	 * 
	 * @see #getDiscStatus()
	 * 
	 * @param fast
	 * 		<code>true</code> to do a fast erase, where only the disc's 
	 * 		headers are erased; <code>false</code> to erase the entire disc.
	 *      With DVD-RW, fast blanking yields media capable only of DAO.
	 */
	public void erase(boolean fast) {
		burn_disc_erase(pointerOf(this), fast);
	}

	/**
	 * Creates a WriteOpts object for burning to this drive.
	 * 
	 * @return
	 * 		a WriteOpts object
	 */
	public WriteOpts newWriteOpts() {
		return new WriteOpts( burn_write_opts_new(pointerOf(this)) );
	}
	
	/**
	 * Return the best possible estimation of the currently available capacity 
	 * of the media. This might depend on particular write option settings. 
	 * For inquiring the space with such a set of options, the drive has to be
	 * grabbed and {@link DriveStatus#IDLE}. If not, then one will only get a 
	 * canned value from the most recent automatic inquiry (e.g. during last 
	 * drive grabbing).
	 * 
	 * <p>
	 * An eventual start address from burn_write_opts_set_start_byte() will be
	 * subtracted from the obtained capacity estimation. Negative results get
	 * defaulted to 0.
	 * 
	 * @param opts
	 * 		If not <code>null</code>, write parameters to be set on drive 
	 * 		before query.
	 * @return
	 * 		number of most probably available free bytes.
	 */
	public long getAvailableSpace(WriteOpts opts) {
		return burn_disc_available_space( pointerOf(this), 
				opts == null ? 0 : pointerOf(opts) );
	}
	
	/**
	 * Write a disc in the drive. 
	 * 
	 * <p>
	 * The drive must be grabbed successfully before calling this function. 
	 * Always ensure that the drive reports a status of 
	 * {@link DiscStatus#BLANK} or {@link DiscStatus#APPENDABLE} before
	 * calling this function.
	 * 
	 * <p>
	 * Note: write type {@link WriteType#SAO} is currently not capable of 
	 * writing a mix of data and audio tracks. You must use 
	 * {@link WriteType#TAO} for such sessions.
	 * To be set by {@link WriteOpts#setWriteType(WriteType, BlockTypes)}.
	 *  
	 * @param o
	 * 		The options for the writing operation.
	 * @param d
	 * 		The {@link Disc} object that described the disc to be created.
	 */
	/*
	 * ok, this not need to be a member of Drive, but as
	 * read, erase, etc are also here...
	 */
	public void write( WriteOpts o, Disc disc) {
		burn_disc_write( pointerOf(o), pointerOf(disc) );
	}
	
	/**
	 * Format media for use with libburn. This currently applies to DVD-RW
	 * in state "Sequential Recording" (profile 0014h) which get formatted to
	 * state "Restricted Overwrite" (profile 0013h). DVD+RW can be "de-iced"
	 * by setting bit2 of flag. Other media cannot be formatted yet.
	 *  
	 * @param size
	 * 		The size in bytes to be used with the format command. It should
	 * 		be divisible by 32*1024. The effect of this parameter may
	 * 		depend on the media profile.
	 * @param flag
	 * 		Bitfield for control purposes:
	 * 		<ul>
	 * 		<li>bit0= after formatting, write the given number of zero-bytes
	 * 		          to the media and eventually perform preliminary closing.
	 * 		<li>bit1= insist in size 0 even if there is a better default known
	 * 		<li>bit2= format to maximum available size
	 * 		<li>bit3= -reserved-
	 * 		<li>bit4= enforce re-format of (partly) formatted media
	 * 		<li>bit7= MMC expert application mode (else libburn tries to
	 * 		          choose a suitable format type):
	 * 		<li>bit8 to bit15 contain the index of the format to use. See
	 *               burn_disc_get_formats(), burn_disc_get_format_descr().
	 *               Acceptable types are: 0x00, 0x10, 0x11, 0x13, 0x15, 0x26.
	 *               If bit7 is set, bit4 is set automatically.
	 *      </ul>
	 */
	public void format(long size, int flag) {
		burn_disc_format(pointerOf(this), size, flag);
	}
	
	/**
	 * Cancel an operation on a drive.
	 * 
	 * <p>
	 * This will only work when the drive's busy state is 
	 * {@link DriveStatus#READING} or {@link DriveStatus#WRITING}.
	 * 
	 * @see #getDriveStatus(Progress)
	 */
	public void cancel() {
		burn_drive_cancel( pointerOf(this) );
	}
 	
	/**
	 * Inquire wether the most recent write run was successful. Reasons for
	 * non-success may be: rejection of burn parameters, abort during fatal 
	 * errors during write, a call to {@link #cancel()} by the application 
	 * thread.
	 * 
	 * @return
	 * 		<code>true</code> if burn seems to have went well, 
	 * 		<code>false</code> if burn failed. 
	 */
	public boolean wroteWell() {
		return burn_drive_wrote_well( pointerOf(this) );
	}
	
	/**
	 * Get the drive's disc object.
	 * 
	 * @return
	 * 		the Disc object or <code>null</code> on failure.
	 */
	public Disc getDisc() {
		
		long ptr = burn_drive_get_disc( pointerOf(this) );
		
		if ( ptr == 0 ) {
			return null;
		}
		
		Disc disc = (Disc) proxyFor(ptr);
		if ( disc == null) {
			disc = new Disc(ptr);
		}
		return disc;
	}
	
	/**
	 * Sets drive read and write speed.
	 * 
	 * @param read
	 * 		Read speed in k/s (0 is max).
	 * @param write
	 * 		Write speed in k/s (0 is max).
	 */
	public void setSpeed(int read, int write) {
		burn_drive_set_speed( pointerOf(this), read, write);
	}
	
	/**
	 * Creates a ReadOpts object for reading from this drive.
	 * 
	 * @return
	 * 		the ReadOpts object.
	 */
	public ReadOpts newReadOpts() {
		return new ReadOpts( burn_read_opts_new(pointerOf(this)) );
	}
	
	/**
	 * Gets the maximum write speed for a drive and eventually loaded media.
	 * 
	 * <p>
	 * The return value might change by the media type of already loaded media,
	 * again by call burn_drive_grab() and again by call burn_disc_read_atip().
	 *  
	 * @return
	 * 		Maximum write speed in K/s.
	 */
	public int getWriteSpeed() {
		return burn_drive_get_write_speed( pointerOf(this) );
	}
	
	/**
	 * Gets the minimum write speed for the drive and eventually loaded media.
	 * 
	 * <p>
	 * The return value might change by the media type of already loaded media,
	 * again by call burn_drive_grab() and again by call burn_disc_read_atip().
	 * 
	 * @return
	 * 		Minimum write speed in K/s.
	 */
	public int getMinWriteSpeed() {
		return burn_drive_get_min_write_speed( pointerOf(this) );
	}
	
	/**
	 * Gets the maximum read speed for the drive.
	 * 
	 * @return
	 * 		Maximum read speed in K/s.
	 */
	public int getReadSpeed() {
		return burn_drive_get_read_speed( pointerOf(this) );
	}
	
	/**
	 * Obtain a copy of the current speed descriptor list. The drive's list 
	 * gets updated on various occasions such as {@link #grab(boolean)} but 
	 * the copy obtained here stays untouched. 
	 * 
	 * <p>
	 * Speeds may appear several times in the list. The list content depends 
	 * much on drive and media type. It seems that .source == 1 applies mostly 
	 * to CD media whereas .source == 2 applies to any media.
	 * 
	 * @return
	 * 		The speed list, may be empty.
	 * @throws BurnException
	 * 		If severe error occurs.
	 */
	public SpeedDescriptor[] getSpeedList() throws BurnException {
		
		SpeedDescriptor[] lst = burn_drive_get_speedlist(pointerOf(this));
		
		if (lst == null) {
			throw new BurnException("Can't get speed list");
		}
		return lst;
	}
	
	/**
	 * <b>WARNING:</b>
	 * This revives an old bug-like behavior that might be dangerous.
	 * 
	 * Sets the drive status to {@link DiscStatus#BLANK} if it is 
	 * {@link DiscStatus#UNREADY} or {@link DiscStatus#UNSUITABLE}. 
	 * Thus marking media as writable which actually failed to declare 
	 * themselves either blank or (partially) filled.
	 * 
	 * @return
	 * 		<code>true</code> on success, <code>false</code> if drive is
	 * 		in an unsuitable status.
	 */
	public boolean pretendBlank() {
		return burn_disc_pretend_blank( pointerOf(this) );
	}
	
	/**
	 * <b>WARNING:</b> 
	 * This overrides the safety measures against unsuitable media.
	 * 
	 * Sets the drive status to {@link DiscStatus#FULL} if it is 
	 * {@link DiscStatus#UNREADY} or {@link DiscStatus#UNSUITABLE}. 
	 * Thus marking media as blankable which actually failed to declare 
	 * themselves either blank or (partially) filled.
	 * 
	 * @return
	 * 		<code>true</code> on success, <code>false</code> if drive is
	 * 		in an unsuitable status.
	 */
	public boolean pretendFull() {
		return burn_disc_pretend_full( pointerOf(this) );
	}
	
	/**
	 * Reads ATIP information from inserted media. To be obtained via
	 * {@link #getWriteSpeed()}, {@link #getMinWriteSpeed()},
	 * burn_drive_get_start_end_lba(). The drive must be grabbed for this call.
	 * 
	 * @return
	 * 		<code>true</code> on success, <code>false</code> if no valid ATIP
	 * 		info read.
	 * @throws BurnException
	 * 		On a severe error.
	 */
	public boolean readAtip() throws BurnException {
		switch ( burn_disc_read_atip( pointerOf(this) ) ) {
		case 1:
			return true;
		case 0:
			return false;
		default:
			throw new BurnException("Severe error reading ATIP");
		}
	}
	
	/**
	 * Returns start lba of the media which is currently inserted in the drive.
	 * The drive has to be grabbed to have hope for reply.
	 * 
	 * <p>
	 * Shortcomming (not a feature): unless {@link #readAtip()} was called
	 * only blank media will return valid info.
	 * 
	 * @return
	 * 		the start lba value
	 * @throws BurnException
	 * 		On invalid lba value.
	 */
	/*
	 * TODO mmm, I really don't like to throw an Exception here...
	 */
	public int getStartLba() throws BurnException {
		
		/* flag is unused yet */
		return burn_drive_get_start_lba(pointerOf(this), 0);
	}
	
	/**
	 * Returns end lba of the media which is currently inserted in the drive.
	 * The drive has to be grabbed to have hope for reply.
	 * 
	 * <p>
	 * Shortcomming (not a feature): unless {@link #readAtip()} was called
	 * only blank media will return valid info.
	 * 
	 * @return
	 * 		the end lba value
	 * @throws BurnException
	 * 		On invalid lba value.
	 */
	/*
	 * TODO mmm, I really don't like to throw an Exception here...
	 */
	public int getEndLba() throws BurnException {
		
		/* flag is unused yet */
		return burn_drive_get_end_lba(pointerOf(this), 0);
	}
	
	/**
	 * Read start lba of a track from media.
	 * 
	 * <p>
	 * Usually a track lba is obtained from the result of 
	 * {@link Track#getEntry()}. This call retrieves an updated lba, 
	 * and can address the invisible track to come.
	 * 
	 * <p>
	 * The drive must be grabbed for this call. One may not issue this call
	 * during ongoing {@link #write(WriteOpts, Disc)} or {@link #erase(boolean)}.
	 * 
	 * @param o
	 * 		If not <code>null</code>, write parameters to be set on drive 
	 * 		before query
	 * @param trackno
	 * 		0=next track to come, >0 number of existing track
	 * @return
	 * 		start lba
	 * @throws BurnException
	 * 		On error
	 */
	public int getTrackLba(WriteOpts o, int trackno) throws BurnException {
		return burn_disc_track_lba( pointerOf(this), 
				o == null ? 0 : pointerOf(o), trackno);
	}
	
	/**
	 * Read Next Writeable Address of a track from media.
	 * 
	 * <p>
	 * The drive must be grabbed for this call. One may not issue this call
	 * during ongoing {@link #write(WriteOpts, Disc)} or {@link #erase(boolean)}.
	 * 
	 * @param o
	 * 		If not <code>null</code>, write parameters to be set on drive 
	 * 		before query
	 * @param trackno
	 * 		0=next track to come, >0 number of existing track
	 * @return
	 * 		Next Writeable Address
	 * @throws BurnException
	 * 		If error or non valid nwa
	 */
	public int getTrackNwa(WriteOpts o, int trackno) throws BurnException {
		return burn_disc_track_nwa( pointerOf(this), 
				o == null ? 0 : pointerOf(o), trackno);
	}
	
	/**
	 * Read start lba of the first track in the last complete session.
	 * 
	 * <p>
	 * This is the first parameter of mkisofs option -C. The second parameter
	 * is nwa as obtained by {@link #getTrackNwa(WriteOpts, int)} with 
	 * trackno 0.
	 * 
	 * @return
	 * 		returns the start address of that track
	 * @throws BurnException
	 * 		on error
	 */
	public int getMsc1() throws BurnException {
		return burn_disc_get_msc1(pointerOf(this));
	}
	
	/**
	 * Creates a {@link MultiCaps} with values which are appropriate for the 
	 * drive and the loaded media. The drive must be grabbed for this call. 
	 * 
	 * @param wt
	 * 		With {@link WriteType#NONE} the best capabilities of all write modes
	 * 		get returned. If set to a write mode like {@link WriteType#SAO} the
	 * 		capabilities with that particular mode are returned and the
	 * 		{@link MultiCaps#isWritingPossible()} return value is 
	 * 		<code>false</code> if the desired mode is not possible.
	 * @return
	 * 		The MultiCaps object.
	 * @throws BurnException
	 * 		On error.
	 */
	public MultiCaps getMultiCaps(WriteType wt) throws BurnException {
		
		/* flag param unused yet */
		return burn_disc_get_multi_caps(pointerOf(this), 
				wt.ordinal(), 0);
	}
	
	/**
	 * TODO not implemented in libburn
	 * 
	 * Read a disc from the drive and write it to an fd pair. The drive must be
	 * grabbed successfully BEFORE calling this function. Always ensure that the
	 * drive reports a status of BURN_DISC_FULL before calling this function.
	 * 
	 * @param opts
	 * 		The options for the read operation.
	 */
	public void read(ReadOpts opts) {
		burn_disc_read( pointerOf(this), pointerOf(opts) );
	}
	
	/**
	 * Inquire the formatting status, the associated sizes and the number of
	 * available formats.  The info is media specific and stems from MMC command
	 * 23h READ FORMAT CAPACITY. See mmc5r03c.pdf 6.24 for background details.
	 * Media type can be determined via burn_disc_get_profile().
	 * 
	 * @return
	 * 		
	 * @throws BurnException
	 * 		On failure
	 */
	public Formats getFormats() throws BurnException {
		Formats f = burn_disc_get_formats( pointerOf(this) );
		if ( f != null ) {
			return f;
		} else {
			throw new BurnException("Can't get formats");
		}
	}
	
	/**
	 * Inquire parameters of an available media format.
	 * 
	 * @param index
	 * 		he index of the format item. Beginning with 0 up to reply
	 * 		parameter from {@link #getFormats()} :
	 * 		{@link Formats#getNumFormats()} - 1
	 * @return
	 * @throws BurnException
	 * 		On error
	 */
	public FormatDesc getFormatDescr(int index) throws BurnException {
		FormatDesc fd = burn_disc_get_format_descr(pointerOf(this), index);
		if ( fd == null ) {
			throw new BurnException("Can't get format desc");
		}
		return fd;
	}
	
	private static native boolean burn_drive_grab(long drive, int load);

	private static native void burn_drive_release(long drive, int eject);
	
	private static native int burn_disc_get_status(long drive);

	private static native short burn_disc_get_profile(long d);
	
	private static native boolean burn_disc_erasable(long d);
	
	private static native int burn_drive_get_status(long drive, long p);

	private static native void burn_disc_erase(long drive, boolean fast);

	private static native long burn_write_opts_new(long drive);
	
	private static native long burn_disc_available_space(long d, long o);

	private static native void burn_disc_write(long o, long disc);
	
	private static native void burn_disc_format(long drive, long size, int flag);

	private static native void burn_drive_cancel(long drive);
	
	private static native boolean burn_drive_wrote_well(long d);

	private static native long burn_drive_get_disc(long d);

	private static native void burn_drive_set_speed(long d, int r, int w);
	
	private static native int burn_drive_get_write_speed(long d);
	
	private static native int burn_drive_get_min_write_speed(long d);
	
	private static native int burn_drive_get_read_speed(long d);

	private static native long burn_read_opts_new(long drive);
	
	private static native SpeedDescriptor[] burn_drive_get_speedlist(long d);
	
	private static native boolean burn_disc_pretend_blank(long drive);
	
	private static native boolean burn_disc_pretend_full(long drive);

	private static native int burn_disc_read_atip(long drive);

	/*
	 * These two functions are wrapping for the single C function:
	 * int burn_drive_get_start_end_lba(struct burn_drive *drive,
	 *                   int *start_lba, int *end_lba, int flag);
	 */
	private static native int burn_drive_get_start_lba(long drive, int flag) 
		throws BurnException;
	
	private static native int burn_drive_get_end_lba(long drive, int flag) 
		throws BurnException;
	
	/*
	 * These two functions are wrapping for the single C function:
	 * int burn_disc_track_lba_nwa(struct burn_drive *d, 
	 *     struct burn_write_opts *o, int trackno, int *lba, int *nwa);
	 */
	private static native int burn_disc_track_lba(long d, long o, int trackno)
		throws BurnException;
	
	private static native int burn_disc_track_nwa(long d, long o, int trackno)
		throws BurnException;

	private static native int burn_disc_get_msc1(long d)
		throws BurnException;

	private static native MultiCaps burn_disc_get_multi_caps(long d, 
			int wt, int flag) throws BurnException;

	private static native void burn_disc_read(long d, long o);
	
	private static native Formats burn_disc_get_formats(long drive);
	
	private static native FormatDesc burn_disc_get_format_descr(long d, 
			int index);
	
}
