/**
 * Portions Copyright 2001 Sun Microsystems, Inc.
 * Portions Copyright 1999-2001 Language Technologies Institute, 
 * Carnegie Mellon University.
 * All Rights Reserved.  Use is subject to license terms.
 * 
 * See the file "license.terms" for information on usage and
 * redistribution of this file, and for a DISCLAIMER OF ALL 
 * WARRANTIES.
 */
package com.sun.speech.freetts;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.speech.freetts.util.Utilities;


/**
 * Interface that Manages a feature or item path. Allows navigation
 * to the corresponding feature or item.
 * This class in controlled by the following system properties:
 *
 * <pre>
 *   com.sun.speech.freetts.interpretCartPaths - default false
 *   com.sun.speech.freetts.lazyCartCompile - default true
 * </pre>
 *   com.sun.speech.freetts.interpretCartPaths
 *
 * Instances of this class will optionally pre-compile the paths.
 * Pre-compiling paths reduces the processing time and objects needed
 * to extract a feature or an item based upon a path.
 */
public class PathExtractorImpl implements PathExtractor {
    /** Logger instance. */
    private static final Logger LOGGER =
        Logger.getLogger(PathExtractorImpl.class.getName());

    /**
      * If this system property is set to true, paths will
      * not be compiled.
      */
    public final static String INTERPRET_PATHS_PROPERTY =
	"com.sun.speech.freetts.interpretCartPaths";

    /**
     * If this system property is set to true, CART feature/item
     * paths will only be compiled as needed.
     */
    public final static String LAZY_COMPILE_PROPERTY =
	"com.sun.speech.freetts.lazyCartCompile";

    private final static boolean INTERPRET_PATHS = 
	Utilities.getProperty(INTERPRET_PATHS_PROPERTY, "false").equals("true");
    private final static boolean LAZY_COMPILE  = 
	Utilities.getProperty(LAZY_COMPILE_PROPERTY, "true").equals("true");

    private String pathAndFeature;
    private String path;
    private String feature;
    private Object[] compiledPath;
    private boolean wantFeature = false;

    /**
     * Creates a path for the given feature.
     */
    public PathExtractorImpl(String pathAndFeature, boolean wantFeature) {
	this.pathAndFeature = pathAndFeature;
	if (INTERPRET_PATHS)  {
	    path = pathAndFeature;
	    return;
	}
    
	if (wantFeature) {
	    int lastDot = pathAndFeature.lastIndexOf(".");
	    // string can be of the form "p.feature" or just "feature"

	    if (lastDot == -1) {
		feature = pathAndFeature;
		path = null;
	    } else {
		feature = pathAndFeature.substring(lastDot + 1);
		path = pathAndFeature.substring(0, lastDot);
	    }
	    this.wantFeature = wantFeature;
	} else {
	    this.path = pathAndFeature;
	}

	if (!LAZY_COMPILE) {
	    compiledPath = compile(path);
	}
    }

    /**
     * Finds the item associated with this Path.
     * @param item the item to start at
     * @return the item associated with the path or null
     */
    public Item findItem(Item item) {

	if (INTERPRET_PATHS) {
	    return item.findItem(path);
	}

	if (compiledPath == null) {
	    compiledPath = compile(path);
	}

	Item pitem = item;

	for (int i = 0; pitem != null && i < compiledPath.length; ) {
	    OpEnum op = (OpEnum) compiledPath[i++];
	    if (op == OpEnum.NEXT) {
		pitem = pitem.getNext();
	    } else if (op == OpEnum.PREV) {
		pitem = pitem.getPrevious();
	    } else if (op == OpEnum.NEXT_NEXT) {
		pitem = pitem.getNext();
		if (pitem != null) {
		    pitem = pitem.getNext();
		}
	    } else if (op == OpEnum.PREV_PREV) {
		pitem = pitem.getPrevious();
		if (pitem != null) {
		    pitem = pitem.getPrevious();
		}
	    } else if (op == OpEnum.PARENT) {
		pitem = pitem.getParent();
	    } else if (op == OpEnum.DAUGHTER) {
		pitem = pitem.getDaughter();
	    } else if (op == OpEnum.LAST_DAUGHTER) {
		pitem = pitem.getLastDaughter();
	    } else if (op == OpEnum.RELATION) {
		String relationName = (String) compiledPath[i++];
		pitem = pitem.getSharedContents().getItemRelation(relationName);
	    } else {
		System.out.println("findItem: bad feature " + op +
			" in " + path);
	    }
	}
	return pitem;
    }


