/**
 * Portions Copyright 2003 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.diphone;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.sun.speech.freetts.relp.Sample;
import com.sun.speech.freetts.relp.SampleInfo;
import com.sun.speech.freetts.util.BulkTimer;
import com.sun.speech.freetts.util.Utilities;

/**
 * Represents and manages the unit data for all diphones.  The diphone
 * data set is stored in a set of data files. These data are loaded by this
 * class into internal data structures before diphone synthesis can
 * occur. 
 * <p>
 *The diphone data set is one of the largest sets of data that
 * needs to be loaded by the synthesizer and therefore can add to the
 * overall startup time for any system using this database.  For
 * certain applications, the startup time is a critical spec that
 * needs to be optimized, while for other applications, startup time
 * is inconsequential.  This class provides settings (via system
 * properties) that control how the database is to be loaded so that
 * applications can tune for quick startup or optimal run time.
 * <p>
 * This class serves also as a testbed for comparing performance of
 * the traditional java binary I/O and the new io ( <code>java.nio </code>)
 * package.
 * <p>
 * <p> A diphone database can be loaded from a text data file, or a
 * binary datafile.  The binary version loads significantly faster
 * than the text version. Additionally, a binary index can be
 * generated and used to reduce overall memory footprint.
 * <p> 
 * <p>
 * A DiphoneUnitDatabase contains an array of frames, and an aray of
 * residuals. The frames are the samples of the wave, and the
 * residuals are for linear predictive coding use. This is called
 * "cst_sts" (a struct) in flite.
 * <p>
 * Note that if 'com.sun.speech.freetts.useNewIO' is set to true and
 * the input type is binary, than the JDK1.4+ new IO api is used to
 * load the database.
 * <p>
 * The system property
 * <pre>
 *	com.sun.speech.freetts.diphone.UnitDatabase.cacheType 
 * </pre>
 *
 * can be set to one of:
 *
 * <ul>
 * <li> preload: database is loaded at startup
 * <li> demand: database is loaded on demand
 * <li> hard: database is loaded on demand but cached
 * <li> soft: database is loaded on demand but cached with soft references
 * </ul>
 *
 * This <code> cacheType </code> setting controls how the database is
 * loaded. The default is to 'preload' the database. This setting
 * gives best runtime performance but with longer initial startup
 * cost.  
 */
public class DiphoneUnitDatabase {
    /** Logger instance. */
    private static final Logger LOGGER =
        Logger.getLogger(DiphoneUnitDatabase.class.getName());

    private String name;
    private int sampleRate;
    private int numChannels;
    private int residualFold = 1;
    private float lpcMin;
    private float lpcRange;
    private int lineCount = 0;
    private Diphone defaultDiphone;
    private Map diphoneMap = null;
    private Map diphoneIndex;
    private SampleInfo sampleInfo;
    
    private boolean useNewIO =
	Utilities.getProperty("com.sun.speech.freetts.useNewIO",
		"true").equals("true");
    // cache can be 'preload' 'none', 'soft' or 'hard'
    private String cacheType = 
	Utilities.getProperty(
	    "com.sun.speech.freetts.diphone.UnitDatabase.cacheType",
	    "preload");
    private boolean useIndexing = !cacheType.equals("preload");
    private boolean useCache = !cacheType.equals("demand");
    private boolean useSoftCache = cacheType.equals("soft");

    private final static int MAGIC = 0xFEEDFACE;
    private final static int INDEX_MAGIC = 0xFACADE;
    private final static int VERSION = 1;
    private final static int MAX_DB_SIZE = 4 * 1024 * 1024;

    private String indexName = null;
    private MappedByteBuffer mbb = null;
    private int defaultIndex = -1;

