/*
 * Write the application bundle file: Info.plist
 *
 *
 * Copyright (c) 2003, Seth J. Morabito <sethm@loomcom.com> All rights reserved.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package net.sourceforge.jarbundler;

// This package's imports
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import net.sourceforge.jarbundler.AppBundleProperties;

// Java I/O
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

// Java Utility
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

// Java language imports
import java.lang.Boolean;
import java.lang.ClassCastException;
import java.lang.Double;
import java.lang.String;
import java.lang.System;
import java.util.Map;

// Apache Ant
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.util.FileUtils;

// Java XML DOM creation
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;

// W3C DOM
import org.w3c.dom.Document;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.Attr;



/**
 * Write out a Java application bundle property list file. For descriptions of
 * the property list keys, see <a
 * href="http://developer.apple.com/documentation/MacOSX/Conceptual/BPRuntimeConfig/Articles/PListKeys.html"
 * >Apple docs</a>.
 */


public class PropertyListWriter {


	// Our application bundle properties
	private AppBundleProperties bundleProperties;

	// DOM version of Info.plist file
	private Document document = null;


	private FileUtils fileUtils = FileUtils.getFileUtils();

	/**
	 * Create a new Property List writer.
	 */
	public PropertyListWriter(AppBundleProperties bundleProperties) {
		this.bundleProperties = bundleProperties;
	}

