/*******************************************************************************
 * Copyright (c) 2017 the TeXlipse team and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     The TeXlipse team - initial API and implementation
 *******************************************************************************/
package org.eclipse.texlipse.builder;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.texlipse.PathUtils;
import org.eclipse.texlipse.TexlipsePlugin;
import org.eclipse.texlipse.properties.TexlipseProperties;
import org.eclipse.ui.texteditor.MarkerUtilities;


/**
 * Helper methods for external program runners.
 * 
 * @author Kimmo Karlsson
 */
public abstract class AbstractProgramRunner implements ProgramRunner {
    
    // the currently running program
    private ExternalProgram extrun;
    
    /**
     * Create a new program runner.
     * @param project the project holding the properties
     * @param propertyName name of the property containing the name of the program
     */
    protected AbstractProgramRunner() {
        extrun = new ExternalProgram();
    }

    /**
     * @return the name of the program runner arguments -preference in the plugin preferences
     */
    private String getArgumentsPreferenceName() {
        return getClass() + "_args";
    }
    
    /**
     * @return the name of the program runner path -preference in the plugin preferences
     */
    private String getCommandPreferenceName() {
        return getClass() + "_prog";
    }
    
    /**
     * @return the program path and filename from the preferences
     */
    public String getProgramPath() {
        return TexlipsePlugin.getPreference(getCommandPreferenceName());
    }
    
    /**
     * @param path the program path and filename for the preferences
     */
    public void setProgramPath(String path) {
        TexlipsePlugin.getDefault().getPreferenceStore().setValue(getCommandPreferenceName(), path);
    }
    
    /**
     * Read the command line arguments for the program from the preferences.
     * The input filename is marked with a "%input" and the output file name
     * is marked with a "%output".
     * @return the command line arguments for the program
     */
    public String getProgramArguments() {
        return TexlipsePlugin.getPreference(getArgumentsPreferenceName());
    }
    
    /**
     * @param args the program arguments for the preferences
     */
    public void setProgramArguments(String args) {
        TexlipsePlugin.getDefault().getPreferenceStore().setValue(getArgumentsPreferenceName(), args);
    }
    
    /**
     *
     */
    public void initializeDefaults(IPreferenceStore pref, String path) {
        pref.setDefault(getCommandPreferenceName(), path);
        pref.setDefault(getArgumentsPreferenceName(), getDefaultArguments());
    }
    
    /**
     * Returns the default value for the program arguments -preference.
     * @return the program arguments
     */
    protected String getDefaultArguments() {
        return "%input";
    }
    
    /**
     * @return the name of the executable program
     */
    public String getProgramName() {
        String os = System.getProperty("os.name").toLowerCase();
        if (os.indexOf("windows") >= 0) {
            return getWindowsProgramName();
        } else {
            return getUnixProgramName();
        }
    }
    
    protected abstract String getWindowsProgramName();
    protected abstract String getUnixProgramName();
    
    /**
     * @param resource the input file to be processed
     * @return the arguments to give to the external program
     */
    protected String getArguments(IResource resource) {
        String args = getProgramArguments();
        if (args == null) {
            return null;
        }
        String ext = resource.getFileExtension();
        String name = resource.getName();
        String baseName = name.substring(0, name.length() - ext.length());
        String inputName = baseName + getInputFormat();
        String outputName = baseName + getOutputFormat();
        if (baseName.indexOf(' ') >= 0) {
            inputName = "\"" + inputName + "\"";
            outputName = "\"" + outputName + "\"";
        }
        
        if (args.indexOf("%input") >= 0) {
            args = args.replaceAll("%input", inputName);
        }
        if (args.indexOf("%output") >= 0) {
            args = args.replaceAll("%output", outputName);
        }
        if (args.indexOf("%fullinput") >= 0) {
            args = args.replaceAll("%fullinput",
                    resource.getParent().getLocation().toFile().getAbsolutePath()
                    + File.separator + inputName);
        }
        if (args.indexOf("%fulloutput") >= 0) {
            args = args.replaceAll("%fulloutput",
                    resource.getParent().getLocation().toFile().getAbsolutePath()
                    + File.separator + outputName);
        }
        return args;
    }
    
    /**
     * Parse errors from the output of an external program.
     * 
     * @param resource the input file that was processed
     * @param output the output of the external program
     * @return true, if error messages were found in the output, false otherwise
     */
    protected abstract boolean parseErrors(IResource resource, String output);
    
    /**
     * Check to see if this program is ready for operation.
     * @return true if this program exists
     */
    public boolean isValid() {
        if (getProgramPath() == null) {
            return false;
        }
        File f = new File(getProgramPath());
        return f.exists() && f.isFile();
    }
    
