/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jdt.internal.ui.macbundler;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.eclipse.core.runtime.IProgressMonitor;
import org.w3c.dom.Document;
import org.w3c.dom.Element;


public class BundleBuilder implements BundleAttributes {
	
	private List<Process> fProcesses= new ArrayList<Process>();
	private BundleDescription fBundleDescription;
	
	
	/**
	 * Create a new bundle
	 * @param bd the new description
	 * @param pm progress monitor
	 * @throws IOException if something happens
	 */
	public void createBundle(BundleDescription bd, IProgressMonitor pm) throws IOException {
		
		fBundleDescription= bd;
		
		File tmp_dir= new File(bd.get(DESTINATIONDIRECTORY));
		String app_dir_name= bd.get(APPNAME) + ".app";	//$NON-NLS-1$
		File app_dir= new File(tmp_dir, app_dir_name);
		if (app_dir.exists()) {
			deleteDir(app_dir);
		}
		app_dir= createDir(tmp_dir, app_dir_name, false);
		
		File contents_dir= createDir(app_dir, "Contents", false);	//$NON-NLS-1$
		createPkgInfo(contents_dir);

		File macos_dir= createDir(contents_dir, "MacOS", false);	//$NON-NLS-1$
		String launcher_path= bd.get(LAUNCHER);
		if (launcher_path == null) {
			throw new IOException();
		}		
		String launcher= copyFile(macos_dir, launcher_path, null);
		
		File resources_dir= createDir(contents_dir, "Resources", false);	//$NON-NLS-1$
		File java_dir= createDir(resources_dir, "Java", false);	//$NON-NLS-1$
				
		createInfoPList(contents_dir, resources_dir, java_dir, launcher);
		
		Iterator<Process> iter= fProcesses.iterator();
		while (iter.hasNext()) {
			Process p= iter.next();
			try {
				p.waitFor();
			} catch (InterruptedException e) {
				// silently ignore
			}
		}		
	}
	