    /**
     * Creates the DiphoneUnitDatabase from the given input stream.
     *
     * @param url the location of the database
     * @param isBinary if <code>true</code> the database is in 
     *		binary format; otherwise it is in text format
     *
     * @throws IOException if there is trouble opening the DB
     */
    public DiphoneUnitDatabase(URL url, boolean isBinary) throws IOException {
        // MS, 22.04.2005: Commented out the "if" clause:
        // indexing is applied only when useNewIO is turned on and
        // data is read from a FileInputStream. This is not true when useing
        // the default settings but setting
        // com.sun.speech.freetts.diphone.UnitDatabase.cacheType=demand
        //if (!useIndexing || useCache) {
	    diphoneMap = new LinkedHashMap();
	    //}
	InputStream is = Utilities.getInputStream(url);

	indexName = getIndexName(url.toString());

	if (isBinary) {
	    loadBinary(is);
	} else {
	    loadText(is);
	}
	is.close();
	sampleInfo = new SampleInfo(sampleRate, numChannels,
		residualFold, lpcMin, lpcRange, 0.0f);
    }

    /**
     * Return the information about the sample data
     * for this database.
     *
     * @return the sample info
     */

    SampleInfo getSampleInfo() {
	return sampleInfo;
    }


    /**
     * Returns the index name from the databaseName.
     *
     * @param databaseName the database name
     *
     * @return the index name or null if the database is not
     * 	       a binary database.
     *
     * [[[ TODO the index should probably be incorporated into the
     * binary database ]]]
     */
    private String getIndexName(String databaseName) {
	String indexName = null;
	if (databaseName.lastIndexOf(".") != -1) {
	    indexName = databaseName.substring(0,
		    databaseName.lastIndexOf(".")) + ".idx";
	}
	return indexName;
    }

    /**
     * Loads the database from the given input stream.
     *
     * @param is the input stream
     */
    private void loadText(InputStream is) {
        BufferedReader reader;
        String line;

	if (is == null) {
	    throw new Error("Can't load diphone db file.");
	}

	reader = new BufferedReader(new InputStreamReader(is));
        try {
            line = reader.readLine();
	    lineCount++;
            while (line != null) {
                if (!line.startsWith("***")) {
                    parseAndAdd(line, reader);
                }
                line = reader.readLine();
            }
	    reader.close();
        } catch (IOException e) {
            throw new Error(e.getMessage() + " at line " + lineCount);
        } finally {
	}
    }

    /**
     * Parses and process the given line. Used to process the text
     * form of the database.
     *
     * @param line the line to process
     * @param reader the source for the lines
     */
    private void parseAndAdd(String line, BufferedReader reader) {
	try {
	    StringTokenizer tokenizer = new StringTokenizer(line," ");
	    String tag = tokenizer.nextToken();
	    if (tag.equals("NAME")) {
		name = tokenizer.nextToken();
	    } else if (tag.equals("SAMPLE_RATE")) {
		sampleRate = Integer.parseInt(tokenizer.nextToken());
	    } else if (tag.equals("NUM_CHANNELS")) {
		numChannels = Integer.parseInt(tokenizer.nextToken());
	    } else if (tag.equals("LPC_MIN")) {
		lpcMin  = Float.parseFloat(tokenizer.nextToken());
	    } else if (tag.equals("COEFF_MIN")) {
		lpcMin  = Float.parseFloat(tokenizer.nextToken());
	    } else if (tag.equals("COEFF_RANGE")) {
		lpcRange  = Float.parseFloat(tokenizer.nextToken());
	    } else if (tag.equals("LPC_RANGE")) {
		lpcRange  = Float.parseFloat(tokenizer.nextToken());
	    } else if (tag.equals("ALIAS")) {
            String name = tokenizer.nextToken();
            String origName = tokenizer.nextToken();
            AliasDiphone diphone = new AliasDiphone(name, origName);
            add(diphone);
        } else if (tag.equals("DIPHONE")) {
		String name = tokenizer.nextToken();
		int start  = Integer.parseInt(tokenizer.nextToken());
		int mid = Integer.parseInt(tokenizer.nextToken());
		int end = Integer.parseInt(tokenizer.nextToken());
		int numSamples = (end - start);
		int midPoint = mid - start;

		if (numChannels <= 0) {
		    throw new Error("For diphone '"+name+"': Bad number of channels " + numChannels);
		}

		if (numSamples <= 0) {
		    throw new Error("For diphone '"+name+"': Bad number of samples " + numSamples);
		}

		Sample[] samples = new Sample[numSamples];

		for (int i = 0; i < samples.length; i++) {
		    samples[i] = new Sample(reader, numChannels);
		}
		Diphone diphone = new Diphone(name, samples, midPoint);
		add(diphone);
	    } else {
		throw new Error("Unsupported tag " + tag);
	    }
	} catch (NoSuchElementException nse) {
	    throw new Error("Error parsing db " + nse.getMessage());
	} catch (NumberFormatException nfe) {
	    throw new Error("Error parsing numbers in db " + nfe.getMessage());
	}
    }


