/************************************************************************
 *
 *  ExternalApps.java
 *
 *  Copyright: 2002-2018 by Henrik Just
 *
 *  This file is part of Writer2LaTeX.
 *  
 *  Writer2LaTeX 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 3 of the License, or
 *  (at your option) any later version.
 *  
 *  Writer2LaTeX is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with Writer2LaTeX.  If not, see <http://www.gnu.org/licenses/>.
 * 
 *  Version 1.6 (2018-03-06)
 *
 */ 
 
package org.openoffice.da.comp.writer2latex;

import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.lang.Process;
import java.lang.ProcessBuilder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import org.openoffice.da.comp.w2lcommon.helper.RegistryHelper;
import org.openoffice.da.comp.w2lcommon.helper.StreamGobbler;
import org.openoffice.da.comp.w2lcommon.helper.XPropertySetHelper;

import com.sun.star.beans.XMultiHierarchicalPropertySet;
import com.sun.star.beans.XPropertySet;
import com.sun.star.uno.UnoRuntime;
import com.sun.star.uno.XComponentContext;
import com.sun.star.util.XChangesBatch;

       
/** This class manages and executes external applications used by the Writer2LaTeX toolbar.
 *  These include TeX and friends as well as viewers for the various backend formats.
 *  The registry is used for persistent storage of the settings.
 */  
public class ExternalApps {
	
	public final static short EXPORT = (short)0;
	public final static short BUILD = (short)1;
	public final static short PREVIEW = (short)2;
	
    public final static String LATEX = "LaTeX"; //$NON-NLS-1$
    public final static String PDFLATEX = "PdfLaTeX"; //$NON-NLS-1$
    public final static String XELATEX = "XeLaTeX"; //$NON-NLS-1$
    public final static String BIBTEX = "BibTeX"; //$NON-NLS-1$
    public final static String MAKEINDEX = "Makeindex"; //$NON-NLS-1$
    public final static String MK4HT = "Mk4ht"; //$NON-NLS-1$
    public final static String DVIPS = "Dvips"; //$NON-NLS-1$
    public final static String DVIVIEWER = "DVIViewer"; //$NON-NLS-1$
    public final static String POSTSCRIPTVIEWER = "PostscriptViewer"; //$NON-NLS-1$
    public final static String PDFVIEWER = "PdfViewer"; //$NON-NLS-1$
	
    private final static String[] sApps = { LATEX, PDFLATEX, XELATEX, BIBTEX, MAKEINDEX, MK4HT, DVIPS, DVIVIEWER, POSTSCRIPTVIEWER, PDFVIEWER };
	
    private XComponentContext xContext;

    private short nLevel = (short)2;
    
    private Map<String,String[]> apps;
    private Set<String> defaultApps;
	
    /** Construct a new ExternalApps object with empty content */
    public ExternalApps(XComponentContext xContext) {
        this.xContext = xContext;
        apps = new HashMap<String,String[]>();
        defaultApps = new HashSet<String>();
        for (int i=0; i<sApps.length; i++) {
            setApplication(sApps[i], "", ""); //$NON-NLS-1$ //$NON-NLS-2$
           	setUseDefaultApplication(sApps[i],true);
        }
    }
    
    /** Return the localized name for an external app to use in the UI (only the viewers has a separate UI name)
     * 
     * @param sName the app name
     * @return the UI name
     */
    public static String getUIAppName(String sName) {
    	if (DVIVIEWER.equals(sName)) {
    		return Messages.getString("ExternalApps.dviviewer"); //$NON-NLS-1$
    	}
    	else if (PDFVIEWER.equals(sName)) {
    		return Messages.getString("ExternalApps.pdfviewer"); //$NON-NLS-1$
    	}
    	else if (POSTSCRIPTVIEWER.equals(sName)) {
    		return Messages.getString("ExternalApps.psviewer"); //$NON-NLS-1$
    	}
    	return sName;
    }
    
    /** Set the desired processing level (0: export only, 1: export and build, 2: export, build and preview)
     * 
     * @param nLevel the desired level
     */
    public void setProcessingLevel(short nLevel) {
    	this.nLevel = nLevel;
    }
    
