/**
 * 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.clunits;

import java.io.BufferedOutputStream;
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.net.URL;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;

import com.sun.speech.freetts.cart.CART;
import com.sun.speech.freetts.cart.CARTImpl;
import com.sun.speech.freetts.relp.SampleInfo;
import com.sun.speech.freetts.relp.SampleSet;
import com.sun.speech.freetts.util.BulkTimer;
import com.sun.speech.freetts.util.Utilities;


/**
 * Provides support for the cluster unit database. The use of the
 * cluster unit database is confined to this clunits package. This
 * class provides a main program that can be used to convert from a
 * text version of the database to a binary version of the database.
 *
 * The ClusterUnitDataBase can be loaded from a text or a binary
 * source. The binary form of the database loads much faster and
 * therefore is generally used in a deployed system.
 *
 */
public class ClusterUnitDatabase {

    final  static int CLUNIT_NONE = 65535;

    private DatabaseClusterUnit[] units;
    private UnitType[] unitTypes;
    private SampleSet sts;
    private SampleSet mcep;
    
    private UnitOriginInfo[] unitOrigins; // for debugging

    private int continuityWeight;
    private int optimalCoupling;
    private int extendSelections;
    private int joinMethod;
    private int[] joinWeights;
    private int joinWeightShift;

    private Map cartMap = new HashMap();
    private CART defaultCart = null;

    private transient List unitList;
    private transient int lineCount;
    private transient List unitTypesList;

    private final static int MAGIC = 0xf0cacc1a;
    private final static int VERSION = 0x1000;


    /**
     * Creates the UnitDatabase from the given input stream.
     *
     * @param is the input stream to read the database from
     * @param isBinary the input stream is a binary stream
     *
     * @throws IOException if there is trouble opening the DB
     */
    ClusterUnitDatabase(URL url, boolean isBinary) throws IOException {
	BulkTimer.LOAD.start("ClusterUnitDatabase");
	InputStream is = Utilities.getInputStream(url);
	if (isBinary) {
	    loadBinary(is);
	} else {
	    loadText(is);
	}
	is.close();
    // Attempt to load debug info from a .debug resource.
    // This will silently fail if no debug info is available.
    String urlString = url.toExternalForm();
    URL debugURL = new URL(urlString.substring(0, urlString.lastIndexOf(".")) + ".debug");
    try {
        InputStream debugInfoStream = Utilities.getInputStream(debugURL);
        loadUnitOrigins(debugInfoStream);
    } catch (IOException ioe) {
        // Silently ignore if you cannot load the debug info
    }
	BulkTimer.LOAD.stop("ClusterUnitDatabase");
    }


    /**
     * Retrieves the begininning sample index for the
     * given entry.
     *
     * @param unitEntry the entry of interest
     *
     * @return the begininning sample index
     */
    int getStart(int unitEntry) {
	return units[unitEntry].start;
    }

    /**
     * Retrieves the ending sample index for the
     * given entry.
     *
     * @param unitEntry the entry of interest
     *
     * @return the ending sample index
     */
    int getEnd(int unitEntry) {
	return units[unitEntry].end;
    }

    /**
     * Retrieves the phone for the given entry
     *
     * @param unitEntry the entry of interest
     *
     * @return the phone for the entry
     */
    int getPhone(int unitEntry) {
	return units[unitEntry].phone;
    }

    /**
     * Returns the cart of the given unit type.
     *
     * @param unitType the type of cart
     *
     * @return the cart 
     */
    CART getTree(String unitType) {
	CART cart =  (CART) cartMap.get(unitType);

	if (cart == null) {
	    System.err.println("ClusterUnitDatabase: can't find tree for " 
		    + unitType);
	    return defaultCart; 	// "graceful" failrue
	}
	return cart;
    }

    /**
     * Retrieves the type index for the name given a name. 
     *
     * @param name the name
     *
     * @return the index for the name
     */
// [[[TODO: perhaps replace this with  java.util.Arrays.binarySearch]]]
    int getUnitTypeIndex(String name) {
	int start, end, mid, c;

	start = 0;
	end = unitTypes.length;

	while (start < end) {
	    mid = (start + end) / 2;
	    c = unitTypes[mid].getName().compareTo(name);
	    if (c == 0) {
		return mid;
	    } else if (c > 0) {
		end = mid;
	    } else {
		start = mid + 1;
	    }
	}
	return -1;
    }