	public void writeFile(File fileName) throws BuildException {

		Writer writer = null;

		try {

			this.document = createDOM();
			buildDOM();

			TransformerFactory transFactory = TransformerFactory.newInstance();
			Transformer trans = transFactory.newTransformer();
			trans.setOutputProperty(OutputKeys.INDENT, "yes");
			trans.setOutputProperty("{http://xml.apache.org/xslt}indent-amount","2");
			trans.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "-//Apple Computer//DTD PLIST 1.0//EN");
			trans.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://www.apple.com/DTDs/PropertyList-1.0.dtd");
			writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(fileName), "UTF-8"));
			trans.transform(new DOMSource(document), new StreamResult(writer));
		} catch (TransformerConfigurationException tce) {
			throw new BuildException(tce);
		} catch (TransformerException te) {
			throw new BuildException(te);
		} catch (ParserConfigurationException pce) {
			throw new BuildException(pce);
		} catch (IOException ex) {
			throw new BuildException("Unable to write  \"" + fileName + "\"");
		} finally {
			fileUtils.close(writer);
		}
	}

	private Document createDOM() throws ParserConfigurationException {

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder documentBuilder = factory.newDocumentBuilder();
		DOMImplementation domImpl = documentBuilder.getDOMImplementation();

		// We needed to reference using the full class name here because we already have
		//  a class named "DocumentType"

		org.w3c.dom.DocumentType doctype = domImpl.createDocumentType(
		       "plist",
		       "-//Apple Computer//DTD PLIST 1.0//EN",
		       "http://www.apple.com/DTDs/PropertyList-1.0.dtd");

		return domImpl.createDocument(null, "plist", doctype);
	}


	private void buildDOM()  {

		Element plist = this.document.getDocumentElement();
		plist.setAttribute("version","1.0");

		// Open the top level dictionary, <dict>

		Node dict = createNode("dict", plist);

		// Application short name i.e. About menu name
		writeKeyStringPair("CFBundleName", bundleProperties.getCFBundleName(), dict);

		// Finder 'Version' label, defaults to "1.0"
		writeKeyStringPair("CFBundleShortVersionString", bundleProperties.getCFBundleShortVersionString(), dict);

		// Finder 'Get Info'
		writeKeyStringPair("CFBundleGetInfoString", bundleProperties.getCFBundleGetInfoString(), dict);

		// Mac OS X required key, defaults to "false"
		writeKeyStringPair("CFBundleAllowMixedLocalizations",
		     (bundleProperties.getCFBundleAllowMixedLocalizations() ? "true" : "false"), dict);

		// Mac OS X required, defaults to "6.0"
		writeKeyStringPair("CFBundleInfoDictionaryVersion",
		     bundleProperties.getCFBundleInfoDictionaryVersion(), dict);

		// Bundle Executable name, required, defaults to "JavaApplicationStub"
		writeKeyStringPair("CFBundleExecutable", bundleProperties.getCFBundleExecutable(), dict);

		// Bundle Development Region, required, defaults to "English"
		writeKeyStringPair("CFBundleDevelopmentRegion", bundleProperties.getCFBundleDevelopmentRegion(), dict);

		// Bundle Package Type, required, defaults tp "APPL"
		writeKeyStringPair("CFBundlePackageType", bundleProperties.getCFBundlePackageType(), dict);

		// Bundle Signature, required, defaults tp "????"
		writeKeyStringPair("CFBundleSignature", bundleProperties.getCFBundleSignature(), dict);

		// Application build number, optional
		if (bundleProperties.getCFBundleVersion() != null)
			writeKeyStringPair("CFBundleVersion", bundleProperties.getCFBundleVersion(), dict);

		// Application Icon file, optional
		if (bundleProperties.getCFBundleIconFile() != null)
			writeKeyStringPair("CFBundleIconFile", bundleProperties.getCFBundleIconFile(), dict);

		// Bundle Identifier, optional
		if (bundleProperties.getCFBundleIdentifier() != null)
			writeKeyStringPair("CFBundleIdentifier", bundleProperties.getCFBundleIdentifier(), dict);

		// Help Book Folder, optional
		if (bundleProperties.getCFBundleHelpBookFolder() != null)
			writeKeyStringPair("CFBundleHelpBookFolder", bundleProperties.getCFBundleHelpBookFolder(), dict);

		// Help Book Name, optional
		if (bundleProperties.getCFBundleHelpBookName() != null)
			writeKeyStringPair("CFBundleHelpBookName", bundleProperties.getCFBundleHelpBookName(), dict);

		// Copyright, optional
		if(bundleProperties.getNSHumanReadableCopyright() != null)
			writeKeyStringPair("NSHumanReadableCopyright", bundleProperties.getNSHumanReadableCopyright(), dict);

		// IsAgent, optional
		if ( bundleProperties.getLSUIElement() != null )
			writeKeyBooleanPair( "LSUIElement", bundleProperties.getLSUIElement(), dict );

		// Document Types, optional
		List documentTypes = bundleProperties.getDocumentTypes();

		if (documentTypes.size() > 0)
 			writeDocumentTypes(documentTypes, dict);

		// Java entries in the plist dictionary
		if (bundleProperties.getJavaVersion() < 1.7) {
			// Apple Java Version

			writeKey("Java", dict);
			Node javaDict = createNode("dict", dict);

			// Main class, required
			writeKeyStringPair("MainClass", bundleProperties.getMainClass(), javaDict);

			// Target JVM version, optional but recommended
			if (bundleProperties.getJVMVersion() != null)
				writeKeyStringPair("JVMVersion", bundleProperties.getJVMVersion(), javaDict);

			// New in JarBundler 2.2.0; Tobias Bley ---------------------------------

			// JVMArchs, optional
			List jvmArchs = bundleProperties.getJVMArchs();

			if (jvmArchs != null && !jvmArchs.isEmpty())
				writeJVMArchs(jvmArchs, javaDict);

			// lsArchitecturePriority, optional
			List lsArchitecturePriority = bundleProperties.getLSArchitecturePriority();

			if (lsArchitecturePriority != null && !lsArchitecturePriority.isEmpty())
				writeLSArchitecturePriority(lsArchitecturePriority, javaDict);

			//-----------------------------------------------------------------------


			// Classpath is composed of two types, required
			// 1: Jars bundled into the JAVA_ROOT of the application
			// 2: External directories or files with an absolute path

			List classPath = bundleProperties.getClassPath();
			List extraClassPath = bundleProperties.getExtraClassPath();

			if ((classPath.size() > 0) || (extraClassPath.size() > 0))
				writeClasspath(classPath, extraClassPath, javaDict);


			// JVM options, optional
			if (bundleProperties.getVMOptions() != null)
				writeKeyStringPair("VMOptions", bundleProperties.getVMOptions(), javaDict);

			// Working directory, optional
			if (bundleProperties.getWorkingDirectory() != null)
				writeKeyStringPair("WorkingDirectory", bundleProperties.getWorkingDirectory(), javaDict);

			// StartOnMainThread, optional
			if (bundleProperties.getStartOnMainThread() != null) {
				writeKey("StartOnMainThread", javaDict);
				createNode(bundleProperties.getStartOnMainThread().toString(), javaDict);
			}

			// SplashFile, optional
			if (bundleProperties.getSplashFile() != null)
				writeKeyStringPair("SplashFile", bundleProperties.getSplashFile(), javaDict);

			// Main class arguments, optional
			if (bundleProperties.getArguments() != null)
				writeKeyStringPair("Arguments", bundleProperties.getArguments(), javaDict);

			// Java properties, optional
			Hashtable javaProperties = bundleProperties.getJavaProperties();

			if (javaProperties.isEmpty() == false)
				writeJavaProperties(javaProperties, javaDict);
		} else {
			// Oracle Java Version

			// Main class, required
			writeKeyStringPair("JVMMainClassName", bundleProperties.getMainClass(), dict);

			// Main class arguments, optional
			if (bundleProperties.getArguments() != null) {
				writeKey("JVMArguments", dict);
				writeArray(Arrays.asList(bundleProperties.getArguments().split("\\s+")), dict);
			}

			// JVM options and Java properties, optional
			if ((bundleProperties.getVMOptions() != null) || !bundleProperties.getJavaProperties().isEmpty()) {
				writeKey("JVMOptions", dict);

				List<String> jvmOptions = new ArrayList<String>();

				if (bundleProperties.getVMOptions() != null)
					jvmOptions.addAll(Arrays.asList(bundleProperties.getVMOptions().split("\\s+")));

				Iterator javaPropertiesIterator = bundleProperties.getJavaProperties().entrySet().iterator();
				while (javaPropertiesIterator.hasNext())
				{
					Map.Entry entry = (Map.Entry) javaPropertiesIterator.next();
					if (((String) entry.getKey()).startsWith("com.apple.")) {
						System.out.println("Deprecated as of 1.4: " + entry.getKey());
						continue;
					}
					jvmOptions.add("-D" + entry.getKey() + '=' + entry.getValue());
				}

				writeArray(jvmOptions, dict);
			}

		}

		// Services, optional
		List services = bundleProperties.getServices();
		if (services.size() > 0)
 			writeServices(services,dict);

	}


	private void writeDocumentTypes(List documentTypes, Node appendTo) {

		writeKey("CFBundleDocumentTypes", appendTo);

		Node array = createNode("array", appendTo);

		Iterator itor = documentTypes.iterator();

		while (itor.hasNext()) {

			DocumentType documentType = (DocumentType) itor.next();

			Node documentDict = createNode("dict", array);

			writeKeyStringPair("CFBundleTypeName", documentType.getName(), documentDict);
			writeKeyStringPair("CFBundleTypeRole", documentType.getRole(), documentDict);

			File iconFile = documentType.getIconFile();

			if (iconFile != null)
				writeKeyStringPair("CFBundleTypeIconFile", iconFile.getName(), documentDict);


			List extensions = documentType.getExtensions();

			if (extensions.isEmpty() == false) {
				writeKey("CFBundleTypeExtensions", documentDict);
				writeArray(extensions, documentDict);
			}

			List osTypes = documentType.getOSTypes();

			if (osTypes.isEmpty() == false) {
				writeKey("CFBundleTypeOSTypes", documentDict);
				writeArray(osTypes, documentDict);
			}


			List mimeTypes = documentType.getMimeTypes();

			if (mimeTypes.isEmpty() == false) {
				writeKey("CFBundleTypeMIMETypes", documentDict);
				writeArray(mimeTypes, documentDict);
			}

			List UTIs = documentType.getUTIs();

			if (UTIs.isEmpty() == false) {
				writeKey("LSItemContentTypes", documentDict);
				writeArray(UTIs, documentDict);
			}

			// Only write this key if true
			if (documentType.isBundle())
				writeKeyStringPair("LSTypeIsPackage", "true", documentDict);
		}
	}

	private void writeServices(List services, Node appendTo) {

		writeKey("NSServices",appendTo);
		Node array = createNode("array",appendTo);
		Iterator itor = services.iterator();

		while (itor.hasNext()) {
			Service service = (Service)itor.next();
			Node serviceDict = createNode("dict",array);

			String portName = service.getPortName();
            if (portName == null)
            	portName = bundleProperties.getCFBundleName();

			writeKeyStringPair("NSPortName", portName, serviceDict);
			writeKeyStringPair("NSMessage",service.getMessage(),serviceDict);

			List sendTypes = service.getSendTypes();
			if (!sendTypes.isEmpty()) {
				writeKey("NSSendTypes",serviceDict);
				writeArray(sendTypes,serviceDict);
			}

			List returnTypes = service.getReturnTypes();
			if (!returnTypes.isEmpty()) {
				writeKey("NSReturnTypes",serviceDict);
				writeArray(returnTypes,serviceDict);
			}

			writeKey("NSMenuItem",serviceDict);
			Node menuItemDict = createNode("dict",serviceDict);
			writeKeyStringPair("default",service.getMenuItem(),menuItemDict);

			String keyEquivalent = service.getKeyEquivalent();
			if (null != keyEquivalent) {
				writeKey("NSKeyEquivalent",serviceDict);
				Node keyEquivalentDict = createNode("dict",serviceDict);
				writeKeyStringPair("default",keyEquivalent,keyEquivalentDict);
			}

			String userData = service.getUserData();
			if (null != userData)
				writeKeyStringPair("NSUserData", userData, serviceDict);

			String timeout = service.getTimeout();
			if (null != timeout)
				writeKeyStringPair("NSTimeout",timeout,serviceDict);
 		}
	}

	private void writeClasspath(List classpath, List extraClasspath, Node appendTo) {
		writeKey("ClassPath", appendTo);
		classpath.addAll(extraClasspath);
		writeArray(classpath, appendTo);
	}


	private void writeJavaProperties(Hashtable javaProperties, Node appendTo) {

		writeKey("Properties", appendTo);

		Node propertiesDict = createNode("dict", appendTo);

		for (Iterator i = javaProperties.keySet().iterator(); i.hasNext();) {
			String key = (String) i.next();

			if (key.startsWith("com.apple.") && (bundleProperties.getJavaVersion() >= 1.4)) {
				System.out.println("Deprecated as of 1.4: " + key);
				continue;
			}

			writeKeyStringPair(key, (String)javaProperties.get(key), propertiesDict);
		}
	}

    // New in JarBundler 2.2.0; Tobias Bley ---------------------------------

    private void writeJVMArchs(List jvmArchs, Node appendTo)
    {
        writeKey("JVMArchs", appendTo);
        writeArray(jvmArchs, appendTo);
    }

    private void writeLSArchitecturePriority(List lsArchitecturePriority, Node appendTo)
    {
        writeKey("LSArchitecturePriority", appendTo);
        writeArray(lsArchitecturePriority, appendTo);
    }

    //----------------------------------------------------------------------

    private Node createNode(String tag, Node appendTo)
    {
		Node node = this.document.createElement(tag);
		appendTo.appendChild(node);
		return node;
	}


	private void writeKeyStringPair(String key, String string, Node appendTo) {

		if (string == null)
			return;

		writeKey(key, appendTo);
		writeString(string, appendTo);
	}


	private void writeKeyBooleanPair(String key, Boolean b, Node appendTo) {

		if ( b == null )
			return;

		writeKey( key, appendTo );
		writeBoolean( b, appendTo );
	}


	private void writeKey(String key, Node appendTo) {
		Element keyNode = this.document.createElement("key");
		appendTo.appendChild(keyNode);
		keyNode.appendChild(this.document.createTextNode(key));
	}


	private void writeString(String string, Node appendTo) {
		Element stringNode = this.document.createElement("string");
		stringNode.appendChild(this.document.createTextNode(string));
		appendTo.appendChild(stringNode);
	}

	private void writeArray(List stringList, Node appendTo) {

		Node arrayNode = createNode("array", appendTo);

		for (Iterator it = stringList.iterator(); it.hasNext();)
			writeString((String)it.next(), arrayNode);

	}

	private void writeBoolean( Boolean b, Node appendTo ) {
		Element booleanNode = null;

		if ( b.booleanValue() ) {
			booleanNode = this.document.createElement( "true" );
		}
		else {
			booleanNode = this.document.createElement( "false" );

		}
		appendTo.appendChild( booleanNode );
	}
}
