// Generator.java
//
//    Created: August 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.service.generator;

import org.tulsoft.shared.FileUtils;
import org.tulsoft.shared.GException;
import org.tulsoft.tools.external.Executor;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.CentralCached;
import org.biomoby.shared.Utils;
import org.biomoby.client.CentralDigestCachedImpl;

import org.apache.commons.lang.BooleanUtils;

import java.util.Properties;
import java.util.Map;
import java.util.regex.Pattern;
import java.io.File;

/**
 * A common superclass to all Moses generators. <p>
 *
 * It also contains property names (fields started by GPROP_) used by
 * data types and service instances generators. The descriptions of
 * the property names actually describe what will be the value of such
 * properties. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: Generator.java,v 1.6 2008/03/03 11:34:17 senger Exp $
 */

abstract public class Generator {

    protected CentralCached worker;
    protected boolean verbose = false;
    // to make some errors appear only the first time
    protected boolean dotErrorReported = false;

    // patterns that will be found in templates and replaced - compile
    // them only once (again, they are immutable, therefore thread-safe)
    static final Pattern P_DATE                   = Pattern.compile ("@DATE@");
    static final Pattern P_USER_OS                = Pattern.compile ("@USER_OS@");
    static final Pattern P_DESCRIPTION            = Pattern.compile ("@DESCRIPTION@");
    static final Pattern P_AUTHORITY              = Pattern.compile ("@AUTHORITY@");
    static final Pattern P_EMAIL_CONTACT          = Pattern.compile ("@EMAIL_CONTACT@");
    static final Pattern P_MEMBERS                = Pattern.compile ("@MEMBERS@");
    static final Pattern P_METHODS                = Pattern.compile ("@METHODS@");
    static final Pattern P_ARTICLE_NAME           = Pattern.compile ("@ARTICLE_NAME@");
    static final Pattern P_ESC_ARTICLE_NAME       = Pattern.compile ("@ESC_ARTICLE_NAME@");
    static final Pattern P_ESC_UPPER_ARTICLE_NAME = Pattern.compile ("@ESC_UPPER_ARTICLE_NAME@");
    static final Pattern P_DATATYPE               = Pattern.compile ("@DATATYPE@");
    static final Pattern P_IMAGESTART             = Pattern.compile ("@IMAGESTART@");
    static final Pattern P_IMAGEEND               = Pattern.compile ("@IMAGEEND@");
    static final Pattern P_IMAGEMAP               = Pattern.compile ("@IMAGEMAP@");

    /**
     * A regular expression that will be applied to a data type
     * name. Only those data types that match this expression will be
     * generated. If its value is null or an empty string filtering is
     * ignored.
     */
    public static final String GPROP_FILTER = "filter";

    /**
     * A name of a directory where a generator puts its generated
     * results. The generator may (and often does) create here further
     * hierarchical structure of subdirectories.
     */
    public static final String GPROP_OUTDIR = "outdir";

    /**
     * A regular expression that will be applied against service
     * authority names. Only those services that match this expression
     * will be generated (unless restriced also by a service name
     * filter - see {@link #GPROP_SERVICE}. If its value is null or an
     * empty string filtering is ignored.
     */
    public static final String GPROP_AUTH = "auth";

    /**
     * A regular expression that will be applied against service
     * names. Only those services that match this expression will be
     * generated (unless restriced by an authority name filter - see
     * {@link #GPROP_AUTH}. If its value is null or an empty string
     * filtering is ignored.
     */
    public static final String GPROP_SERVICE = "service";

    /**
     * Generator will print some messages if this property is set to
     * "true".
     */
    public static final String GPROP_VERBOSE = "verbose";

    /**
     * Data type generator will not produce any graphs if this
     * property is set to "true". Such graphs are supposed to be used
     * in the generated API documentation (by javadoc).
     */
    public static final String GPROP_NOGRAPHS = "nographs";

    /**
     * A path to a <a href="http://www.graphviz.org/"
     * target="_top">Graphviz</a> program <tt>dot</tt>. The program
     * can (unless property {@link #GPROP_NOGRAPHS} is set to
     * <tt>false</tt>) produce graphs showing place of a generated
     * data type in a tree of other data types. This property is
     * needed only if the <tt>dot</tt> program is not already on the
     * PATH.
     */
    public static final String GPROP_DOTLOCATION = "dot";

    /**
     * A debugging property: do not generate anything, just print what
     * you would generate.
     */
    public static final String GPROP_NOGEN = "nogen";


    /**************************************************************************
     * Default constructor. It does not use any cache for storing data
     * types retrieved from a Biomoby registry. It uses a {@link
     * org.biomoby.client.CentralImpl#DEFAULT_ENDPOINT default Biomoby
     * registry}.
     *************************************************************************/
    public Generator() {
 	try {
	    worker = new CentralDigestCachedImpl (null);
	} catch (MobyException e) {
	    // because there is not cache involved there should be no
	    // exception
	    System.err.println ("I am surprised...: " + e.getMessage());
	}
    }