    /**
     * Retrieves the unit index given a unit type and val.
     *
     * @param unitType the type of the unit
     * @param instance the value associated with the unit
     *
     * @return the index.
     */
    int getUnitIndex(String unitType, int instance) {
	int i = getUnitTypeIndex(unitType);
	if (i == -1) {
	    error("getUnitIndex: can't find unit type " + unitType);
	    i = 0;
	}
	if (instance >= unitTypes[i].getCount()) {
	    error("getUnitIndex: can't find instance " 
		    + instance + " of " + unitType);
	    instance = 0;
	}
	return unitTypes[i].getStart() + instance;
    }


    /**
     * Retrieves the index for the name given a name. 
     *
     * @param name the name
     *
     * @return the index for the name
     */
    int getUnitIndexName(String name) {
	int lastIndex = name.lastIndexOf('_');
	if (lastIndex == -1) {
	    error("getUnitIndexName: bad unit name " + name);
	    return -1;
	}
	int index = Integer.parseInt(name.substring(lastIndex + 1));
	String type = name.substring(0, lastIndex);
	return getUnitIndex(type, index);
    }

    /**
     * Retrieves the extend selections setting.
     *
     * @return the extend selections setting
     */
    int getExtendSelections() {
	return extendSelections;
    }

    /**
     * Gets the next unit.
     *
     * @return the next unit
     */
    int getNextUnit(int which) {
	return units[which].next;
    }

    /**
     * Gets the previous units.
     *
     * @param which which unit is of interest
     *
     * @return the previous unit
     */
    int getPrevUnit(int which) {
	return units[which].prev;
    }


    /**
     * Determines if the unit types are equal.
     *
     * @param unitA the index of unit a
     * @param unitB the index of unit B
     *
     * @return <code>true</code> if the types of units a and b are
     *     equal; otherwise return <code>false</code> 
     */
    boolean isUnitTypeEqual(int unitA, int unitB)  {
	return units[unitA].type == units[unitB].type;
	// String nameA = units[unitA].getName();
	// String nameB = units[unitB].getName();
	// int lastUnderscore = nameA.lastIndexOf('_');
	// return nameA.regionMatches(0, nameB, 0, lastUnderscore + 1);
    }

    /**
     * Retrieves the optimal coupling setting.
     *
     * @return the optimal coupling setting
     */
    int getOptimalCoupling() {
	return optimalCoupling;
    }

    /**
     * Retrieves the continuity weight setting.
     *
     * 
     * @return  the continuity weight setting
     */
    int getContinuityWeight()  {
	return continuityWeight;
    }

    /**
     * Retrieves the join weights.
     *
     * @return  the join weights
     */
    int[] getJoinWeights() {
	return joinWeights;
    }


    /**
     * Looks up the unit with the given name.
     *
     * @param unitName the name of the unit to look for
     *
     * @return the unit or the defaultUnit if not found.
     */
    DatabaseClusterUnit getUnit(String unitName) {
	return null;
    }
    
    /**
     * Looks up the unit with the given index.
     *
     * @param index the index of the unit to look for
     *
     * @return the unit 
     */
    DatabaseClusterUnit getUnit(int which) {
	return units[which];
    }

    /**
     * Looks up the origin info for the unit with the given index.
     *
     * @param index the index of the unit to look for
     *
     * @return the origin info for the unit, or null if none is available 
     */
    UnitOriginInfo getUnitOriginInfo(int which) {
        if (unitOrigins != null)
            return unitOrigins[which];
        else
            return null;
    }

    
    /**
     * Returns the name of this UnitDatabase.
     *
     * @return the name of the database
     */
    String getName() {
	return "ClusterUnitDatabase";
    }
    
    /**
     * Returns the sample info for this set of data.
     *
     * @return the sample info
     */
    SampleInfo getSampleInfo() {
	return sts.getSampleInfo();
    }


