/*
 * @(#)SimpleFunction.java   2.6.0   13 June 2007
 *
 * Copyright 2007
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.parser;

import edu.neu.ccs.*;

import java.text.ParseException;
import java.util.*;

/**
 * <p>Class <code>SimpleFunction</code> is an
 * implementation of <code>AbstractFunction</code>
 * that is defined by providing its information as
 * strings.</p>
 * 
 * <p>The function name is a string, the formal
 * parameter list is an array of string with the
 * parameter identifiers, and the function body
 * is a string that will be parsed in a context
 * in which the formal parameters are bound via
 * a let to the actual argument values.</p>
 * 
 * <p>The parameter list may also be provided to
 * the constructors as a comma separated list of
 * parameter names in a single string.</p>
 * 
 * <p>In the constructors, the user may supply a
 * specific parser but if that is omitted then
 * the current default parser is used.</p>
 * 
 * <p>The function is automatically installed as
 * a function in its parser.  This is both
 * convenient for the caller and essential since
 * a simple function cannot be parsed without a
 * parser.</p>
 * 
 * <p>A SimpleFunction may replace another
 * SimpleFunction with the same name that has
 * already been installed in the parser but it
 * may not replace an AbstractFunction that is
 * not a SimpleFunction.  This design prevents
 * the user from replacing a function that has
 * been built into the parser algorithmically.</p>
 * 
 * <p>See also:</p>
 * 
 * <ul>
 *   <li><code>SimpleFunctionPane</code>
 *       in package <code>edu.neu.ccs.gui</code></li>
 *   <li><code>SimpleFunctionBuilder</code>
 *       in package <code>edu.neu.ccs.gui</code></li>
 *   <li><code>SimpleFunctionPaneWithIO</code>
 *       in package <code>edu.neu.ccs.gui</code></li>
 *   <li><code>SimpleFunctionBuilderWithIO</code>
 *       in package <code>edu.neu.ccs.gui</code></li>
 * </ul>
 * 
 * @author Richard Rasala
 * @version 2.6.0
 */