	private void createInfoPList(File contents_dir, File resources_dir, File java_dir, String launcher) throws IOException {
		DocumentBuilder docBuilder= null;
		DocumentBuilderFactory factory= DocumentBuilderFactory.newInstance();
		factory.setValidating(false);
		try {   	
			docBuilder= factory.newDocumentBuilder();
		} catch (ParserConfigurationException ex) {
			System.err.println("createInfoPList: could not get XML builder"); //$NON-NLS-1$
			throw new IOException("Could not get XML builder"); //$NON-NLS-1$
		}
		Document doc= docBuilder.newDocument();
		
		Element plist= doc.createElement("plist"); //$NON-NLS-1$
		doc.appendChild(plist);
		plist.setAttribute("version", "1.0"); //$NON-NLS-1$ //$NON-NLS-2$
		
		Element dict= doc.createElement("dict"); //$NON-NLS-1$
		plist.appendChild(dict);
		
		pair(dict, "CFBundleExecutable", null, launcher); //$NON-NLS-1$
		pair(dict, "CFBundleGetInfoString", GETINFO, null); //$NON-NLS-1$
		pair(dict, "CFBundleInfoDictionaryVersion", null, "6.0"); //$NON-NLS-1$ //$NON-NLS-2$
		
		String iconName= null;
		String appName= fBundleDescription.get(APPNAME, null);
		if (appName != null)
		 {
			iconName= appName + ".icns"; //$NON-NLS-1$
		}
		String fname= copyFile(resources_dir, fBundleDescription.get(ICONFILE, null), iconName);
		if (fname != null)
		 {
			pair(dict, "CFBundleIconFile", null, fname); //$NON-NLS-1$
		}
		
		pair(dict, "CFBundleIdentifier", IDENTIFIER, null); //$NON-NLS-1$
		pair(dict, "CFBundleName", APPNAME, null); //$NON-NLS-1$
		pair(dict, "CFBundlePackageType", null, "APPL"); //$NON-NLS-1$ //$NON-NLS-2$
		pair(dict, "CFBundleShortVersionString", VERSION, null); //$NON-NLS-1$
		pair(dict, "CFBundleSignature", SIGNATURE, "????"); //$NON-NLS-1$ //$NON-NLS-2$
		pair(dict, "CFBundleVersion", null, "1.0.1"); //$NON-NLS-1$ //$NON-NLS-2$
		
		Element jdict= doc.createElement("dict"); //$NON-NLS-1$
		add(dict, "Java", jdict); //$NON-NLS-1$
		
		pair(jdict, "JVMVersion", JVMVERSION, null); //$NON-NLS-1$
		pair(jdict, "MainClass", MAINCLASS, null); //$NON-NLS-1$
		pair(jdict, "WorkingDirectory", WORKINGDIR, null); //$NON-NLS-1$
		
		if (fBundleDescription.get(USES_SWT, false))
		 {
			addTrue(jdict, "StartOnMainThread"); //$NON-NLS-1$
		}
		
		String arguments= fBundleDescription.get(ARGUMENTS, null);
		if (arguments != null) {
			Element argArray= doc.createElement("array");	//$NON-NLS-1$
			add(jdict, "Arguments", argArray);	//$NON-NLS-1$
			StringTokenizer st= new StringTokenizer(arguments);	
			while (st.hasMoreTokens()) {
				String arg= st.nextToken();
				Element type= doc.createElement("string"); //$NON-NLS-1$
				argArray.appendChild(type);	
				type.appendChild(doc.createTextNode(arg));			
			}
		}
		
		pair(jdict, "VMOptions", VMOPTIONS, null); //$NON-NLS-1$
		
		int[] id= new int[] { 0 };
		ResourceInfo[] ris= fBundleDescription.getResources(true);
		if (ris.length > 0) {
			StringBuilder cp= new StringBuilder();
			for (int i= 0; i < ris.length; i++) {
				ResourceInfo ri= ris[i];
				String e= processClasspathEntry(java_dir, ri.fPath, id);
				if (cp.length() > 0) {
					cp.append(':');
				}
				cp.append(e);
			}
			add(jdict, "ClassPath", cp.toString()); //$NON-NLS-1$
		}

		ris= fBundleDescription.getResources(false);
		if (ris.length > 0) {
			for (int i= 0; i < ris.length; i++) {
				ResourceInfo ri= ris[i];
				processClasspathEntry(java_dir, ri.fPath, id);
			}
		}

		File info= new File(contents_dir, "Info.plist"); //$NON-NLS-1$
		try (FileOutputStream fos = new FileOutputStream(info); BufferedOutputStream fOutputStream = new BufferedOutputStream(fos);) {
			// Write the document to the stream
			Transformer transformer= TransformerFactory.newInstance().newTransformer();
			transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "-//Apple Computer//DTD PLIST 1.0//EN"); //$NON-NLS-1$
			transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://www.apple.com/DTDs/PropertyList-1.0.dtd"); //$NON-NLS-1$
 			transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
			transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); //$NON-NLS-1$
			transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
			transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4"); //$NON-NLS-1$ //$NON-NLS-2$
			DOMSource source= new DOMSource(doc);
			StreamResult result= new StreamResult(fOutputStream);
			transformer.transform(source, result);
		} catch (TransformerException e) {
			System.err.println("createInfoPList: could not transform to XML"); //$NON-NLS-1$
		}
	}
	
	private void add(Element dict, String key, Element value) {
		Document document= dict.getOwnerDocument();
		Element k= document.createElement("key"); //$NON-NLS-1$
		dict.appendChild(k);
		k.appendChild(document.createTextNode(key));
		dict.appendChild(value);
	}
	
	private void create(Element parent, String s) {
		Document document= parent.getOwnerDocument();
		Element type= document.createElement("string"); //$NON-NLS-1$
		parent.appendChild(type);	
		type.appendChild(document.createTextNode(s));
	}

	private void createTrue(Element parent) {
		Document document= parent.getOwnerDocument();
		Element type= document.createElement("true"); //$NON-NLS-1$
		parent.appendChild(type);	
	}

	private void add(Element dict, String key, String value) {
		Document document= dict.getOwnerDocument();
		Element k= document.createElement("key"); //$NON-NLS-1$
		dict.appendChild(k);
		k.appendChild(document.createTextNode(key));
		create(dict, value);
	}
	
	private void addTrue(Element dict, String key) {
		Document document= dict.getOwnerDocument();
		Element k= document.createElement("key"); //$NON-NLS-1$
		dict.appendChild(k);
		k.appendChild(document.createTextNode(key));
		createTrue(dict);
	}
	
	private void pair(Element dict, String outkey, String inkey, String dflt) {
		String value= null;
		if (inkey != null) {
			value= fBundleDescription.get(inkey, dflt);
		} else {
			value= dflt;
		}
		if (value != null && value.trim().length() > 0) {
			add(dict, outkey, value);
		}
	}
	
	private String processClasspathEntry(File java_dir, String name, int[] id_ref) throws IOException {
		File f= new File(name);
		if (f.isDirectory()) {
			int id= id_ref[0]++;
			String archivename= "jar_" + id + ".jar"; //$NON-NLS-1$ //$NON-NLS-2$
			File to= new File(java_dir, archivename);
			zip(name, to.getAbsolutePath());
			name= archivename;
		} else {
			name= copyFile(java_dir, name, null);
		}
		return "$JAVAROOT/" + name; //$NON-NLS-1$
	}
	
	private void createPkgInfo(File contents_dir) throws IOException {
		File pkgInfo= new File(contents_dir, "PkgInfo"); //$NON-NLS-1$
		try (FileOutputStream os = new FileOutputStream(pkgInfo)) {
			os.write(("APPL" + fBundleDescription.get(SIGNATURE, "????")).getBytes()); //$NON-NLS-1$ //$NON-NLS-2$
		}
	}
		
	private static void deleteDir(File dir) {
		File[] files= dir.listFiles();
		if (files != null) {
			for (int i= 0; i < files.length; i++) {
				deleteDir(files[i]);
			}
		}
		dir.delete();
	}
	
	private File createDir(File parent_dir, String dir_name, boolean remove) throws IOException {
		File dir= new File(parent_dir, dir_name);
		if (dir.exists()) {
			if (!remove) {
				return dir;
			}
			deleteDir(dir);
		}
		if (! dir.mkdir())
		 {
			throw new IOException("cannot create dir " + dir_name); //$NON-NLS-1$
		}
		return dir;
	}
	
	private String copyFile(File todir, String fromPath, String toname) throws IOException {
		if (toname == null) {
			int pos= fromPath.lastIndexOf('/');
			if (pos >= 0) {
				toname= fromPath.substring(pos+1);
			} else {
				toname= fromPath;
			}
		}
		File to= new File(todir, toname);
		fProcesses.add(Runtime.getRuntime().exec(new String[] { "/bin/cp", fromPath, to.getAbsolutePath() }));	//$NON-NLS-1$
		return toname;
	}

	private void zip(String dir, String dest) throws IOException {
		fProcesses.add(Runtime.getRuntime().exec(new String[] { "/usr/bin/jar", "cf", dest, "-C", dir, "." })); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
	}
}