    /**************************************************************************
     * Another constructor pointing to a cache directory where the
     * retrieved data types are stored. Otherwise, a {@link
     * org.biomoby.client.CentralImpl#DEFAULT_ENDPOINT default Biomoby
     * registry} is used. <p>
     *
     * @param cacheDir is a directory where to create or read from a
     * cache; the same directory can be safely shared for various
     * Biomoby registries
     *
     * @throws MobyException if there is a problem with reading or
     * creating the cache
     *************************************************************************/
    public Generator (String cacheDir)
	throws MobyException {
	worker = new CentralDigestCachedImpl (cacheDir);
    }

    /**************************************************************************
     * Another constructor pointing to a cache directory and defining
     * where is a Biomoby registry whoe data types will be generated. <p>
     *
     * @param registryEndpoint is a URL of a Biomoby registry
     * @param registryURI is a namespace/URI of such registry
     * @param cacheDir is a directory where to create or read from a
     * cache; the same directory can be safely shared for various
     * Biomoby registries
     *
     * @throws MobyException if there is a problem with reading or
     * creating the cache
     *************************************************************************/
    public Generator (String registryEndpoint,
		      String registryURI,
		      String cacheDir)
	throws MobyException {
	worker = new CentralDigestCachedImpl (registryEndpoint,
					      registryURI,
					      cacheDir);
    }

    /**************************************************************************
     * Another constructor getting a ready-to-use accessor (the
     * 'worker') to a, hopefully locally cached, BioMoby registry. <p>
     *
     * @param worker is an accessor to a BioMoby registry
     *************************************************************************/
    public Generator (final CentralCached worker) {
	this.worker = worker;
    }

    /**************************************************************************
     * Return an underlying object (a worker) that does all data types
     * retrieving and caching them. This is useful if you wish to have
     * more control over the cached results (for example if you want
     * to re-read the data types again). <p>
     *
     * @return a worker giving you a full access to a Biomoby registry
     *************************************************************************/
    public CentralCached getWorker() {
	return worker;
    }

    /*************************************************************************
     * More dealing with templates...
     *************************************************************************/
    protected String loadTemplate (Object templateSource)
	throws MobyException {
	if (templateSource instanceof File) {
	    String templateFile = ((File)templateSource).getPath();
	    try {
		return new String (FileUtils.findAndGetBinaryFile (templateFile));
	    } catch (GException e) {
		throw new MobyException ("Cannot find a template file '" + templateFile + "'." +
					 e.getMessage());
	    }
	} else {
	    throw new MobyException ("Loading template failed. Found unsupported type " +
				     templateSource.getClass().getName());
	}
    }

    /*************************************************************************
     * Format who generated this...
     *************************************************************************/
    protected static String getSignature() {
	return (System.getProperty ("user.name") + " on " + System.getProperty ("os.name"));
    }

    /*************************************************************************
     * Escape double quotes in 'value' (because the whole value will
     * be quoted in the generated code).
     *************************************************************************/
    protected static String escapeQuotes (String value) {
	if (value.indexOf ("\"") == -1) return value;
	// TBD: this replacement does not work... Why?
 	return value.replaceAll ("\"", "\\\"");
    }

    /*************************************************************************
     * Make 'value' HTML friendly. Additionally make sure that new
     * lines are replaced - that will prevent breaking Java comment
     * blocks (because the resulting HTML is used here in Java
     * generated sources). <p>
     *
     * Additionally it escapes also dollar signs and backslashes
     * because they cause problems in the replacement strings in
     * Java's pattern matching. <p>
     *
     * @param value will be checked
     * @return escaped 'value'
     *************************************************************************/
    protected static String htmlEscape (String value) {
	int len = value.length();
	StringBuffer buf = new StringBuffer (len);
	char c;
	for (int i = 0; i < len; i++) {
	    c = value.charAt(i);
            if (c == '"')       buf.append ("&quot;");
            else if (c == '&')  buf.append ("&amp;");
            else if (c == '<')  buf.append ("&lt;");
            else if (c == '>')  buf.append ("&gt;");
            else if (c == '\n') buf.append ("<br>");
            else if (c == '$')  buf.append ("&dollar;");
            else if (c == '\\') buf.append ("&bsol;");
            else {
                int ci = 0xffff & c;
                if (ci < 160 ) {
                    // has only 7 bits => nothing special
                    buf.append(c);
                } else {
                    // has 8 bits => escape them
                    buf.append ("&#");
                    buf.append (new Integer(ci).toString());
                    buf.append (';');
		}
            }
        }
	return buf.toString();
    }