    /**
     * Gets the sample list.
     *
     * @return the sample list
     */
    SampleSet getSts() {
	return sts;
    }

    /**
     * Gets the Mel Ceptra list.
     *
     * @return the Mel Ceptra list
     */
    SampleSet getMcep() {
	return mcep;
    }

    /**
     * Determines if the application of the given join weights could
     * be applied  as a simple right-shift. If so return the shift
     * otherwise return 0.
     *
     * @return the amount to right shift (or zero if not possible)
     */
    int getJoinWeightShift() {
	return joinWeightShift;
    }


    /**
     * Calculates the join weight shift.
     *
     * @param joinWeights the weights to check
     *
     * @return the amount to right shift (or zero if not possible)
     */
    private int calcJoinWeightShift(int[] joinWeights) {
	int first = joinWeights[0];
	for (int i = 1; i < joinWeights.length; i++) {
	    if (joinWeights[i] != first) {
		return 0;
	    }
	}

	int divisor = 65536 / first;
	if (divisor == 2) {
	    return 1;
	} else if (divisor == 4) {
	    return 2;
	}
	return 0;
    }

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


	unitList = new ArrayList();
	unitTypesList = new ArrayList();

	if (is == null) {
	    throw new Error("Can't load cluster 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();

	    units = new DatabaseClusterUnit[unitList.size()];
	    units = (DatabaseClusterUnit[]) unitList.toArray(units);
	    unitList = null;

	    unitTypes = new UnitType[unitTypesList.size()];
	    unitTypes = (UnitType[]) unitTypesList.toArray(unitTypes);
	    unitTypesList = null;

        } catch (IOException e) {
            throw new Error(e.getMessage() + " at line " + lineCount);
        } finally {
	}
    }

    /**
     * Parses and process the given line.
     *
     * @param line the line to process
     * @param reader the source for the lines
     *
     * @throws IOException if an error occurs while reading
     */
    private void parseAndAdd(String line, BufferedReader reader)
	throws IOException {
	try {
	    StringTokenizer tokenizer = new StringTokenizer(line," ");
	    String tag = tokenizer.nextToken();
	    if (tag.equals("CONTINUITY_WEIGHT")) {
		continuityWeight = Integer.parseInt(tokenizer.nextToken());
	    } else if (tag.equals("OPTIMAL_COUPLING")) {
		optimalCoupling = Integer.parseInt(tokenizer.nextToken());
	    }  else if (tag.equals("EXTEND_SELECTIONS")) {
		extendSelections = Integer.parseInt(tokenizer.nextToken());
	    }  else if (tag.equals("JOIN_METHOD")) {
		joinMethod = Integer.parseInt(tokenizer.nextToken());
	    }  else if (tag.equals("JOIN_WEIGHTS")) {
		int numWeights = Integer.parseInt(tokenizer.nextToken());
    		joinWeights = new int[numWeights];
		for (int i = 0; i < numWeights; i++) {
		    joinWeights[i]  = Integer.parseInt(tokenizer.nextToken());
		}

		joinWeightShift = calcJoinWeightShift(joinWeights);

	    } else if (tag.equals("STS")) {
		String name = tokenizer.nextToken();
		if (name.equals("STS")) {
		    sts = new SampleSet(tokenizer, reader);
		} else {
		    mcep = new SampleSet(tokenizer, reader);
		}
	    } else if (tag.equals("UNITS")) {
		int type = Integer.parseInt(tokenizer.nextToken());
		int phone = Integer.parseInt(tokenizer.nextToken());
		int start = Integer.parseInt(tokenizer.nextToken());
		int end = Integer.parseInt(tokenizer.nextToken());
		int prev = Integer.parseInt(tokenizer.nextToken());
		int next = Integer.parseInt(tokenizer.nextToken());
		DatabaseClusterUnit unit 
		      = new DatabaseClusterUnit(type, phone, start, 
			      end, prev, next);
		unitList.add(unit);
	    } else if (tag.equals("CART")) {
		String name = tokenizer.nextToken();
		int nodes = Integer.parseInt(tokenizer.nextToken());
		CART cart = new CARTImpl(reader, nodes);
		cartMap.put(name, cart);

		if (defaultCart == null) {
		    defaultCart = cart;
		}
	    } else if (tag.equals("UNIT_TYPE")) {
		String name = tokenizer.nextToken();
		int start = Integer.parseInt(tokenizer.nextToken());
		int count = Integer.parseInt(tokenizer.nextToken());
		UnitType unitType = new UnitType(name, start, count);
		unitTypesList.add(unitType);
	    } else {
		throw new Error("Unsupported tag " + tag + " in db line `" + line + "'");
	    }
	} catch (NoSuchElementException nse) {
	    throw new Error("Error parsing db " + nse.getMessage());
	} catch (NumberFormatException nfe) {
	    throw new Error("Error parsing numbers in db line `" + line + "':" + nfe.getMessage());
	}
    }

