/*
 *   This program 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.
 *
 *   This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Javadoc.java
 * Copyright (C) 2006-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.core;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;

/**
 * Abstract superclass for classes that generate Javadoc comments and replace
 * the content between certain comment tags.
 * 
 * @author  fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 8034 $
 */
public abstract class Javadoc 
  implements OptionHandler, RevisionHandler {

  /** the start tag */
  protected String[] m_StartTag = null;

  /** the end tag */
  protected String[] m_EndTag = null;
  
  /** the classname */
  protected String m_Classname = Javadoc.class.getName();
  
  /** whether to include the stars in the Javadoc */
  protected boolean m_UseStars = true;

  /** the directory above the class to update */
  protected String m_Dir = "";
  
  /** whether to suppress error messages (no printout in the console) */
  protected boolean m_Silent = false;
  
  /**
   * Returns an enumeration describing the available options.
   *
   * @return an enumeration of all the available options.
   */
  public Enumeration listOptions() {
    Vector<Option> result = new Vector<Option>();

    result.addElement(new Option(
        "\tThe class to load.",
        "W", 1, "-W <classname>"));
    
    result.addElement(new Option(
        "\tSuppresses the '*' in the Javadoc.",
        "nostars", 0, "-nostars"));
    
    result.addElement(new Option(
        "\tThe directory above the package hierarchy of the class.",
        "dir", 1, "-dir <dir>"));
    
    result.addElement(new Option(
        "\tSuppresses printing in the console.",
        "silent", 0, "-silent"));
    
    return result.elements();
  }
  
  /**
   * Parses a given list of options. 
   *
   * @param options the list of options as an array of strings
   * @throws Exception if an option is not supported
   */
  public void setOptions(String[] options) throws Exception {
    String      		tmpStr;
    
    tmpStr = Utils.getOption('W', options);
    if (tmpStr.length() > 0)
      setClassname(tmpStr);
    else
      setClassname(this.getClass().getName());

    setUseStars(!Utils.getFlag("nostars", options));

    setDir(Utils.getOption("dir", options));

    setSilent(Utils.getFlag("silent", options));
  }
  
  /**
   * Gets the current settings of this object.
   *
   * @return an array of strings suitable for passing to setOptions
   */
  public String[] getOptions() {
    Vector<String> 	result;

    result = new Vector<String>();
    
    result.add("-W");
    result.add(getClassname());
    
    if (!getUseStars())
      result.add("-nostars");
    
    if (getDir().length() != 0) {
      result.add("-dir");
      result.add(getDir());
    }
    
    if (getSilent())
      result.add("-silent");
    
    return (String[]) result.toArray(new String[result.size()]);
  }
  
  /**
   * sets the classname of the class to generate the Javadoc for
   * 
   * @param value	the new classname
   */
  public void setClassname(String value) {
    m_Classname = value;
  }
  
  /**
   * returns the current classname
   * 
   * @return	the current classname
   */
  public String getClassname() {
    return m_Classname;
  }
  
  /**
   * sets whether to prefix the Javadoc with "*"
   * 
   * @param value	true if stars are used
   */
  public void setUseStars(boolean value) {
    m_UseStars = value;
  }
  
  /**
   * whether the Javadoc is prefixed with "*"
   * 
   * @return 		whether stars are used
   */
  public boolean getUseStars() {
    return m_UseStars;
  }
  
  /**
   * sets the dir containing the file that is to be updated. It is the dir
   * above the package hierarchy of the class.
   * 
   * @param value	the directory containing the classes
   */
  public void setDir(String value) {
    m_Dir = value;
  }
  
  /**
   * returns the current dir containing the class to update. It is the dir
   * above the package name of the class.
   * 
   * @return		the  current directory
   */
  public String getDir() {
    return m_Dir;
  }
  
  /**
   * sets whether to suppress output in the console
   * 
   * @param value	true if output is to be suppressed
   */
  public void setSilent(boolean value) {
    m_Silent = value;
  }
  
  /**
   * whether output in the console is suppressed
   * 
   * @return 		true if output is suppressed
   */
  public boolean getSilent() {
    return m_Silent;
  }
  
  /**
   * prints the given object to System.err
   * 
   * @param o		the object to print
   */
  protected void println(Object o) {
    if (!getSilent())
      System.err.println(o.toString());
  }

  /**
   * returns true if the class can be instantiated, i.e., has a default
   * constructor.
   * 
   * @return true if the class can be instantiated
   */
  protected boolean canInstantiateClass() {
    boolean	result;
    Class	cls;

    result = true;
    cls    = null;

    try {
      cls = Class.forName(getClassname());
    }
    catch (Exception e) {
      result = false;
      println("Cannot instantiate '" + getClassname() + "'! Class in CLASSPATH?");
    }

    if (result) {
      try {
	cls.newInstance();
      }
      catch (Exception e) {
	result = false;
	println("Cannot instantiate '" + getClassname() + "'! Missing default constructor?");
      }
    }
    
    return result;
  }
  
  /**
   * Returns a new instance of the class
   * 
   * @return a new instance of the class
   */
  protected Object getInstance() {
    Object	result;
    Class	cls;

    result = null;
    
    try {
      cls    = Class.forName(getClassname());
      result = cls.newInstance();
    }
    catch (Exception e) {
      result = null;
    }
    
    return result;
  }
  
  /**
   * converts the given String into HTML, i.e., replacing some char entities
   * with HTML entities.
   * 
   * @param s		the string to convert
   * @return		the HTML conform string
   */
  protected String toHTML(String s) {
    String	result;
    
    result = s;
    
    result = result.replaceAll("&", "&amp;");
    result = result.replaceAll("<", "&lt;");
    result = result.replaceAll(">", "&gt;");
    result = result.replaceAll("@", "&#64;");
    result = result.replaceAll("\n", "<br/>\n");
    
    return result;
  }

  /**
   * indents the given string by a given number of indention strings
   * 
   * @param content	the string to indent
   * @param count	the number of times to indent one line
   * @param indentStr	the indention string
   * @return		the indented content
   */
  protected String indent(String content, int count, String indentStr) {
    String		result;
    StringTokenizer	tok;
    int			i;
    
    tok = new StringTokenizer(content, "\n", true);
    result = "";
    while (tok.hasMoreTokens()) {
      if (result.endsWith("\n") || (result.length() == 0)) {
	for (i = 0; i < count; i++)
	  result += indentStr;
      }
      result += tok.nextToken();
    }
    
    return result;
  }
  
  /**
   * generates and returns the Javadoc for the specified start/end tag pair.
   * 
   * @param index	the index in the start/end tag array
   * @return		the generated Javadoc
   * @throws Exception 	in case the generation fails
   */
  protected abstract String generateJavadoc(int index) throws Exception;
  
  /**
   * generates and returns the Javadoc
   * 
   * @return		the generated Javadoc
   * @throws Exception 	in case the generation fails
   */
  protected String generateJavadoc() throws Exception {
    String	result;
    int		i;
    
    result = "";
    
    for (i = 0; i < m_StartTag.length; i++) {
      if (i > 0)
	result += "\n\n";
      result += generateJavadoc(i).trim();
    }
    
    return result;
  }

  /**
   * determines the base string of the given indention string, whether it's
   * either only spaces (one space will be retured) or mixed mode (tabs and 
   * spaces, in that case the same string will be returned)
   * 
   * @param str		the string to analyze
   * @return 		the indention string
   */
  protected String getIndentionString(String str) {
    String	result;
    
    // only spaces?
    if (str.replaceAll(" ", "").length() == 0)
      result = " ";
    // only tabs?
    else if (str.replaceAll("\t", "").length() == 0)
      result = "\t";
    else
      result = str;
      
    return result;
  }
  
  /**
   * determines the number of indention strings that have to be inserted to
   * generated the given indention string.
   * 
   * @param str 	the string to analyze
   * @return		the number of base indention strings to insert
   */
  protected int getIndentionLength(String str) {
    int		result;
    
    // only spaces?
    if (str.replaceAll(" ", "").length() == 0)
      result = str.length();
    // only tabs?
    else if (str.replaceAll("\t", "").length() == 0)
      result = str.length();
    else
      result = 1;
    
    return result;
  }
  
  /**
   * generates and returns the Javadoc for the specified start/end tag pair
   * 
   * @param content	the current source code
   * @param index	the index in the start/end tag array
   * @return		the generated Javadoc
   * @throws Exception 	in case the generation fails
   */
  protected String updateJavadoc(String content, int index) throws Exception {
    StringBuffer	resultBuf;
    int			indentionLen;
    String		indentionStr;
    String		part;
    String		tmpStr;

    // start and end tag?
    if (    (content.indexOf(m_StartTag[index]) == -1)
	   || (content.indexOf(m_EndTag[index]) == -1) ) {
	println(
	    "No start and/or end tags found: " 
	    + m_StartTag[index] + "/" + m_EndTag[index]);
	return content;
    }

    // replace option-tags
    resultBuf = new StringBuffer();
    while (content.length() > 0) {
      if (content.indexOf(m_StartTag[index]) > -1) {
	part = content.substring(0, content.indexOf(m_StartTag[index]));
	// is it a Java constant? -> skip
	if (part.endsWith("\"")) {
	  resultBuf.append(part);
	  resultBuf.append(m_StartTag[index]);
	  content = content.substring(part.length() + m_StartTag[index].length());
	}
	else {
	  tmpStr       = part.substring(part.lastIndexOf("\n") + 1);
	  indentionLen = getIndentionLength(tmpStr);
	  indentionStr = getIndentionString(tmpStr);
	  part         = part.substring(0, part.lastIndexOf("\n") + 1);
	  resultBuf.append(part);
	  resultBuf.append(indent(m_StartTag[index], indentionLen, indentionStr) + "\n");
	  resultBuf.append(indent(generateJavadoc(index), indentionLen, indentionStr));
	  resultBuf.append(indent(m_EndTag[index], indentionLen, indentionStr));
	  content = content.substring(content.indexOf(m_EndTag[index]));
	  content = content.substring(m_EndTag[index].length());
	}
      }
      else {
	resultBuf.append(content);
	content = "";
      }
    }
    
    return resultBuf.toString().trim();
  }
  
  /**
   * updates the Javadoc in the given source code.
   * 
   * @param content	the source code
   * @return		the updated source code
   * @throws Exception 	in case the generation fails
   */
  protected String updateJavadoc(String content) throws Exception {
    String	result;
    int		i;
    
    result = content;
    
    for (i = 0; i < m_StartTag.length; i++) {
      result = updateJavadoc(result, i);
    }
    
    return result;
  }
  
  /**
   * generates the Javadoc and returns it applied to the source file if one
   * was provided, otherwise an empty string.
   * 
   * @return		the generated Javadoc
   * @throws Exception 	in case the generation fails
   */
  public String updateJavadoc() throws Exception {
    StringBuffer	contentBuf;
    BufferedReader	reader;
    String		line;
    String		result;
    File		file;
    
    result = "";
    
    // non-existing?
    file = new File(getDir() + "/" + getClassname().replaceAll("\\.", "/") + ".java");
    if (!file.exists()) {
      println("File '" + file.getAbsolutePath() + "' doesn't exist!");
      return result;
    }
    
    try {
      // load file
      reader     = new BufferedReader(new FileReader(file));
      contentBuf = new StringBuffer();
      while ((line = reader.readLine()) != null) {
	contentBuf.append(line + "\n");
      }
      reader.close();
      result = updateJavadoc(contentBuf.toString());
    }
    catch (Exception e) {
      e.printStackTrace();
    }
    
    return result.trim();
  }
  
  /**
   * generates either the plain Javadoc (if no filename specified) or the
   * updated file (if a filename is specified). The start and end tag for
   * the global info have to be specified in the file in the latter case.
   * 
   * @return 		either the plain Javadoc or the modified file
   * @throws Exception 	in case the generation fails
   */
  public String generate() throws Exception {
    if (getDir().length() == 0)
      return generateJavadoc();
    else
      return updateJavadoc();
  }
  
  /**
   * generates a string to print as help on the console
   * 
   * @return 	the generated help
   */
  public String generateHelp() {
    String 		result;
    Enumeration 	enm;
    Option 		option;
    
    result = getClass().getName().replaceAll(".*\\.", "") + " Options:\n\n";
    enm = listOptions();
    while (enm.hasMoreElements()) {
      option = (Option) enm.nextElement();
      result += option.synopsis() + "\n" + option.description() + "\n";
    }
    
    return result;
  }
  
  /**
   * runs the javadoc producer with the given commandline options
   * 
   * @param javadoc	the javadoc producer to execute
   * @param options	the commandline options
   */
  protected static void runJavadoc(Javadoc javadoc, String[] options) {
    try {
      try {
	if (Utils.getFlag('h', options))
	  throw new Exception("Help requested");

	javadoc.setOptions(options);
        Utils.checkForRemainingOptions(options);

        // directory is necessary!
        if (javadoc.getDir().length() == 0)
          throw new Exception("No directory provided!");
      } 
      catch (Exception ex) {
        String result = "\n" + ex.getMessage() + "\n\n" + javadoc.generateHelp();
        throw new Exception(result);
      }

      System.out.println(javadoc.generate() + "\n");
    } 
    catch (Exception ex) {
      System.err.println(ex.getMessage());
    }
  }
}