    /*************************************************************************
     * Conditionally, make a directory where graphs for API (javadoc)
     * will be stored. The directory will be created in
     * 'outputDir'. If the creation fails, change set property
     * GPROP_NOGRAPHS to "false".
     *************************************************************************/
    protected void makeDirForGraphs (File outputDir, Properties props) {

	if (BooleanUtils.toBoolean (props.getProperty (GPROP_NOGRAPHS)))
	    return;

	File graphDir = new File (outputDir, "doc-files");
	if (! graphDir.exists()) {
	    if (! graphDir.mkdirs()) {
		if (verbose)
		    System.err.println ("Cannot create directory '" + graphDir + "'.");
		props.put (GPROP_NOGRAPHS, "true");
	    }
	}
    }

    /*************************************************************************
     * Call 'dot' program to create a graph in 'outputDir' named by
     * 'name' (and an extension .dot). Return true if graph was
     * successfully created.
     *
     * Additionally, if 'spaceForImageMap' is not null, create there a
     * client-side-image map (for HTML hyperlinks).
     *************************************************************************/
    protected boolean executeDot (String dot, File outputDir, String name,
				  Properties props,
				  StringBuffer spaceForImageMap) {
	String dotLocation = props.getProperty (GPROP_DOTLOCATION);
	if (dotLocation == null || "".equals (dotLocation.trim()))
	    dotLocation = "dot";
	try {
	    String imageFileName =
		new File (new File (outputDir, "doc-files"),
			  name + ".png").getAbsolutePath();
	    Executor process = new Executor (new String[] { dotLocation,
							    "-Tpng",
							    "-o",
							    imageFileName },
					     new String[] {}, dot);
	    int exitCode = process.waitFor();
	    if (exitCode != 0)
		throw new GException (name + ": " + process.getStderr());

	    if (spaceForImageMap != null) {
		process = new Executor (new String[] { dotLocation,
						       "-Tcmapx" },
					new String[] {}, dot);
		exitCode = process.waitFor();
		if (exitCode == 0)
		    spaceForImageMap.append (process.getStdout());
	    }

	    return true;
	} catch (java.lang.InterruptedException e) {
	    // we'll see...
	} catch (GException e) {
	    // report...
	    if (verbose && ! dotErrorReported) {
		dotErrorReported = true;
		System.err.println (e.getMessage());
	    }
	    // ...but save at least the 'dot' file (but ignore errors)
	    try {
		File dotFile =
		    new File (new File (outputDir, "doc-files"),
			      name + ".dot");
		Utils.createFile (dotFile, dot);
	    } catch (Exception e2) { }
	}
	return false;
    }

    /*************************************************************************
     * Create all needed sub-directories - as required by given Java
     * package name. <p>
     *
     * @param dirName a name of a directory where package-related
     * subdirectories will be created. 'dirName' itself does not need
     * to exist either (will be created - unless file permissions will
     * stop it). If 'dirName' is null, however, its default value will
     * be used - which is defined by System property 'user.dir'.
     *
     * @param packageName a usual Java dot-separated name for which
     * all needed subdirectories will be created starting from the
     * 'dirName'
     *
     * @return an abstract representation of the created directory
     *
     * @throws MobyException if 'packageName' is empty, or if some
     * directories could not be created
     *************************************************************************/
    public static File makeDirs (String dirName, String packageName)
	throws MobyException {

	// some checking and defaults
	if (dirName == null)
	    dirName = System.getProperty ("user.dir");
	if ( packageName == null || "".equals (packageName.trim()) )
	    throw  new MobyException
		("makeDirs: Package name must be specified. (in: " + dirName + ")");

	if ("".equals (dirName.trim()))
	    dirName = ".";
	if (!dirName.endsWith (File.separator))
	    dirName += File.separator;

	// make all directories as dictated by the package
	File fullDir = new File (dirName + packageName.replace ('.', File.separatorChar));
	if (!fullDir.exists())
	    if (!fullDir.mkdirs())
		throw new MobyException ("Cannot create directory '" + fullDir + "'.");

	return fullDir;
    }


    //
    // Abstract methods for subclasses
    //

    /*************************************************************************
     * Getting names of templates is isolated here so a sub-class can
     * override it (but any new teplate still needs to follow the same
     * patterns as the original template - so it is not too flexible,
     * anyway). <p>
     *
     * The values of returned Map are files. It their names are
     * relative (which is the best way) then they are looked for in
     * the CLASSPATH (including all jar files there). <p>
     *
     * Make sure that the returned path is correct for the system
     * where JVM is running.
     *************************************************************************/
    abstract public Map<String,File> getTemplateNames();

    /**************************************************************************
     * Generate "things" from a given Biomoby registry. This is the
     * main method. What the "things" are depends on the subclass that
     * overrides and implements this method. <p>
     *
     * @param props are properties influencing the generated results
     * (e.g. where to put them)
     *
     * @throws MobyException if anything goes wrong
     *************************************************************************/
    abstract public void generate (Properties props)
	throws MobyException;

}