    /**
     * Finds the feature associated with this Path.
     * @param item the item to start at
     * @return the feature associated or "0"  if the
     * feature was not found.
     */
    public Object findFeature(Item item) {

	if (INTERPRET_PATHS) {
	    return item.findFeature(path);
	}

	Item pitem = findItem(item);
	Object results = null;
	if (pitem != null) {
	        if (LOGGER.isLoggable(Level.FINER)) {
	            LOGGER.finer("findFeature: Item [" + pitem + "], feature '" 
	                    + feature + "'");
	        }

	    FeatureProcessor fp =
		pitem.getOwnerRelation().getUtterance().
		    getVoice().getFeatureProcessor(feature);

	    if (fp != null) {
	        if (LOGGER.isLoggable(Level.FINER)) {
	            LOGGER.finer(
	                    "findFeature: There is a feature processor for '" 
	                    + feature + "'");
	        }
		try {
		    results = fp.process(pitem);
		} catch (ProcessException pe) {
		    LOGGER.severe("trouble while processing " + fp);
		     throw new Error(pe);
		}
	    } else {
		results = pitem.getFeatures().getObject(feature);
	    }
	}

	results = (results == null) ? "0" : results;
	if (LOGGER.isLoggable(Level.FINER)) {
	    LOGGER.finer("findFeature: ...results = '" + results + "'");
	}
	return results;
    }


    /**
     * Compiles the given path into the compiled form
     * @param path the path to compile
     * @return the compiled form which is in the form
     * of an array path traversal enums and associated strings
     */
    private Object[] compile(String path) {
	List list = new ArrayList();

	if (path == null) {
	    return list.toArray();
	}

	StringTokenizer tok = new StringTokenizer(path, ":.");

	while (tok.hasMoreTokens()) {
	    String token = tok.nextToken();
	    OpEnum op = OpEnum.getInstance(token);
	    if (op == null) {
		throw new Error("Bad path compiled " + path);
	    } 

	    list.add(op);

	    if (op == OpEnum.RELATION) {
		list.add(tok.nextToken());
	    }
	}
	return list.toArray();
    }

    // inherited for Object

    public String toString() {
	return pathAndFeature;
    }


    // TODO: add these to the interface should we support binary
    // files
    /*
    public void writeBinary();
    public void readBinary();
    */
}


/**
 * An enumerated type associated with path operations.
 */
class OpEnum {
    static private Map map = new HashMap();

    public final static OpEnum NEXT = new OpEnum("n");
    public final static OpEnum PREV = new OpEnum("p");
    public final static OpEnum NEXT_NEXT = new OpEnum("nn");
    public final static OpEnum PREV_PREV = new OpEnum("pp");
    public final static OpEnum PARENT = new OpEnum("parent");
    public final static OpEnum DAUGHTER = new OpEnum("daughter");
    public final static OpEnum LAST_DAUGHTER = new OpEnum("daughtern");
    public final static OpEnum RELATION = new OpEnum("R");

    private String name;

    /**
     * Creates a new OpEnum.. There is a limited
     * set of OpEnums
     * @param name the path name for this Enum
     */
    private OpEnum(String name) {
	this.name = name;
	map.put(name, this);
    }

    /**
     * gets an OpEnum thats associated with
     * the given name.
     * @param name the name of the OpEnum of interest
     */
    public static OpEnum getInstance(String name) {
	return (OpEnum) map.get(name);
    }

    // inherited from Object
    public String toString() {
	return name;
    }
}