    /**
     * Loads a binary file from the input stream. 
     *
     * @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 (is instanceof FileInputStream) {
	    FileInputStream fis = (FileInputStream) is;
	    FileChannel fc = fis.getChannel();

	    MappedByteBuffer bb = 
		fc.map(FileChannel.MapMode.READ_ONLY, 0, (int) fc.size());
	    bb.load();
	    loadBinary(bb);
	    is.close();
	} else {
	    loadBinary(new DataInputStream(is));
	}
    }

    /**
     * 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 loadBinary(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");
	}

	continuityWeight = bb.getInt();
	optimalCoupling = bb.getInt();
	extendSelections = bb.getInt();
	joinMethod = bb.getInt();
	joinWeightShift = bb.getInt();

	int weightLength = bb.getInt();
	joinWeights = new int[weightLength];
	for (int i = 0; i < joinWeights.length; i++) {
	    joinWeights[i] = bb.getInt();
	}

	int unitsLength = bb.getInt();
	units = new DatabaseClusterUnit[unitsLength];
	for (int i = 0; i < units.length; i++) {
	    units[i] = new DatabaseClusterUnit(bb);
	}

	int unitTypesLength = bb.getInt();
	unitTypes = new UnitType[unitTypesLength];
	for (int i = 0; i < unitTypes.length; i++) {
	    unitTypes[i] = new UnitType(bb);
	}
	sts = new SampleSet(bb);
	mcep = new SampleSet(bb);

	int numCarts = bb.getInt();
	cartMap = new HashMap();
	for (int i = 0; i < numCarts; i++) {
	    String name = Utilities.getString(bb);
	    CART cart = CARTImpl.loadBinary(bb);
	    cartMap.put(name, cart);

	    if (defaultCart == null) {
		defaultCart = cart;
	    }
	}
    }

    /**
     * Loads the database from the given input stream.
     *
     * @param is the input stream to load the db from
     *
     * @throws IOException if there is trouble opening the DB
     */
    private void loadBinary(DataInputStream is) throws IOException {

	if (is.readInt() != MAGIC)  {
	    throw new Error("Bad magic in db");
	}
	if (is.readInt() != VERSION)  {
	    throw new Error("Bad VERSION in db");
	}

	continuityWeight = is.readInt();
	optimalCoupling = is.readInt();
	extendSelections = is.readInt();
	joinMethod = is.readInt();
	joinWeightShift = is.readInt();

	int weightLength = is.readInt();
	joinWeights = new int[weightLength];
	for (int i = 0; i < joinWeights.length; i++) {
	    joinWeights[i] = is.readInt();
	}

	int unitsLength = is.readInt();
	units = new DatabaseClusterUnit[unitsLength];
	for (int i = 0; i < units.length; i++) {
	    units[i] = new DatabaseClusterUnit(is);
	}

	int unitTypesLength = is.readInt();
	unitTypes = new UnitType[unitTypesLength];
	for (int i = 0; i < unitTypes.length; i++) {
	    unitTypes[i] = new UnitType(is);
	}
	sts = new SampleSet(is);
	mcep = new SampleSet(is);

	int numCarts = is.readInt();
	cartMap = new HashMap();
	for (int i = 0; i < numCarts; i++) {
	    String name = Utilities.getString(is);
	    CART cart = CARTImpl.loadBinary(is);
	    cartMap.put(name, cart);

	    if (defaultCart == null) {
		defaultCart = cart;
	    }
	}
    }
    