public class SimpleFunction
    extends AbstractFunction
{
    /** The parser that will be used to evaluate this function. */
    private BaseParser parser;
    
    /** The function formal parameters. */
    private String[] parameters = null;
    
    /** The function body that will be parsed during evaluation. */
    private String body = null;
    
    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * <p>Constructor for <code>SimpleFunction</code>
     * that uses the current default parser.</p>
     * 
     * <p>The function name is a string, the formal
     * parameter list is an array of string with the
     * parameter identifiers, and the function body
     * is a string that will be parsed in a context
     * in which the formal parameters are bound via
     * a let to the actual argument values.</p>
     * 
     * <p>If parameters is <code>null</code>, it is
     * converted to an array of length 0 which means
     * that the function will take no parameters.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if:</p>
     * 
     * <ul>
     *   <li>Either name or body is <code>null</code>.</li>
     *   <li>The name is the name of a function built
     *   into the parser, that is, an AbstractFunction
     *   that is not a SimpleFunction.</p>
     *   <li>Either name or any parameter fails to be a
     *   possible identifier.</li>
     *   <li>The identifiers in the parameters list are
     *   not all distinct.</li>
     *   <li>An identifier in the parameters list is the
     *   same as the name of the proposed function.</li>
     * </ul>
     * 
     * <p>No effort is made to assess the validity of
     * the body as a parsable string.  This is done
     * when the function is called and the context is
     * fully known.</p>
     * 
     * <p>The body will be trimmed and also passed through
     * <code>Strings.flatten</code> so it may later be
     * displayed on a single line if desired.</p>
     * 
     * @param name the function name
     * @param parameters the function formal parameters
     * @param body the function body
     * @throws IllegalArgumentException
     */
    public SimpleFunction(
        String name,
        String[] parameters,
        String body)
    {
        this(null, name, parameters, body);
    }
    
    
    /**
     * <p>Constructor for <code>SimpleFunction</code>
     * that uses the current default parser.</p>
     * 
     * <p>The function name is a string, the formal
     * parameter list is a comma separated string with the
     * parameter identifiers, and the function body
     * is a string that will be parsed in a context
     * in which the formal parameters are bound via
     * a let to the actual argument values.</p>
     * 
     * <p>If parameters is <code>null</code>, the
     * function will take no parameters.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if:</p>
     * 
     * <ul>
     *   <li>Either name or body is <code>null</code>.</li>
     *   <li>The name is the name of a function built
     *   into the parser, that is, an AbstractFunction
     *   that is not a SimpleFunction.</p>
     *   <li>Either name or any parameter fails to be a
     *   possible identifier.</li>
     *   <li>The identifiers in the parameters list are
     *   not all distinct.</li>
     *   <li>An identifier in the parameters list is the
     *   same as the name of the proposed function.</li>
     * </ul>
     * 
     * <p>No effort is made to assess the validity of
     * the body as a parsable string.  This is done
     * when the function is called and the context is
     * fully known.</p>
     * 
     * <p>The body will be trimmed and also passed through
     * <code>Strings.flatten</code> so it may later be
     * displayed on a single line if desired.</p>
     * 
     * @param name the function name
     * @param parameters the function formal parameters
     * @param body the function body
     * @throws IllegalArgumentException
     */
    public SimpleFunction(
        String name,
        String parameters,
        String body)
    {
        this(null, name, Strings.splitCommaList(parameters), body);
    }
    
    
    /**
     * <p>Constructor for <code>SimpleFunction</code>
     * that uses the given parser.</p>
     * 
     * <p>If the given parser is <code>null</code>,
     * then the current default parser will be used.</p>
     * 
     * <p>The function name is a string, the formal
     * parameter list is an array of string with the
     * parameter identifiers, and the function body
     * is a string that will be parsed in a context
     * in which the formal parameters are bound via
     * a let to the actual argument values.</p>
     * 
     * <p>If parameters is <code>null</code>, it is
     * converted to an array of length 0 which means
     * that the function will take no parameters.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if:</p>
     * 
     * <ul>
     *   <li>Either name or body is <code>null</code>.</li>
     *   <li>The name is the name of a function built
     *   into the parser, that is, an AbstractFunction
     *   that is not a SimpleFunction.</p>
     *   <li>Either name or any parameter fails to be a
     *   possible identifier.</li>
     *   <li>The identifiers in the parameters list are
     *   not all distinct.</li>
     *   <li>An identifier in the parameters list is the
     *   same as the name of the proposed function.</li>
     * </ul>
     * 
     * <p>No effort is made to assess the validity of
     * the body as a parsable string.  This is done
     * when the function is called and the context is
     * fully known.</p>
     * 
     * <p>The body will be trimmed and also passed through
     * <code>Strings.flatten</code> so it may later be
     * displayed on a single line if desired.</p>
     * 
     * @param parser the parser to use to parse this function
     * @param name the function name
     * @param parameters the function formal parameters
     * @param body the function body
     * @throws IllegalArgumentException
     */
    public SimpleFunction(
        BaseParser parser,
        String name,
        String[] parameters,
        String body)
    {
        // construct base object
        //
        // note that if name is null the base constructor
        // will fail with IllegalArgumentException
        super(
            (name == null) ? null : (name = name.trim()),
            (parameters == null) ? 0 :parameters.length);
        
        if (parser == null)
            parser = ParserUtilities.getDefaultParser();
        
        this.parser = parser;
        
        if (parameters == null)
            parameters = new String[0];
        else
            Strings.trim(parameters);
        
        if (body != null)
            body = body.trim();
        
        // check for errors
        String message = testFunctionName(parser, name);
        
        if (message == null)
            message = testParameterArrayNames(parser, parameters, name);
        
        if (message == null)
            message = testFunctionBody(body);
        
        if (message != null)
            throw new IllegalArgumentException(message);
        
        // install data
        this.parameters = parameters;
        this.body = Strings.flatten(body);
        
        // install this function in its parser
        parser.addFunction(this);
    }
        
    
    /**
     * <p>Constructor for <code>SimpleFunction</code>
     * that uses the given parser.</p>
     * 
     * <p>If the given parser is <code>null</code>,
     * then the current default parser will be used.</p>
     * 
     * <p>The function name is a string, the formal
     * parameter list is a comma separated string with the
     * parameter identifiers, and the function body
     * is a string that will be parsed in a context
     * in which the formal parameters are bound via
     * a let to the actual argument values.</p>
     * 
     * <p>If parameters is <code>null</code>, the
     * function will take no parameters.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if:</p>
     * 
     * <ul>
     *   <li>Either name or body is <code>null</code>.</li>
     *   <li>The name is the name of a function built
     *   into the parser, that is, an AbstractFunction
     *   that is not a SimpleFunction.</p>
     *   <li>Either name or any parameter fails to be a
     *   possible identifier.</li>
     *   <li>The identifiers in the parameters list are
     *   not all distinct.</li>
     *   <li>An identifier in the parameters list is the
     *   same as the name of the proposed function.</li>
     * </ul>
     * 
     * <p>No effort is made to assess the validity of
     * the body as a parsable string.  This is done
     * when the function is called and the context is
     * fully known.</p>
     * 
     * <p>The body will be trimmed and also passed through
     * <code>Strings.flatten</code> so it may later be
     * displayed on a single line if desired.</p>
     * 
     * @param parser the parser to use to parse this function
     * @param name the function name
     * @param parameters the function formal parameters
     * @param body the function body
     * @throws IllegalArgumentException
     */
    public SimpleFunction(
        BaseParser parser,
        String name,
        String parameters,
        String body)
    {
        this(parser, name, Strings.splitCommaList(parameters), body);
    }
    
    
    /**
     * <p>Parses the body of this function in a context
     * in which the formal parameters are bound via let
     * to the given values and returns the object
     * computed.</p>
     * 
     * <p>The number of values should equal the number
     * of expected function parameters.</p>
     * 
     * @param values the actual parameter values
     */
    public final Object functionCall(Object[] values)
        throws ParseException
    {
        this.checkValuesAsXObject(values);
        
        return parser.parseWithArgumentList(body, parameters, values);
    }
    
    
    /**
     * <p>Returns a copy of this function's
     * formal parameter list.</p>
     */
    public final String[] parameters() {
        int length = parameters.length;
        
        String[] copy = new String[length];
        
        for (int i = 0; i < length; i++)
            copy[i] = parameters[i];
        
        return copy;
    }
    
    
    /**
     * <p>Returns a copy of this function's
     * formal parameter list
     * as a comma separated string.</p>
     * 
     * <p>If the function has 0 parameters,
     * this method will return a string
     * with one blank to act as a
     * placeholder for parsing purposes.</p>
     */
    public final String parametersAsString() {
        String result = Strings.makeCommaList(parameters);
        
        if (result.length() == 0)
            return " ";
        else
            return result;
    }
    
    
    /**
     * <p>Returns this function's body.</p>
     * 
     * <p>The string that is returned has been trimmed
     * and flattened (via <code>Strings.flatten</code>)
     * so it may viewed on a single line.</p>
     */
    public final String body() { return body; }
    
    
    /**
     * <p>Returns this function's parser.</p>
     */
    public final BaseParser parser() { return parser; }
    
    
    /**
     * <p>Tests if the given name
     * is a possible simple function name
     * in the context of the current default parser;
     * returns a non-<code>null</code> error message
     * if a problem is detected;
     * otherwise returns <code>null</code>.</p>
     * 
     * <p>Used in the constructors for this class and
     * may be used by a caller to pretest prior to
     * invoking one of the constructors.</p>
     * 
     * @param fcnName the proposed function name
     */
    public static String testFunctionName(String fcnName)
    {
        return testFunctionName(null, fcnName);
    }
    
    
    /**
     * <p>Tests if the given name
     * is a possible simple function name
     * in the context of the given parser;
     * returns a non-<code>null</code> error message
     * if a problem is detected;
     * otherwise returns <code>null</code>.</p>
     * 
     * <p>If the given parser is <code>null</code>,
     * then the current default parser will be used.</p>
     * 
     * <p>Used in the constructors for this class and
     * may be used by a caller to pretest prior to
     * invoking one of the constructors.</p>
     * 
     * @param parser the parser context
     * @param fcnName the proposed function name
     */
    public static String testFunctionName
        (BaseParser parser, String fcnName)
    {
        if (parser == null)
            parser = ParserUtilities.getDefaultParser();
        
        String message = null;
        
        if (fcnName == null) {
            message = "A function name must not be null";
        }
        else{
            fcnName = fcnName.trim();
            
            if (fcnName.length() == 0) {
                message = "A function name must not be empty";
            }
            
            else if (! BaseParser.isPossibleIdentifier(fcnName)) {
                message = fcnName + " is not a valid identifier\n"
                    + "and therefore may not be a function name";
            }
            
            else if (parser.isReservedID(fcnName)) {
                message = fcnName + " may not define a function name\n"
                + "since it is the reserved ID of a special form";
            }
            
            else if (parser.isConstantID(fcnName)) {
                message = fcnName + " may not define a function name\n"
                + "since it already defines a constant ID";
            }
            
            else if (parser.isOrdinaryFunctionName(fcnName)) {
                message = fcnName
                    + " is already installed as a built-in function\n"
                    + " and may not be replaced by a simple function";
            }
        }
        
        return message;
    }
    
    
    /**
     * <p>Tests if the given name is a possible parameter name
     * in the context of the current default parser
     * and the given function name;
     * returns a non-<code>null</code> error message
     * if a problem is detected;
     * otherwise returns <code>null</code>.</p>
     * 
     * <p>Note that a parameter name may not be the same as its
     * function name.</p>
     * 
     * <p>Used in the constructors for this class and
     * may be used by a caller to pretest prior to
     * invoking one of the constructors.</p>
     * 
     * @param paramName the proposed parameter name
     * @param fcnName the proposed function name
     */
    public static String testParameterName
        (String paramName, String fcnName)
    {
        return testParameterName(null, paramName, fcnName);
    }
    
    
    /**
     * <p>Tests if the given name is a possible parameter name
     * in the context of the given parser
     * and the given function name;
     * returns a non-<code>null</code> error message
     * if a problem is detected;
     * otherwise returns <code>null</code>.</p>
     * 
     * <p>Note that a parameter name may not be the same as its
     * function name.</p>
     * 
     * <p>If the given parser is <code>null</code>,
     * then the current default parser will be used.</p>
     * 
     * <p>Used in the constructors for this class and
     * may be used by a caller to pretest prior to
     * invoking one of the constructors.</p>
     * 
     * @param parser the parser context
     * @param paramName the proposed parameter name
     * @param fcnName the proposed function name
     */
    public static String testParameterName
        (BaseParser parser, String paramName, String fcnName)
    {
        if (parser == null)
            parser = ParserUtilities.getDefaultParser();
        
        String message = null;
        
        if (paramName == null) {
            message = "A parameter name must not be null";
        }
        else if (fcnName == null) {
            message = "A function name must not be null";
        }
        else{
            paramName = paramName.trim();
            fcnName   = fcnName.trim();
            
            if (paramName.length() == 0) {
                message = "A parameter name must not be empty";
            }
            
            else if (fcnName.length() == 0) {
                message = "A function name must not be empty";
            }
            
            else if (! BaseParser.isPossibleIdentifier(paramName)) {
                message = paramName + " is not a valid identifier\n"
                    + "and therefore may not be a parameter name";
            }
            
            else if (paramName.equals(fcnName)) {
                message = "A parameter name " + paramName
                    + " must not equal its function name";
            }
            
            else if (parser.isReservedID(paramName)) {
                message = paramName + " may not define a parameter name\n"
                + "since it already defines a reserved ID";
            }
            
            else if (parser.isConstantID(paramName)) {
                message = paramName + " may not define a parameter name\n"
                + "since it already defines a constant ID";
            }
            
            else if (parser.isFunctionName(paramName)) {
                message = paramName + " may not define a parameter name\n"
                + "since it already defines a function name";
            }
        }
        
        return message;
    }
    
    
    /**
     * <p>Tests if the given array of names
     * is a possible set of parameter names
     * in the context of the current default parser
     * and the given function name;
     * returns a non-<code>null</code> error message
     * if a problem is detected;
     * otherwise returns <code>null</code>.</p>
     * 
     * <p>Note that a parameter name may not be the same as its
     * function name.</p>
     * 
     * <p>Used in the constructors for this class and
     * may be used by a caller to pretest prior to
     * invoking one of the constructors.</p>
     * 
     * @param parameters the proposed array of parameter names
     * @param fcnName the proposed function name
     */
    public static String testParameterArrayNames
        (String[] parameters, String fcnName)
    {
        return testParameterArrayNames(null, parameters, fcnName);
    }
    
    
    /**
     * <p>Tests if the given array of names
     * is a possible set of parameter names
     * in the context of the given parser
     * and the given function name;
     * returns a non-<code>null</code> error message
     * if a problem is detected;
     * otherwise returns <code>null</code>.</p>
     * 
     * <p>Note that a parameter name may not be the same as its
     * function name.</p>
     * 
     * <p>If the given parser is <code>null</code>,
     * then the current default parser will be used.</p>
     * 
     * <p>Used in the constructors for this class and
     * may be used by a caller to pretest prior to
     * invoking one of the constructors.</p>
     * 
     * @param parser the parser context
     * @param parameters the proposed array of parameter names
     * @param fcnName the proposed function name
     */
    public static String testParameterArrayNames
        (BaseParser parser, String[] parameters, String fcnName)
    {
        if (parser == null)
            parser = ParserUtilities.getDefaultParser();
        
        String message = null;
        
        if (parameters == null) {
            message = "An array of parameter names must not be null";
        }
        else {
            parameters = Strings.trim(parameters);
            
            int length = parameters.length;
            
            Hashtable args = new Hashtable();
            
            for (int i = 0; i < length; i++) {
                String arg = parameters[i];
                
                message = testParameterName(parser, arg, fcnName);
                
                if (message != null)
                    break;
                
                args.put(arg, arg);
            }
            
            if ((message == null) && (args.size() < length)) {
                message = "An array of parameter names"
                    + " has a parameter name that is repeated";
            }
        }
        
        return message;
    }
    
    
    /**
     * <p>Tests if the proposed function body is
     * non-<code>null</code> and non-empty.</p>
     * 
     * <p>Note that it is not possible to make more
     * sophisticated tests until runtime so this
     * method does only the most obvious tests.</p>
     * 
     * <p>There is no corresponding method to this
     * method that accepts a parser argument since
     * we do not wish to execute the body in the
     * context of a parser since functions may
     * have side effects.</p>
     * 
     * <p>Used in the constructors for this class and
     * may be used by a caller to pretest prior to
     * invoking one of the constructors.</p>
     * 
     * @param body the proposed function body
     */
    public static String testFunctionBody(String body) {
        String message = null;
        
        if (body == null) {
            message = "A function body must not be null";
        }
        else{
            body = body.trim();
            
            if (body.length() == 0) {
                message = "A function body must not be empty";
            }
        }
        
        return message;
    }
    
    
    /**
     * <p>Returns a string with 3 lines consisting of the
     * function name, the function parameters as a comma
     * separated list, and the function body on one line.</p>
     * 
     * <p>If the function has 0 parameters, the line for
     * its parameters will contain a blank to act as a
     * placeholder for parsing purposes.</p>
     */
    public String toString() {
        String fcnName = name();
        String fcnParams = parametersAsString();
        String fcnBody = body();
        
        int length = 3
            + fcnName.length()
            + fcnParams.length()
            + fcnBody.length();
        
        StringBuffer buffer = new StringBuffer(length);
        
        buffer.append(fcnName);
        buffer.append("\n");
        
        buffer.append(fcnParams);
        buffer.append("\n");
        
        buffer.append(fcnBody);
        buffer.append("\n");
        
        return buffer.toString();
    }
    
}

