/**
 * 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.io.PrintWriter;
import java.io.Serializable;
import java.util.Iterator;
import java.util.List;

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

/**
 * Holds all the data for an utterance to be spoken.
 * It is incrementally modified by various UtteranceProcessor
 * implementations.  An utterance contains a set of Features (essential
 * a set of properties) and a set of Relations. A Relation is an
 * ordered set of Item graphs. The utterance contains a set of
 * features and implements FeatureSet so that applications can set/get
 * features directly from the utterance. If a feature query is not
 * found in the utterance feature set, the query is forwarded to the
 * FeatureSet of the voice associated with the utterance.
 */
public class Utterance implements FeatureSet, Serializable {

    private Voice voice;
    private FeatureSetImpl features;
    private FeatureSetImpl relations;
    private boolean first;	// first in a connected series
    private boolean last;	// last in a connected series
    private FreeTTSSpeakable speakable;

    /**
     * Creates a new, empty utterance.
     *
     * @param voice the voice associated with the utterance
     */
    public Utterance(Voice voice) {
	this.voice = voice;
	features = new FeatureSetImpl();
	relations = new FeatureSetImpl();
    }

    /**
     * Creates an utterance with the given set of tokenized text.
     *
     * @param voice the voice associated with the utterance
     * @param tokenList the list of tokens for this utterance
     */
    public Utterance(Voice voice, List tokenList) {
	this(voice);
	setTokenList(tokenList);
    }

    /**
     * Sets the speakable item for this utterance.
     *
     * @param speakable the speakable item for this utterance
     */
    public void setSpeakable(FreeTTSSpeakable speakable) {
	this.speakable = speakable;
    }

    /**
     * Returns the queueitem associated with this utterance.
     *
     * @return the queue item
     */
    public FreeTTSSpeakable getSpeakable() {
	return speakable;
    }

    /**
     * Creates a new relation with the given name and adds it to this
     * utterance.
     *
     * @param name the name of the new relation
     *
     * @return the newly created relation
     */
    public Relation createRelation(String name) {
	Relation relation = new Relation(name, this);
	relations.setObject(name, relation);
	return relation;
    }


    /**
     * Retrieves a relation from this utterance.
     *
     * @param name the name of the Relation
     *
     * @return the relation or null if the relation is not found
     */
    public Relation getRelation(String name) {
	return (Relation) relations.getObject(name);
    }

    /**
     * Determines if this utterance contains a relation with the given
     * name.
     *
     * @param name the name of the relation of interest.
     */
    public boolean hasRelation(String name) {
	return relations.isPresent(name);
    }

    /**
     * Retrieves the Voice associated with this Utterance.
     *
     * @return the voice associated with this utterance.
     */
    public Voice getVoice() {
	return voice;
    }

    /**
     * Dumps this utterance in textual form.
     *
     * @param output where to send the formatted output
     * @param pad the initial padding
     * @param title the title to print when dumping out the utterance
     * @param justRelations if true don't print voice features
     */
    public void dump(PrintWriter output, int pad, String title,
	    	boolean justRelations) {
	output.println(" ============ " + title + " ========== ");
	if (!justRelations) {
	    voice.dump(output, pad + 4, "Voice");
	    features.dump(output, pad + 4, "Features");
	}
	relations.dump(output, pad + 4, "Relations");
	output.flush();
    }

    /**
     * Dumps this utterance in textual form.
     *
     * @param output where to send the formatted output
     * @param pad the initial padding
     * @param title the title to print when dumping out the utterance
     */
    public void dump(PrintWriter output, int pad, String title) {
	dump(output, pad, title, false);
    }

    /**
     * Dumps this utterance in textual form.
     *
     * @param output where to send the formatted output
     * @param title the title to print when dumping out the utterance
     */
    public void dump(PrintWriter output, String title) {
	dump(output, 0, title, false);
    }

    /**
     * Dumps this utterance in textual form.
     *
     * @param title the title to print when dumping out the utterance
     */
    public void dump(String title) {
	dump(new PrintWriter(System.out), 0, title, false);
    }

    /**
     * Dumps the utterance in textual form
     * @param title the title to print when dumping out the utterance
     */
    public void dumpRelations(String title) {
	dump(new PrintWriter(System.out), 0, title, true);
    }