    /** Get the desired processing level (0: export only, 1: export and build, 2: export, build and preview)
     * 
     * @return the level
     */
    public short getProcessingLevel() {
    	return nLevel;
    }
    
    public boolean isViewer(String sAppName) {
    	return sAppName!=null && sAppName.endsWith("Viewer"); //$NON-NLS-1$
    }
	
    /** Define an external application
     *  @param sAppName the name of the application to define
     *  @param sExecutable the system dependent path to the executable file
     *  @param sOptions the options to the external application; %s will be
     *  replaced by the filename on execution 
     */
    public void setApplication(String sAppName, String sExecutable, String sOptions) {
        String[] sValue = { sExecutable, sOptions };
        apps.put(sAppName, sValue);
    }
    
    /** Get the definition for an external application
     *  @param sAppName the name of the application to get
     *  @return a String array containing the system dependent path to the
     *  executable file as entry 0 and the parameters as entry 1
     *  returns null if the application is unknown
     */
    public String[] getApplication(String sAppName) {
        return apps.get(sAppName);
    } 
    
    /** Define to use the system's default for an external application. This is only possible if the application is a viewer,
     *  otherwise setting the value to true will be ignored
     *  @param sAppName the name of the application
     *  @param bUseDefault flag defining whether or not to use the default
     */
    public void setUseDefaultApplication(String sAppName, boolean bUseDefault) {
    	if (bUseDefault && isViewer(sAppName)) {
    		defaultApps.add(sAppName);
    	}
    	else if (defaultApps.contains(sAppName)) {
    		defaultApps.remove(sAppName);
    	}
    }
	
    /** Get the setting to use the system's default application
     * 
     * @param sAppName the name of the application
     * @return true if the system's default should be used, false if not or if the application is unknown
     */
    public boolean getUseDefaultApplication(String sAppName) {
    	return defaultApps.contains(sAppName);
    }
    
    /** Execute an external application
     *  @param sAppName the name of the application to execute (ignored for default apps)
     *  @param sFileName the file name to use
     *  @param workDir the working directory to use
     *  @param env map of environment variables to set (or null if no variables needs to be set, ignored for default apps)
     *  @param bWaitFor true if the method should wait for the execution to finish (ignored for default apps)
     *  @return error code 
     */
    public int execute(String sAppName, String sFileName, File workDir, Map<String,String> env, boolean bWaitFor) {
    	if (defaultApps.contains(sAppName)) {
    		return openWithDefaultApplication(new File(sFileName)) ? 0 : 1;
    	}
    	else {
    		return execute(sAppName, "", sFileName, workDir, env, bWaitFor); //$NON-NLS-1$
    	}
    }
    
    // Open the file in the default application on this system (if any)
    private boolean openWithDefaultApplication(File file) {
        if (Desktop.isDesktopSupported()) {
            Desktop desktop = Desktop.getDesktop();
            try {
				desktop.open(file);
				return true;
			} catch (IOException e) {
				System.err.println(e.getMessage());
			}
        }
        return false;
    }

	
    /** Execute an external application
     *  @param sAppName the name of the application to execute
     *  @param sCommand subcommand/option to pass to the command
     *  @param sFileName the file name to use
     *  @param workDir the working directory to use
     *  @param env map of environment variables to set (or null if no variables needs to be set)
     *  @param bWaitFor true if the method should wait for the execution to finish
     *  @return error code 
     */
    public int execute(String sAppName, String sCommand, String sFileName, File workDir, Map<String,String> env, boolean bWaitFor) {
        // Assemble the command
        String[] sApp = getApplication(sAppName);
        if (sApp==null) { return 1; }
 
        try {
			Vector<String> command = new Vector<String>();
			command.add(sApp[0]);
			String[] sArguments = sApp[1].split(" "); //$NON-NLS-1$
			for (String s : sArguments) {
				command.add(s.replace("%c",sCommand).replace("%s",sFileName)); //$NON-NLS-1$ //$NON-NLS-2$
			}
			
            ProcessBuilder pb = new ProcessBuilder(command);
            pb.directory(workDir);
            if (env!=null) {
            	pb.environment().putAll(env);
            }
            Process proc = pb.start();        
        
            // Gobble the error stream of the application
            StreamGobbler errorGobbler = new 
                StreamGobbler(proc.getErrorStream(), "ERROR");             //$NON-NLS-1$
            
            // Gobble the output stream of the application
            StreamGobbler outputGobbler = new 
                StreamGobbler(proc.getInputStream(), "OUTPUT"); //$NON-NLS-1$
                
            // Kick them off
            errorGobbler.start();
            outputGobbler.start();
                                    
            // Any error?
            return bWaitFor ? proc.waitFor() : 0;
        }
        catch (InterruptedException e) {
            return 1;
        }
        catch (IOException e) {
            return 1;
        }
    }
	
