import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Vector;

/**
 * A MS Windows Shortcut (*.LNK) file builder, pure Java port.
 * Supports MS WebFolders.
 *
 * @author ggongaware _at_ itensil _dot_ com
 *
 * 7-7-04 cleaned up some documentation
 * 3-3-03 partial WinNT5 functionality
 *
 * Note:
 * Some Win9x functionality based on Jesse Hager's
 * "The Windows Shortcut File Format" Document Version 1.0
 *
 */
public class Win32Link {

	private static final int HEAD = 'L';

	// *.LNK GUID  00021401-0000-0000-c000-000000000046
	private static final byte[] LNK_GUID = {
		(byte)0x01,(byte)0x14,(byte)0x02,(byte)0x00, //-
		(byte)0x00,(byte)0x00, //-
		(byte)0x00,(byte)0x00, //-
		(byte)0xc0,(byte)0x00, //-
		(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x00,(byte)0x46};

	// My Computer GUID 20d04fe0-3aea-1069-a2d8-08002b30309d
	private final byte[] MY_COMP_GUID = {
		(byte)0xe0,(byte)0x4f,(byte)0xd0,(byte)0x20, //-
		(byte)0xea,(byte)0x3a, //-
		(byte)0x69,(byte)0x10, //-
		(byte)0xa2,(byte)0xd8, //-
		(byte)0x08,(byte)0x00,(byte)0x2b,(byte)0x30,(byte)0x30,(byte)0x9d};

	// Web Folders GUID bdeadf00-c265-11d0-bced-00a0c90ab50f
	private final byte[] WEB_GUID = {
		(byte)0x00,(byte)0xdf,(byte)0xea,(byte)0xbd, //-
		(byte)0x65,(byte)0xc2, //-
		(byte)0xd0,(byte)0x11, //-
		(byte)0xbc,(byte)0xed, //-
		(byte)0x00,(byte)0xa0,(byte)0xc9,(byte)0x0a,(byte)0xb5,(byte)0x0f};
	//	+
	//	|--> 26 MAGIC_WEBFOLDERBYTES???
	//			+ [Display BSTR] + [URL BSTR] + (int)0
	// I don't know what these undocoumented SHITEMID.abID bytes are
	// but this variation of them work on all URL path levels, so I'm happy.
	private static final byte[] MAGIC_WEBFOLDERBYTES = {
												0x4c, 0x50, 0x00, 0x22,
												0x42, 0x57, 0x00, 0x00,
												0x00, 0x00, 0x00, 0x00,
												0x00, 0x00, 0x00, 0x00,
												0x00, 0x00, 0x00, 0x00,
												0x00, 0x00, 0x10, 0x00,
												0x00, 0x00 };
	//	OR +
	//	|--> 26 MAGIC_WEBFILEBYTES???
	//			+ [Display BSTR] + [URL BSTR] + (int)0
	// I don't know what these undocoumented SHITEMID.abID bytes are
	// but this variation works on the file path level, so I'm happy.
	private static final byte[] MAGIC_WEBFILEBYTES = {
												0x4c, 0x50, 0x00, 0x22,
												0x42, 0x57, (byte)0x80, 0x4c,
												0x70, 0x48, 0x54, 0x3e,
												(byte)0xc3, 0x01, 0x00, 0x6a,
												0x01, 0x00, 0x00, 0x00,
												0x00, 0x00, (byte)0x80, 0x00,
												0x00, 0x00 };

    /**
     * Items ids, similar to the MS Shell API's struct SHITEMID
     */
    public static interface ItemID {
		public char getSize();
		public byte [] getBytes();
	}

	/**
	 * TODO: Make local and NetBIOS ItemID classes
	 */

    /**
     * The Root item of a local Machine "My Computer"
     */
	public class MyComputerId implements ItemID {
		public char getSize() {
			return (char)(MY_COMP_GUID.length + 2 + 2);
		}

		public final byte [] getBytes() {
			char size = getSize();
			ByteBuffer buf = new ByteBuffer(size);
			buf.putChar(size);
			buf.putChar((char)0x501f); // Unknown word
			buf.putBytes(MY_COMP_GUID);
			return buf.getBytes();
		}
	}

    /**
     * The Root of "WebFolders"
     */
	public class WebFoldersId implements ItemID {

		public char getSize() {
			return (char)(WEB_GUID.length + 2 + 2);
		}

		public byte [] getBytes() {
			char size = getSize();
			ByteBuffer buf = new ByteBuffer(size);
			buf.putChar(size);
			buf.putChar((char)0x002E); // Unknown word
			buf.putBytes(WEB_GUID);
			return buf.getBytes();
		}
	}

	/**
     * A Folder (path) of a webfolder
     */
	public class URLFolderId implements ItemID {

		private String displayName;
		private String URL;

		public URLFolderId(String displayName, String URL) {
			this.displayName = displayName;
			this.URL = URL;
		}

		public char getSize() {
			return (char)(MAGIC_WEBFOLDERBYTES.length
					+ getBSTRSize(displayName)
					+ getBSTRSize(URL)
					+ 4 + 2);
		}

		public byte [] getBytes() {
			char size = getSize();
			ByteBuffer buf = new ByteBuffer(size);
			buf.putChar(size);
			buf.putBytes(MAGIC_WEBFOLDERBYTES);
			buf.putBSTR(displayName, unicode);
			buf.putBSTR(URL, unicode);
			buf.putInt(0);
			return buf.getBytes();
		}
	}

    /**
     * A File in a webfolder
     */
    public class URLFileId implements ItemID {

		private String displayName;
		private String URL;

		public URLFileId(String displayName, String URL) {
			this.displayName = displayName;
			this.URL = URL;
		}

		public char getSize() {
			return (char)(MAGIC_WEBFILEBYTES.length
					+ getBSTRSize(displayName)
					+ getBSTRSize(URL)
					+ 4 + 2);
		}

		public byte [] getBytes() {
			char size = getSize();
			ByteBuffer buf = new ByteBuffer(size);
			buf.putChar(size);
			buf.putBytes(MAGIC_WEBFILEBYTES);
			buf.putBSTR(displayName, unicode);
			buf.putBSTR(URL, unicode);
			buf.putInt(0);
			return buf.getBytes();
		}
	}

	// shortcut bit flags
	private static final int SC_HAS_SHELLITEM = 0;
	private static final int SC_IS_FILE_OR_FOLDER = 1;//false = something else
	private static final int SC_HAS_DESCRIPTION = 2;
	private static final int SC_HAS_RELATIVE_PATH = 3;
	private static final int SC_HAS_WORKING_DIRECTORY = 4;
	private static final int SC_HAS_COMMAND_LINE_ARGS = 5;
	private static final int SC_HAS_CUSTOM_ICON = 6;
	private static final int SC_FOR_NT5 = 7;

	// file attributes bit flags
	public static final int FA_READ_ONLY = 0;
	public static final int FA_HIDDEN = 1;
	public static final int FA_SYSTEM = 2;
	public static final int FA_DIRECTORY = 4;
	public static final int FA_ARCHIVE = 5;
	public static final int FA_ENCRYPTED = 6;
	public static final int FA_NORMAL = 7;
	public static final int FA_TEMPORARY = 8;
	public static final int FA_SPARSE = 9;
	public static final int FA_REPARSE = 10;
	public static final int FA_COMPRESSED = 11;
	public static final int FA_OFFLINE = 12;

	// show window flags
	public static final int SW_HIDE = 0;
	public static final int SW_NORMAL = 1;
	public static final int SW_SHOWMINIMIZED = 2;
	public static final int SW_SHOWMAXIMIZED = 3;
	public static final int SW_SHOWNOACTIVATE = 4;
	public static final int SW_SHOW = 5;
	public static final int SW_MINIMIZE = 6;
	public static final int SW_SHOWMINNOACTIVE = 7;
	public static final int SW_SHOWNA = 8;
	public static final int SW_RESTORE = 9;
	public static final int SW_SHOWDEFAULT = 10;

	// volume flags
	public static final int VF_LOCAL = 0;
	public static final int VF_NETWORK = 1;

	// data fields
	//
	// int flags
	private int 		fileAttibutes;
	private long 		createTime;
	private long 		modifiedTime;
	private long 		lastAccessTime;
	private int 		fileLength;
	private int 		iconNumber;
	private int 		showWindow;
	private int 		hotKey;
	// int zero
	// int zero
	// char size of item id list
	//  - loop over SHITEMID
	// 	- Start with "My Computer" GUID... etc.
	//	- These can get Undefined real fast
	// char zero (end null)
	// - Should match flags
	// char description length (BSTR)
	private String 		description;
	// char relativePath langth (BSTR)
	private String 		relativePath;
	// char workingDirectory length (BSTR)
	private String 		workingDirectory;
	// char commandlineArgs length (BSTR)
	private String 		commandlineArgs;
	// char iconFile length (BSTR)
	private String 		iconFile;
	// int zero


	private boolean unicode;
	private Vector itemIds;
	private boolean isfileFolder;

	public Win32Link() {
		showWindow = SW_NORMAL;
		createTime = System.currentTimeMillis();
		modifiedTime = createTime;
		lastAccessTime = createTime;
		unicode = true;
		isfileFolder = true;
		itemIds = new Vector();
	}

	/**
     * @param displayName - internal display name
     * @param URL - the folder destination (ex: http://abc.org/pub/docs/)
     * @param baseURL - the oldest anscestor folder you can recursively browse up to
     */
	public void setWebfolder(String displayName, String URL, String baseURL) {
		isfileFolder = false;
		createTime = 0;
		modifiedTime = 0;
		lastAccessTime = 0;
		addItemId(new MyComputerId());
		addItemId(new WebFoldersId());
		URLFolderId uid = new URLFolderId(displayName, baseURL);
		addItemId(uid);
		String path = URL.substring(baseURL.length());
		while (path.startsWith("/")) path = path.substring(1);
		StringBuffer parents = new StringBuffer(baseURL);
		int idx;
		while ((idx = path.indexOf('/')) >= 0
				|| path.length() > 0) {
			String name;
			if (idx > 0){
			 	name = path.substring(0, idx);
			 	path = path.substring(idx+1);
			} else {
				name = path;
				path = "";
			}
			parents.append('/');
			parents.append(name);
			uid = new URLFolderId(name, parents.toString());
			addItemId(uid);
		}
	}

    /**
     * @param displayName - internal display name
     * @param URL - the file destination (ex: http://abc.org/pub/docs/report.xls)
     * @param baseURL - the oldest anscestor folder you can recursively browse up to
     */
    public void setWebfile(String displayName, String URL, String baseURL) {
        int idx = URL.lastIndexOf("/");
        if (idx >= 0) {
            String pathURL = URL.substring(0, idx+1);
            String file =  URL.substring(idx+1);
            setWebfolder(displayName, pathURL, baseURL);
            if (idx < URL.length() - 1) {
                addItemId(new URLFileId(file, URL));
            }
        }
    }

	private byte [] buildBuffer() {
		ByteBuffer buf = new ByteBuffer(4096);
		buf.putInt(HEAD);
		buf.putBytes(LNK_GUID);
		int flags = 1;
		flags = setBit(flags, SC_IS_FILE_OR_FOLDER, isfileFolder);
		flags = setBit(flags, SC_HAS_DESCRIPTION, description != null);
		flags = setBit(flags, SC_HAS_RELATIVE_PATH, relativePath != null);
		flags = setBit(flags, SC_HAS_WORKING_DIRECTORY, workingDirectory != null);
		flags = setBit(flags, SC_HAS_COMMAND_LINE_ARGS, commandlineArgs != null);
		flags = setBit(flags, SC_HAS_CUSTOM_ICON, iconFile != null);
		flags = setBit(flags, SC_FOR_NT5, unicode);
		buf.putInt(flags);
		buf.putInt(fileAttibutes);
		buf.putLong(createTime);
		buf.putLong(modifiedTime);
		buf.putLong(lastAccessTime);
		buf.putInt(fileLength);
		buf.putInt(iconNumber);
		buf.putInt(showWindow);
		buf.putInt(hotKey);
		buf.putInt(0);
		buf.putInt(0);
		char itemSize = 2; // include end null item
		for (int i=0; i < itemIds.size(); i++) {
			ItemID item = (ItemID)itemIds.get(i);
			itemSize += item.getSize();
		}
		buf.putChar(itemSize);

		// add all items
		for (int i=0; i < itemIds.size(); i++) {
			ItemID item = (ItemID)itemIds.get(i);
			buf.putBytes(item.getBytes());
		}
		buf.putChar((char)0); // end item
		if (description != null) buf.putBSTR(description, unicode);
		if (relativePath != null) buf.putBSTR(relativePath, unicode);
		if (workingDirectory != null) buf.putBSTR(workingDirectory, unicode);
		if (commandlineArgs != null) buf.putBSTR(commandlineArgs, unicode);
		if (iconFile != null) buf.putBSTR(iconFile, unicode);
		buf.putInt(0);
		return buf.getBytes();
	}

	public void load(String fileName) throws IOException {
		load(new File(fileName));
	}

	public void load(File file) throws IOException {
		//DataInputStream in = new DataInputStream(new FileInputStream(file));
		// TODO: implement loading
	}

	public void save(String fileName) throws IOException {
		save(new File(fileName));
	}

    /**
     * Write out a *.lnk file
     * @param file
     * @throws IOException
     */
	public void save(File file) throws IOException {
		FileOutputStream fout = new FileOutputStream(file);
		fout.write(buildBuffer());
	}

    /**
     * Build and return the bytes that would appear in the *.lnk file
     * @return
     */
	public byte [] getBytes() {
		return buildBuffer();
	}

	private boolean testBit(int bits, int bitOffset) {
		return ((bits >> bitOffset) & 1) == 1;
	}

	private int setBit(int bits, int bitOffset, boolean on) {
		if (on) {
			bits |= 1 << bitOffset;
		} else {
			bits &= ~(1 << bitOffset);
		}
		return bits;
	}

	private int getBSTRSize(String str) {
		int len = 0;
		if (str != null) {
			len = str.length();
			len += 1; // null terminated
			if (unicode) {
				len *= 2;
			}
			len += 2; // pre-size WORD
		}
		return len;
	}

	/**
	 * Returns the commandlineArgs.
	 * @return String
	 */
	public String getCommandlineArgs() {
		return commandlineArgs;
	}

	/**
	 * Returns the createTime.
	 * @return long
	 */
	public long getCreateTime() {
		return createTime;
	}

	/**
	 * Returns the description.
	 * @return String
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * Returns the fileAttibutes.
	 * @return int
	 */
	public int getFileAttibutes() {
		return fileAttibutes;
	}

	/**
	 * Returns the fileLength.
	 * @return int
	 */
	public int getFileLength() {
		return fileLength;
	}

	/**
	 * Returns the hotKey.
	 * @return int
	 */
	public int getHotKey() {
		return hotKey;
	}

	/**
	 * Returns the iconFile.
	 * @return String
	 */
	public String getIconFile() {
		return iconFile;
	}

	/**
	 * Returns the iconNumber.
	 * @return int
	 */
	public int getIconNumber() {
		return iconNumber;
	}

	/**
	 * Returns the lastAccessTime.
	 * @return long
	 */
	public long getLastAccessTime() {
		return lastAccessTime;
	}

	/**
	 * Returns the modifiedTime.
	 * @return long
	 */
	public long getModifiedTime() {
		return modifiedTime;
	}


	/**
	 * Returns the relativePath.
	 * @return String
	 */
	public String getRelativePath() {
		return relativePath;
	}

	/**
	 * Returns the showWindow.
	 * @return int
	 */
	public int getShowWindow() {
		return showWindow;
	}

	/**
	 * Returns the workingDirectory.
	 * @return String
	 */
	public String getWorkingDirectory() {
		return workingDirectory;
	}

	/**
	 * Sets the commandlineArgs.
	 * @param commandlineArgs The commandlineArgs to set
	 */
	public void setCommandlineArgs(String commandlineArgs) {
		this.commandlineArgs = commandlineArgs;
	}

	/**
	 * Sets the createTime.
	 * @param createTime The createTime to set
	 */
	public void setCreateTime(long createTime) {
		this.createTime = createTime;
	}

	/**
	 * Sets the description.
	 * @param description The description to set
	 */
	public void setDescription(String description) {
		this.description = description;
	}

	/**
	 * Sets the fileAttibutes.
	 * @param fileAttibutes The fileAttibutes to set
	 */
	public void setFileAttibutes(int fileAttibutes) {
		this.fileAttibutes = fileAttibutes;
	}

	/**
	 * Sets the fileLength.
	 * @param fileLength The fileLength to set
	 */
	public void setFileLength(int fileLength) {
		this.fileLength = fileLength;
	}

	/**
	 * Sets the hotKey.
	 * @param hotKey The hotKey to set
	 */
	public void setHotKey(int hotKey) {
		this.hotKey = hotKey;
	}

	/**
	 * Sets the iconFile.
	 * @param iconFile The iconFile to set
	 */
	public void setIconFile(String iconFile) {
		this.iconFile = iconFile;
	}

	/**
	 * Sets the iconNumber.
	 * @param iconNumber The iconNumber to set
	 */
	public void setIconNumber(int iconNumber) {
		this.iconNumber = iconNumber;
	}

	/**
	 * Sets the lastAccessTime.
	 * @param lastAccessTime The lastAccessTime to set
	 */
	public void setLastAccessTime(long lastAccessTime) {
		this.lastAccessTime = lastAccessTime;
	}


	/**
	 * Sets the modifiedTime.
	 * @param modifiedTime The modifiedTime to set
	 */
	public void setModifiedTime(long modifiedTime) {
		this.modifiedTime = modifiedTime;
	}

	/**
	 * Sets the relativePath.
	 * @param relativePath The relativePath to set
	 */
	public void setRelativePath(String relativePath) {
		this.relativePath = relativePath;
	}

	/**
	 * Sets the showWindow.
	 * @param showWindow The showWindow to set
	 */
	public void setShowWindow(int showWindow) {
		this.showWindow = showWindow;
	}

	/**
	 * Sets the workingDirectory.
	 * @param workingDirectory The workingDirectory to set
	 */
	public void setWorkingDirectory(String workingDirectory) {
		this.workingDirectory = workingDirectory;
	}

    /**
     * Are the internal string Multi-byte unicode? (UTF-16)
     * @return
     */
	public boolean getUnicode() {
		return unicode;
	}

    /**
     * Set internal strings to Multi-byte unicode (UTF-16)
     * @param unicode
     */
	public void setUnicode(boolean unicode) {
		this.unicode = unicode;
	}

    /**
     * Add another ItemId to the trail of ItemIds
     * @param item
     */
	public void addItemId(ItemID item) {
		itemIds.add(item);
	}
}