    /**
     * Determines if the given feature is present. If the feature is
     * not present in the utterance, the feature set of the voice is
     * checked.
     *
     * @param name the name of the feature of interest
     * @return true if the named feature is present
     */
    public boolean isPresent(String name) {
	if (!features.isPresent(name)) {
	    return getVoice().getFeatures().isPresent(name);
	} else {
	    return true;
	}
    }

    /**
     * Removes the named feature from this set of features.
     *
     * @param name the name of the feature of interest
     */
    public void remove(String name) {
	features.remove(name);
    }

    /**
     * Convenience method that returns the named feature as a string.
     * If the named feature is not present in the utterance, then this
     * attempts to retrieve it from the voice.
     *
     * @param name the name of the feature
     *
     * @return the value associated with the name or null if the value
     *   is not found
     *
     * @throws ClassCastException if the associated value is not a
     *   String
     */
    public String getString(String name) {
	if (!features.isPresent(name)) {
	    return getVoice().getFeatures().getString(name);
	} else {
	    return features.getString(name);
	}
    }

    /**
     * Convenience method that returns the named feature as a int.
     * If the named feature is not present in the utterance, then this
     * attempts to retrieve it from the voice.
     *
     * @param name the name of the feature
     *
     * @return the value associated with the name or null if the value
     *   is not found
     *
     * @throws ClassCastException if the associated value is not an
     *   int
     */
    public int getInt(String name) {
	if (!features.isPresent(name)) {
	    return getVoice().getFeatures().getInt(name);
	} else {
	    return features.getInt(name);
	}
    }

    /**
     * Convenience method that returns the named feature as a float.
     * If the named feature is not present in the utterance, then this
     * attempts to retrieve it from the voice.
     *
     * @param name the name of the feature
     *
     * @return the value associated with the name or null if the value
     *   is not found
     *
     * @throws ClassCastException if the associated value is not a
     *   float
     */
    public float getFloat(String name) {
	if (!features.isPresent(name)) {
	    return getVoice().getFeatures().getFloat(name);
	} else {
	    return features.getFloat(name);
	}
    }

    /**
     * Returns the named feature as an object.
     * If the named feature is not present in the utterance, then this
     * attempts to retrieve it from the voice.
     *
     * @param name the name of the feature
     *
     * @return the value associated with the name or null if the value
     *   is not found
     */
    public Object getObject(String name) {
	if (!features.isPresent(name)) {
	    return getVoice().getFeatures().getObject(name);
	} else {
	    return features.getObject(name);
	}
    }

    /**
     * Convenience method that sets the named feature as an int.
     *
     * @param name the name of the feature
     * @param value the value of the feature
     */
    public void setInt(String name, int value) {
	features.setInt(name, value);
    }

    /**
     * Convenience method that sets the named feature as a float.
     *
     * @param name the name of the feature
     * @param value the value of the feature
     */
    public void setFloat(String name, float value) {
	features.setFloat(name, value);
    }

    /**
     * Convenience method that sets the named feature as a String.
     *
     * @param name the name of the feature
     * @param value the value of the feature
     */
    public void setString(String name, String value) {
	features.setString(name, value);
    }

    /**
     * Sets the named feature.
     *
     * @param name the name of the feature
     * @param value the value of the feature
     */
    public void setObject(String name, Object value) {
	features.setObject(name, value);
    }

    /**
     * Returns the Item in the given Relation associated with the given time.
     *
     * @param relation the name of the relation
     * @param time the time
     *
     * @throws IllegalStateException if the Segment durations
     *   have not been calculated in the Utterance or if the
     *   given relation is not present in the Utterance
     */
    public Item getItem(String relation, float time) {
        
        Relation segmentRelation = null;
        
        if ((segmentRelation = getRelation(Relation.SEGMENT)) == null) {
            throw new IllegalStateException
                ("Utterance has no Segment relation");
        }
        
        String pathName = null;

        if (relation.equals(Relation.SEGMENT)) {
            // do nothing
        } else if (relation.equals(Relation.SYLLABLE)) {
            pathName = "R:SylStructure.parent.R:Syllable";
        } else if (relation.equals(Relation.SYLLABLE_STRUCTURE)) {
            pathName = "R:SylStructure.parent.parent";
        } else if (relation.equals(Relation.WORD)) {
            pathName = "R:SylStructure.parent.parent.R:Word";
        } else if (relation.equals(Relation.TOKEN)) {
	     pathName = "R:SylStructure.parent.parent.R:Token.parent";
        } else if (relation.equals(Relation.PHRASE)) {
            pathName = "R:SylStructure.parent.parent.R:Phrase.parent";
        } else {
            throw new IllegalArgumentException
                ("Utterance.getItem(): relation cannot be " + relation);
        }
        
        PathExtractor path = new PathExtractorImpl(pathName, false);
	 
        // get the Item in the Segment Relation with the given time
        Item segmentItem = SegmentRelationUtils.getItem
            (segmentRelation, time);
        
        if (relation.equals(Relation.SEGMENT)) {
            return segmentItem;
        } else if (segmentItem != null) {
            return path.findItem(segmentItem);
        } else {
            return null;
        }
    }