    /** Load the external applications from the registry
     */
    public void load() {
    	RegistryHelper registry = new RegistryHelper(xContext);
    	Object view;
    	try {
    		// Prepare registry view
    		view = registry.getRegistryView("/org.openoffice.da.Writer2LaTeX.toolbar.ToolbarOptions/Applications",false); //$NON-NLS-1$
    	}
        catch (com.sun.star.uno.Exception e) {
            // Give up...
            return;
        }

		XPropertySet xSimpleProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class,view);
		nLevel = XPropertySetHelper.getPropertyValueAsShort(xSimpleProps,"AfterExport"); //$NON-NLS-1$

		XMultiHierarchicalPropertySet xProps = (XMultiHierarchicalPropertySet)
            UnoRuntime.queryInterface(XMultiHierarchicalPropertySet.class, view);
        for (int i=0; i<sApps.length; i++) {
            String[] sNames = new String[3];
            sNames[0] = sApps[i]+"/Executable"; //$NON-NLS-1$
            sNames[1] = sApps[i]+"/Options"; //$NON-NLS-1$
            sNames[2] = sApps[i]+"/UseDefault"; //$NON-NLS-1$
            try {
                Object[] values = xProps.getHierarchicalPropertyValues(sNames);
                setApplication(sApps[i], (String) values[0], (String) values[1]);
                setUseDefaultApplication(sApps[i], ((Boolean) values[2]).booleanValue());
            }
            catch (com.sun.star.uno.Exception e) {
                // Ignore...
            }
        }
		
        registry.disposeRegistryView(view);
    }
	
    /** Save the external applications to the registry
     */
    public void save() {
    	RegistryHelper registry = new RegistryHelper(xContext);
        Object view;
        try {
    		view = registry.getRegistryView("/org.openoffice.da.Writer2LaTeX.toolbar.ToolbarOptions/Applications",true); //$NON-NLS-1$
        }
        catch (com.sun.star.uno.Exception e) {
            // Give up...
            return;
        }
        
		XPropertySet xSimpleProps = (XPropertySet) UnoRuntime.queryInterface(XPropertySet.class,view);
		XPropertySetHelper.setPropertyValue(xSimpleProps, "AfterExport", nLevel); //$NON-NLS-1$

        XMultiHierarchicalPropertySet xProps = (XMultiHierarchicalPropertySet)
            UnoRuntime.queryInterface(XMultiHierarchicalPropertySet.class, view);
        for (int i=0; i<sApps.length; i++) {
            String[] sNames = new String[3];
            sNames[0] = sApps[i]+"/Executable"; //$NON-NLS-1$
            sNames[1] = sApps[i]+"/Options"; //$NON-NLS-1$
            sNames[2] = sApps[i]+"/UseDefault"; //$NON-NLS-1$
            String[] sApp = getApplication(sApps[i]);
            boolean bUseDefault = getUseDefaultApplication(sApps[i]);
            Object[] values = { sApp[0], sApp[1], new Boolean(bUseDefault) }; 
            try {
                xProps.setHierarchicalPropertyValues(sNames, values);
            }
            catch (com.sun.star.uno.Exception e) {
                // Ignore...
            }
        }
		
        // Commit registry changes
        XChangesBatch  xUpdateContext = (XChangesBatch)
            UnoRuntime.queryInterface(XChangesBatch.class, view);
        try {
            xUpdateContext.commitChanges();
        }
        catch (Exception e) {
            // ignore
        }

        registry.disposeRegistryView(view);
    }
	
}