    /**
     * Adds the given diphone to the DB. Diphones are kept in a map so
     * they can be accessed by name.
     *
     * @param diphone the diphone to add.
     */
    private void add(Diphone diphone) {
        if (diphone instanceof AliasDiphone) {
            AliasDiphone adiph = (AliasDiphone) diphone;
            Diphone original = (Diphone) 
                diphoneMap.get(adiph.getOriginalName());
            if (original != null) {
                adiph.setOriginalDiphone(original);
            } else {
                // No original was found for this alias
                // -- complain, and ignore
                if (LOGGER.isLoggable(Level.FINER)) {
                    LOGGER.finer("For diphone alias "
                        +adiph.getName()+", could not find original "
                        +adiph.getOriginalName());
                }
                return;
            }
        }
	diphoneMap.put(diphone.getName(), diphone);
	if (defaultDiphone == null) {
	    defaultDiphone = diphone;
	}
    }

    /**
     * Looks up the diphone with the given name.
     *
     * @param unitName the name of the diphone to look for
     *
     * @return the diphone or the defaultDiphone if not found.
     */
    public Diphone getUnit(String unitName) {
	Diphone diphone = null;

	if (useIndexing) {
	    diphone = getFromCache(unitName);
	    if (diphone == null) {
		int index = getIndex(unitName);
		if (index != -1) {
		    mbb.position(index);
		    try {
			diphone = Diphone.loadBinary(mbb);
			if (diphone != null) {
                // If diphone is an alias, must also get the original
                if (diphone instanceof AliasDiphone) {
                    AliasDiphone adiph = (AliasDiphone) diphone;
                    Diphone original = getUnit(adiph.getOriginalName());
                    if (original != null) {
                        adiph.setOriginalDiphone(original);
                        putIntoCache(unitName, adiph);
                    } else {
                        // No original was found for this alias
                        // -- complain, and ignore
                        if (LOGGER.isLoggable(Level.FINER)) {
                            LOGGER.finer("For diphone alias "
                                +adiph.getName()+", could not find original "
                                +adiph.getOriginalName());
                        }
                        diphone = null;
                    }
                } else { // a normal diphone
                    putIntoCache(unitName, diphone);
                }
			}
		    } catch (IOException ioe) {
			System.err.println("Can't load diphone " +
				unitName);
			diphone = null;
		    }
		}
	    }
	} else {
	    diphone = (Diphone) diphoneMap.get(unitName);
	}

	if (diphone == null) {
	    System.err.println("Can't find diphone " + unitName);
	    diphone = defaultDiphone;
	}

	return diphone;
    }