    /**
     * Returns the duration of this Utterance in seconds. It does this
     * by looking at last Item in the following Relations, in this order:
     * Segment, Target. If none of these Relations exist, or if these
     * Relations contain no Items, an IllegalStateException will be thrown.
     *
     * @return the duration of this Utterance in seconds
     */
    public float getDuration() {
	float duration = -1;
	if ((duration = getLastFloat(Relation.SEGMENT, "end")) == -1) {
	    if ((duration = getLastFloat(Relation.TARGET, "pos")) == -1) {
		throw new IllegalStateException
		    ("Utterance: Error finding duration");
	    }
	}
	return duration;
    }

    /**
     * Returns the float feature of the last Item in the named
     * Relation.
     *
     * @return the float feature of the last Item in the named Relation,
     * or -1 otherwise
     */
    private float getLastFloat(String relationName, String feature) {
	float duration = -1;
	Relation relation;
	if ((relation = getRelation(relationName)) != null) {
	    Item lastItem = relation.getTail();
	    if (lastItem != null) {
		duration = lastItem.getFeatures().getFloat(feature);
	    }
	}
	return duration;
    }


    /**
     * Sets the input text for this utterance
     * 
     * @param tokenList the set of tokens for this utterance
     *
     */
    private void setInputText(List tokenList) {
	StringBuffer sb = new StringBuffer();
	for (Iterator i = tokenList.iterator(); i.hasNext(); ) {
	    sb.append(i.next().toString());
	}
	setString("input_text", sb.toString());
    }


    /**
     * Sets the token list for this utterance. Note that this could be
     * optimized by turning the token list directly into the token
     * relation. 
     *
     * @param tokenList the tokenList
     *
     */
    private void setTokenList(List tokenList) {
	setInputText(tokenList);

	Relation relation = createRelation(Relation.TOKEN);
	for (Iterator i = tokenList.iterator(); i.hasNext(); ) {
	    Token token = (Token) i.next();
	    String tokenWord = token.getWord();
	    
	    if (tokenWord != null && tokenWord.length() > 0) {
		Item item = relation.appendItem();
		
		FeatureSet featureSet = item.getFeatures();
		featureSet.setString("name", tokenWord);
		featureSet.setString("whitespace", token.getWhitespace());
		featureSet.setString("prepunctuation", 
			token.getPrepunctuation());
		featureSet.setString("punc", token.getPostpunctuation());
		featureSet.setString("file_pos", 
			String.valueOf(token.getPosition()));
		featureSet.setString("line_number", 
			String.valueOf(token.getLineNumber()));
		
	    }
	}
    }

    /**
     * Returns true if this utterance is the first is a series of
     * utterances.
     *
     * @return true if this is the first utterance in a series of
     *     connected utterances.
     */
    public boolean isFirst() {
	return first;
    }

    /**
     * Sets this utterance as the first in a series.
     *
     * @param first if true, the item is the first in a series
     */
    public void setFirst(boolean first) {
	this.first = first;
    }

    /**
     * Returns true if this utterance is the last is a series of
     * utterances.
     *
     * @return true if this is the last utterance in a series of
     *     connected utterances.
     */
    public boolean isLast() {
	return last;
    }

    /**
     * Sets this utterance as the last in a series.
     *
     * @param last if true, the item is the last in a series
     */
    public void setLast(boolean last) {
	this.last = last;
    }
}