    /**
     * The main method.
     * 
     * @param resource the input file to feed to the external program
     * @throws CoreException if the external program is not found
     *                       or if there was an error during the build
     */
    public void run(IResource resource) throws CoreException {
        
        File sourceDir = resource.getLocation().toFile().getParentFile();
        
        // find executable file
        String programPath = getProgramPath();
        File exec = new File(programPath);
        if (!exec.exists()) {
            throw new CoreException(TexlipsePlugin.stat("External program (" + programPath + ") not found"));
        }
        
        // split command into array
        ArrayList list = new ArrayList();
        list.add(exec.getAbsolutePath());
        PathUtils.tokenizeEscapedString(getArguments(resource), list);
        String[] command =  (String[]) list.toArray(new String[0]);
        
        // check if we are using console
        String console = null;
        if (TexlipsePlugin.getDefault().getPreferenceStore().getBoolean(TexlipseProperties.BUILDER_CONSOLE_OUTPUT)) {
            console = getProgramName();
        }
        extrun.setup(command, sourceDir, console);
        
        String output = null;
        try {
            
            String[] query = getQueryString();
            if (query != null) {
                output = extrun.run(query);
            } else {
                output = extrun.run();
            }
            
        } catch (Exception e) {
            throw new CoreException(new Status(IStatus.ERROR, TexlipsePlugin.getPluginId(),
                    IStatus.ERROR, "Building the project: ", e));
        } finally {
            extrun.stop();
        }

        if (parseErrors(resource, output)) {
            throw new BuilderCoreException(TexlipsePlugin.stat("Errors during build. See the problems dialog."));
        }
    }

    /**
     * Kill the external program if it is running.
     */
    public void stop() {
        if (extrun != null) {
            extrun.stop();
        }
    }

    /**
     * Returns a special query string that indicates that this program is waiting an input from the user.
     * @return the query string to look for in the output of the program
     */
    protected String[] getQueryString() {
        return null;
    }
    
    /**
     * Create a layout warning marker to the given resource.
     *
     * @param resource the file where the problem occurred
     * @param message error message
     * @param lineNumber line number
     * @param markerType
     * @param severity Severity of the error
     */
    @SuppressWarnings("unchecked")
	protected static void createMarker(IResource resource, 
    		Integer lineNumber, String message, String markerType, int severity) {
    	int lineNr = -1;
    	if (lineNumber != null) {
    		lineNr = lineNumber;
    	}
    	IMarker marker = AbstractProgramRunner.findMarker(resource, lineNr, message, markerType);
    	if (marker == null) {
    		try {
    			HashMap map = new HashMap();
    			map.put(IMarker.MESSAGE, message);
    			map.put(IMarker.SEVERITY, new Integer (severity));

    			if (lineNumber != null)
    				map.put(IMarker.LINE_NUMBER, lineNumber);

    			MarkerUtilities.createMarker(resource, map, markerType);
    		} catch (CoreException e) {
    			throw new RuntimeException(e);
    		}
    	}
    }
    
    /**
     * Create a layout warning marker to the given resource.
     *
     * @param resource the file where the problem occured
     * @param message error message
     * @param lineNumber line number
     */
    public static void createLayoutMarker(IResource resource, Integer lineNumber, String message) {
        String markerType = TexlipseBuilder.LAYOUT_WARNING_TYPE;
        int severity = IMarker.SEVERITY_WARNING;
        createMarker(resource, lineNumber, message, markerType, severity);
    }
    
    /**
     * Create a marker to the given resource.
     * 
     * @param resource the file where the problem occured
     * @param message error message
     * @param lineNumber line number
     * @param severity severity of the marker
     */
    public static void createMarker(IResource resource, Integer lineNumber, String message, int severity) {
        String markerType = TexlipseBuilder.MARKER_TYPE;
        createMarker(resource, lineNumber, message, markerType, severity);
    }

    /**
     * Create a marker to the given resource. The marker's severity will be "ERROR".
     * 
     * @param resource the file where the problem occured
     * @param message error message
     * @param lineNumber line number
     */
    public static void createMarker(IResource resource, Integer lineNumber, String message) {
        createMarker(resource, lineNumber, message, IMarker.SEVERITY_ERROR);
    }
    
    /**
     * Checks pre-existance of marker.
     * 
     * @param resource Resource in which marker will searched
     * @param lineNr IMarker.LINE_NUMBER of the marker
     * @param message Message for marker
     * @param type The type of the marker to find
     * @return pre-existance of marker or null if no marker was found
     */
    public static IMarker findMarker(IResource resource, int lineNr, String message, String type) {
        
        try {
        	IMarker[] tasks = resource.findMarkers(type, true, IResource.DEPTH_ZERO);
            for (IMarker marker : tasks) {
            	Object lNrObj = marker.getAttribute(IMarker.LINE_NUMBER);
            	int lNr = -1;
            	if (lNrObj != null) {
            		lNr = ((Integer) lNrObj);
            	}
				if (lNr == lineNr
						&& marker.getAttribute(IMarker.MESSAGE).equals(message)) {
					return marker;
				}
					
			}
        } catch (CoreException e) {
            throw new RuntimeException(e);
        }
        return null;
    }
}