    /**
     * Load debug info about the origin of units from the given input stream.
     * The file format is identical to that of the Festvox .catalogue files.
     * This is useful when creating and debugging new voices: For a selected
     * unit, you can find out which unit from which original sound file
     * was used.
     * @param is the input stream from which to read the debug info.
     * @throws IOException if a read problem occurs.
     */
    private void loadUnitOrigins(InputStream is)  throws IOException
    {
        unitOrigins = new UnitOriginInfo[units.length];
        BufferedReader in = new BufferedReader(new InputStreamReader(is));
        
        String currentLine = null;
        // Skip EST header:
        while ((currentLine = in.readLine()) != null) {
            if (currentLine.startsWith("EST_Header_End")) break;
        }
        while ((currentLine = in.readLine()) != null) {
            String[] tokens = currentLine.split(" ");
            String name = tokens[0];
            int index = getUnitIndexName(name);
            try {
                unitOrigins[index] = new UnitOriginInfo();
                unitOrigins[index].originFile = tokens[1];
                unitOrigins[index].originStart = Float.valueOf(tokens[2]).floatValue();
                unitOrigins[index].originEnd = Float.valueOf(tokens[4]).floatValue();
            } catch (NumberFormatException nfe) {}
        }
        in.close();
    }


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

	    os.writeInt(MAGIC);
	    os.writeInt(VERSION);
	    os.writeInt(continuityWeight);
	    os.writeInt(optimalCoupling);
	    os.writeInt(extendSelections);
	    os.writeInt(joinMethod);
	    os.writeInt(joinWeightShift);
	    os.writeInt(joinWeights.length);
	    for (int i = 0; i < joinWeights.length; i++) {
		os.writeInt(joinWeights[i]);
	    }

	    os.writeInt(units.length);
	    for (int i = 0; i < units.length; i++) {
		units[i].dumpBinary(os);
	    }

	    os.writeInt(unitTypes.length);
	    for (int i = 0; i < unitTypes.length; i++) {
		unitTypes[i].dumpBinary(os);
	    }
	    sts.dumpBinary(os);
	    mcep.dumpBinary(os);

	    os.writeInt(cartMap.size());
	    for (Iterator i = cartMap.keySet().iterator(); i.hasNext();) {
		String name = (String) i.next();
		CART cart =  (CART) cartMap.get(name);

		Utilities.outString(os, name);
		cart.dumpBinary(os);
	    }
	    os.close();