    /**
     * Gets the named diphone from the cache. If we are using soft
     * caching, the reference may be a soft/weak reference so check to
     * see if the reference is still valid, if so return it; otherwise
     * invalidate it. Note that we have not had good success with weak
     * caches so far. The goal is to reduce the minimum required
     * memory footprint as far as possible while not compromising
     * performance. In small memory systems, the weak cache would
     * likely be reclaimed, giving us lower performance but with the
     * ability to still be able to run. In reality, the soft caches
     * did not help much. They just did not work correctly. 
     * [[[ TODO: test weak/soft cache behavior with new versions of
     * the runtime to see if their behavior has improved ]]]
     *
     * @param name the name of the diphone
     *
     * @return the diphone or <code> null </code>  if not in the cache
     */
    private Diphone getFromCache(String name) {
	if (diphoneMap == null) {
	    return null;
	}
	Diphone diphone = null;

	if (useSoftCache) {
	    Reference ref  = (Reference) diphoneMap.get(name);
	    if (ref != null) {
		diphone = (Diphone) ref.get();
		if (diphone == null) {
		    diphoneMap.remove(name);
		} else {
		}
	    }
	} else {
	    diphone = (Diphone) diphoneMap.get(name);
	}
	return diphone;
    }

    /**
     * Puts the diphone in the cache.
     *
     * @param diphoneName the name of the diphone 
     * @param diphone the diphone to put in the cache
     */
    private void putIntoCache(String diphoneName, Diphone diphone) {
	if (diphoneMap == null) {
	    return ;
	}
	if (useSoftCache) {
  	    diphoneMap.put(diphoneName, new WeakReference(diphone));
	} else {
  	    diphoneMap.put(diphoneName, diphone);
	}
    }