	    // note that we are not currently saving the state
	    // of the default cart

	} 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());
	}
    }


    /**
     * Determines if two databases are identical.
     *
     * @param other the database to compare this one to
     *
     * @return true if the databases are identical
     */
    public boolean compare(ClusterUnitDatabase other) {
	System.out.println("Warning: Compare not implemented yet");
	return false;
    }

    /**
     *  Manipulates a ClusterUnitDatabase.  
     *
     * <p>
     * <b> Usage </b>
     * <p>
     *  <code> java com.sun.speech.freetts.clunits.ClusterUnitDatabase
     *  [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 = new BulkTimer();
		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 = "clunits.txt";
                         if (i + 1 < args.length) {
                             String nameArg = args[++i];
                             if (!nameArg.startsWith("-")) {
                                 name = nameArg;
                             }
			 }

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

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

			 System.out.println("Loading " + name);
			 timer.start("load_text");
			 ClusterUnitDatabase udb = new
			     ClusterUnitDatabase(
				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");

		    } else if (args[i].equals("-compare")) {

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

			timer.start("load_binary");
			ClusterUnitDatabase budb = 
			    new ClusterUnitDatabase(
				    new URL("file:./cmu_time_awb.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("ClusterUnitDatabase");
		}
	    } 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);
	}
    }


    /**
     * Represents a unit  for the cluster database.
     */
    class DatabaseClusterUnit {

	int type;
	int phone;
	int start;
	int end;
	int prev;
	int next;

	/**
	 * Constructs a unit.
	 *
	 * @param type the name of the unit
	 * @param phone the name of the unit
	 * @param start the starting frame
	 * @param end the ending frame
	 * @param prev the previous index
	 * @param next the next index
	 */
	DatabaseClusterUnit(int type, int phone, int start, 
		int end, int prev, int next) {
	    this.type = type;
	    this.phone = phone;
	    this.start = start;
	    this.end = end;
	    this.prev = prev;
	    this.next = next;
	}

	/**
	 * Creates a unit by reading it from the given byte buffer.
	 *
	 * @param bb source of the DatabaseClusterUnit data
	 *
	 * @throws IOException if an IO error occurs
	 */
	DatabaseClusterUnit(ByteBuffer bb) throws IOException {
	    this.type = bb.getInt();
	    this.phone = bb.getInt();
	    this.start = bb.getInt();
	    this.end = bb.getInt();
	    this.prev = bb.getInt();
	    this.next = bb.getInt();
	}

	/**
	 * Creates a unit by reading it from the given input stream.
	 *
	 * @param is source of the DatabaseClusterUnit data
	 *
	 * @throws IOException if an IO error occurs
	 */
	DatabaseClusterUnit(DataInputStream is) throws IOException {
	    this.type = is.readInt();
	    this.phone = is.readInt();
	    this.start = is.readInt();
	    this.end = is.readInt();
	    this.prev = is.readInt();
	    this.next = is.readInt();
	}

	/**
	 * Returns the name of the unit.
	 *
	 * @return the name
	 */
	String getName() {
	    return unitTypes[type].getName();
	}

	/**
	 * Dumps this unit to the given output stream.
	 *
	 * @param os the output stream
	 *
	 * @throws IOException if an error occurs.
	 */
	void dumpBinary(DataOutputStream os) throws IOException {
	    os.writeInt(type);
	    os.writeInt(phone);
	    os.writeInt(start);
	    os.writeInt(end);
	    os.writeInt(prev);
	    os.writeInt(next);
	}
    }
    
    /**
     * Represents debug information about the origin of a unit.
     */
    class UnitOriginInfo {
        String originFile;
        float originStart;
        float originEnd;
    }

    /**
     * Displays an error message
     *
     * @param s the error message
     */
    private void error(String s) {
	System.out.println("ClusterUnitDatabase Error: " + s);
    }
}

/**
 * Represents a unit type in the system
 */
class UnitType {
    private String name;
    private int start;
    private int count;

    /**
     * Constructs a UnitType from the given parameters
     *
     * @param name the name of the type
     * @param start the starting index for this type
     * @param count the number of elements for this type
     */
    UnitType(String name, int start, int count) {
	this.name = name;
	this.start = start;
	this.count = count;
    }

    /**
     * Creates a unit type by reading it from the given input stream.
     *
     * @param is source of the UnitType data
     *
     * @throws IOException if an IO error occurs
     */
    UnitType(DataInputStream is) throws IOException {
	this.name = Utilities.getString(is);
	this.start = is.readInt();
	this.count = is.readInt();
    }

    /**
     * Creates a unit type by reading it from the given byte buffer.
     *
     * @param bb source of the UnitType  data
     *
     * @throws IOException if an IO error occurs
     */
    UnitType(ByteBuffer bb) throws IOException {
	this.name = Utilities.getString(bb);
	this.start = bb.getInt();
	this.count = bb.getInt();
    }

    /**
     * Gets the name for this unit type
     * 
     * @return the name for the type
     */
    String getName() {
	return name;
    }

    /**
     * Gets the start index for this type
     *
     * @return the start index
     */
    int getStart() {
	return start;
    }

    /**
     * Gets the count for this type
     *
     * @return the  count for this type
     */
    int getCount() {
	return count;
    }

    /**
     * Dumps this unit to the given output stream.
     *
     * @param os the output stream
     *
     * @throws IOException if an error occurs.
     */
    void dumpBinary(DataOutputStream os) throws IOException {
	Utilities.outString(os, name);
	os.writeInt(start);
	os.writeInt(count);
    }
}