    /**
     * Dumps the soft ref cache.
     */
    private void dumpCacheSize() {
	int empty = 0;
	int full = 0;
	System.out.println("Entries: " + diphoneMap.size());
	for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
	    Reference ref = (Reference) i.next();
	    if (ref.get() == null) {
		empty++;
	    } else {
		full++;
	    }
	}
	System.out.println("   empty: " + empty);
	System.out.println("    full: " + full);
    }

    
    /**
     * Returns the name of this DiphoneUnitDatabase.
     */
    public String getName() {
	return name;
    }
    
    /**
     * Dumps the diphone database.
     */
    public void dump() {
	System.out.println("Name        " + name);
	System.out.println("SampleRate  " + sampleRate);
	System.out.println("NumChannels " + numChannels);
	System.out.println("lpcMin      " + lpcMin);
	System.out.println("lpcRange    " + lpcRange);

	for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
	    Diphone diphone = (Diphone) i.next();
	    diphone.dump();
	}
    }

    /**
     * Dumps a binary form of the database.
     *
     * @param path the path to dump the file to
     */
    public void dumpBinary(String path) {
	try {
	    FileOutputStream fos = new FileOutputStream(path);
            DataOutputStream os = new DataOutputStream(fos);
	    int written;

	    os.writeInt(MAGIC);
	    os.writeInt(VERSION);
	    os.writeInt(sampleRate);
	    os.writeInt(numChannels);
	    os.writeFloat(lpcMin);
	    os.writeFloat(lpcRange);
	    os.writeInt(diphoneMap.size());

	    for (Iterator i = diphoneMap.values().iterator(); i.hasNext();) {
		Diphone diphone = (Diphone) i.next();
		diphone.dumpBinary(os);
	    }
            os.flush();
            fos.close();

	} catch (FileNotFoundException fe) {
	    throw new Error("Can't dump binary database " +
		    fe.getMessage());
	} catch (IOException ioe) {
	    throw new Error("Can't write binary database " +
		    ioe.getMessage());
	}
    }

    /**
     * Dumps a binary index. The database index is used if our
     * cacheType is not set to 'preload' and we are loading a binary
     * database. The index is a simple mapping of diphone names (the
     * key) to the file position in the database. In situations where
     * the entire database is not preloaded, this index can be loaded
     * and used to provide quicker startup (since only the index need
     * be loaded at startup) and quick access to the diphone data.
     *
     * @param path the path to dump the file to
     */
    void dumpBinaryIndex(String path) {
	try {
	    FileOutputStream fos = new FileOutputStream(path);
	    DataOutputStream dos = new DataOutputStream(fos);

	    dos.writeInt(INDEX_MAGIC);
	    dos.writeInt(diphoneIndex.keySet().size());

	    for (Iterator i = diphoneIndex.keySet().iterator(); i.hasNext();) {
		String key = (String) i.next();
		int pos = ((Integer) diphoneIndex.get(key)).intValue();
		dos.writeUTF(key);
		dos.writeInt(pos);
	    }
	    dos.close();

	} catch (FileNotFoundException fe) {
	    throw new Error("Can't dump binary index " +
		    fe.getMessage());
	} catch (IOException ioe) {
	    throw new Error("Can't write binary index " +
		    ioe.getMessage());
	}
    }

    /**
     * Loads a binary index.
     *
     * @param url the location  of the binary index file
     */
    private void loadBinaryIndex(URL url) {

	diphoneIndex = new HashMap();

	try {
	    InputStream is = Utilities.getInputStream(url);
	    DataInputStream dis = new DataInputStream(is);

	    if (dis.readInt() != INDEX_MAGIC) {
		throw new Error("Bad index file format");
	    }

	    int size = dis.readInt();

	    for (int i = 0; i < size; i++) {
		String diphoneName = dis.readUTF();
		int pos = dis.readInt();
		diphoneIndex.put(diphoneName, new Integer(pos));
	    }
	    dis.close();

	} catch (FileNotFoundException fe) {
	    throw new Error("Can't load binary index " +
		    fe.getMessage());
	} catch (IOException ioe) {
	    throw new Error("Can't read binary index " +
		    ioe.getMessage());
	}
    }

    /**
     * Gets the index for the given diphone.
     *
     * @param diphone the name of the diphone
     *
     * @return the index into the database for the diphone
     */
    private int getIndex(String diphone) {
	Integer index = (Integer) diphoneIndex.get(diphone);
	if (index != null) {
	    int idx = index.intValue();
	    if (defaultIndex == -1) {
		defaultIndex = idx;
	    }
	    return idx;
	} else {
	    System.out.println("Can't find index entry for " + diphone);
	    return defaultIndex;
	}
    }



    /**
     * Loads a binary file from the input stream. 
     * <p>
     * Note that we currently have four! methods of loading up the
     * database. We were interested in the performance characteristics
     * of the various methods of loading the database so we coded it
     * all up.
     *
     * @param is the input stream to read the database
     * 		from
     *
     * @throws IOException if there is trouble opening the DB
     * 
     */
    private void loadBinary(InputStream is) throws IOException {
	// we get better performance if we can map the file in
	// 1.0 seconds vs. 1.75 seconds, but we can't
	// always guarantee that we can do that.
	if (useNewIO && is instanceof FileInputStream) {
	    FileInputStream fis = (FileInputStream) is;
	    if (useIndexing) {
		loadBinaryIndex(new URL(indexName));
		mapDatabase(fis);
	    } else {
		loadMappedBinary(fis);
	    }
	} else {
        useIndexing = false; // just to make this clear
	    DataInputStream dis = new DataInputStream(
		    new BufferedInputStream(is));
	    loadBinary(dis);
	}
    }


    /**
     * Loads the binary data from the given input stream.
     *
     * @param dis the data input stream.
     */
    private void loadBinary(DataInputStream dis) throws IOException {
	int size;
	if (dis.readInt() != MAGIC)  {
	    throw new Error("Bad magic in db");
	}
	if (dis.readInt() != VERSION)  {
	    throw new Error("Bad VERSION in db");
	}

	sampleRate = dis.readInt();
	numChannels = dis.readInt();
	lpcMin = dis.readFloat();
	lpcRange = dis.readFloat();
	size = dis.readInt();

	for (int i = 0; i < size; i++) {
	    Diphone diphone = Diphone.loadBinary(dis);
	    add(diphone);
	}
    }


    /**
     * Loads the database from the given FileInputStream.
     *
     * @param is the InputStream to load the database from
     *
     * @throws IOException if there is trouble opening the DB
     */
    private void loadMappedBinary(FileInputStream is) throws IOException {
	FileChannel fc = is.getChannel();

	MappedByteBuffer bb = 
	    fc.map(FileChannel.MapMode.READ_ONLY, 0, (int) fc.size());
	bb.load();
	loadDatabase(bb);
	is.close();
    }

    /**
     * Maps the database from the given FileInputStream.
     *
     * @param is the InputStream to load the database from
     *
     * @throws IOException if there is trouble opening the DB
     */
    private void mapDatabase(FileInputStream is) throws IOException {
	FileChannel fc = is.getChannel();
	mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, (int) fc.size());
	mbb.load();
        loadDatabaseHeader(mbb);
    }

    /**
     * Loads the database header from the given byte buffer.
     *
     * @param bb the byte buffer to load the db from
     *
     * @throws IOException if there is trouble opening the DB
     */
    private void loadDatabaseHeader(ByteBuffer bb) throws IOException {
	if (bb.getInt() != MAGIC)  {
	    throw new Error("Bad magic in db");
	}
	if (bb.getInt() != VERSION)  {
	    throw new Error("Bad VERSION in db");
	}

	sampleRate = bb.getInt();
	numChannels = bb.getInt();
	lpcMin = bb.getFloat();
	lpcRange = bb.getFloat();
    }

    /**
     * Loads the database from the given byte buffer.
     *
     * @param bb the byte buffer to load the db from
     *
     * @throws IOException if there is trouble opening the DB
     */
    private void loadDatabase(ByteBuffer bb) throws IOException {
        int size;
	loadDatabaseHeader(bb);
	size = bb.getInt();

	diphoneIndex = new HashMap();
	for (int i = 0; i < size; i++) {
	    int pos = bb.position();
	    Diphone diphone = Diphone.loadBinary(bb);
	    add(diphone);
	    diphoneIndex.put(diphone.getName(), new Integer(pos));
	}
    }

    /**
     * Compares this database to another. This is used for testing.
     * With this method we can load up two databases (one perhaps from
     * a text source and one from a binary source) and compare to
     * verify that the dbs are identical
     *
     * @param other the other database
     *
     * @return <code>true</code>  if the DBs are identical; 
     *		otherwise <code>false</code> 
     */
    public boolean compare(DiphoneUnitDatabase other) {
	if (sampleRate != other.sampleRate) {
	    return false;
	}

	if (numChannels != other.numChannels) {
	    return false;
	}

	if (lpcMin != other.lpcMin) {
	    return false;
	}

	if (lpcRange != other.lpcRange) {
	    return false;
	}

	for (Iterator i = diphoneMap.values().iterator(); i.hasNext(); ) {
	    Diphone diphone = (Diphone) i.next();
	    Diphone otherDiphone = (Diphone) other.getUnit(diphone.getName());
	    if (!diphone.compare(otherDiphone)) {
		System.out.println("Diphones differ:");
		System.out.println("THis:");
		diphone.dump();
		System.out.println("Other:");
		otherDiphone.dump();
		return false;
	    }
	}
	return true;
    }


    /**
     * Manipulates a DiphoneUnitDatabase. This program is typically
     * used to generate the binary form (with index) of the 
     * DiphoneUnitDatabase from the text form. Additionally, this program 
     * can be used to compare two databases to see if they are
     * identical (used for testing).
     *
     * <p>
     * <b> Usage </b>
     * <p>
     *  <code> java com.sun.speech.freetts.diphone.DiphoneUnitDatabase
     *  [options]</code> 
     * <p>
     * <b> Options </b>
     * <p>
     *    <ul>
     *          <li> <code> -src path </code> provides a directory
     *          path to the source text for the database
     *          <li> <code> -dest path </code> provides a directory
     *          for where to place the resulting binaries
     *		<li> <code> -generate_binary [filename] </code> 
     *		reads in the text
     *		version of the database and generates the binary
     *		version of the database.
     *		<li> <code> -compare </code>  Loads the text and
     *		binary versions of the database and compares them to
     *		see if they are equivalent.
     *		<li> <code> -showTimes </code> shows timings for any
     *		loading, comparing or dumping operation
     *    </ul>
     * 
     */
    public static void main(String[] args) {
	boolean showTimes = false;
        String srcPath = ".";
        String destPath = ".";
        
	try {
	    if (args.length > 0) {
		BulkTimer timer = BulkTimer.LOAD;
		timer.start();
		for (int i = 0 ; i < args.length; i++) {
                    if (args[i].equals("-src")) {
                        srcPath = args[++i];
                    } else if (args[i].equals("-dest")) {
                        destPath = args[++i];
                    } else if (args[i].equals("-generate_binary")) {
			 String name = "diphone_units.txt";
			 if (i + 1 < args.length) {
                             String nameArg = args[++i];
                             if (!nameArg.startsWith("-")) {
                                 name = nameArg;
                             }
			 } 

			 int suffixPos = name.lastIndexOf(".txt");

			 String binaryName = "diphone_units.bin";
			 if (suffixPos != -1) {
			     binaryName = name.substring(0, suffixPos) + ".bin";
			 }

			 String indexName = "diphone_units.idx";

			 if (suffixPos != -1) {
			     indexName = name.substring(0, suffixPos) + ".idx";
			 }

			 System.out.println("Loading " + name);
			 timer.start("load_text");
			 DiphoneUnitDatabase udb = new DiphoneUnitDatabase(
				new URL("file:"
                                        + srcPath + "/" + name), false);
			 timer.stop("load_text");

			 System.out.println("Dumping " + binaryName);
			 timer.start("dump_binary");
	    		 udb.dumpBinary(destPath + "/" + binaryName);
			 timer.stop("dump_binary");

			 timer.start("load_binary");
			 DiphoneUnitDatabase budb = 
			    new DiphoneUnitDatabase(
				    new URL("file:"
                                            + destPath + "/" + binaryName),
                                    true);
			 timer.stop("load_binary");

			 System.out.println("Dumping " + indexName);
			 timer.start("dump index");
	    		 budb.dumpBinaryIndex(destPath + "/" + indexName);
			 timer.stop("dump index");
		    } else if (args[i].equals("-compare")) {

			timer.start("load_text");
			 DiphoneUnitDatabase udb = new DiphoneUnitDatabase(
				new URL("file:./diphone_units.txt"), false);
			timer.stop("load_text");

			timer.start("load_binary");
			DiphoneUnitDatabase budb = 
			    new DiphoneUnitDatabase(
				    new URL("file:./diphone_units.bin"), true);
			timer.stop("load_binary");

			timer.start("compare");
			if (udb.compare(budb)) {
			    System.out.println("other compare ok");
			} else {
			    System.out.println("other compare different");
			}
			timer.stop("compare");
		    } else if (args[i].equals("-showtimes")) {
			showTimes = true;
		    } else {
			System.out.println("Unknown option " + args[i]);
		    }
		}
		timer.stop();
		if (showTimes) {
		    timer.show("DiphoneUnitDatabase");
		}
	    } else {
		System.out.println("Options: ");
		System.out.println("    -src path");
		System.out.println("    -dest path");
		System.out.println("    -compare");
		System.out.println("    -generate_binary");
		System.out.println("    -showTimes");
	    }
	} catch (IOException ioe) {
	    System.err.println(ioe);
	}
    }
}
