/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.data.vpf.file;

import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.URI;
import java.util.AbstractList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;

import org.geotools.data.vpf.VPFColumn;
import org.geotools.data.vpf.exc.VPFHeaderFormatException;
import org.geotools.data.vpf.ifc.DataTypesDefinition;
import org.geotools.data.vpf.ifc.FileConstants;
import org.geotools.data.vpf.io.TripletId;
import org.geotools.data.vpf.io.VPFInputStream;
import org.geotools.data.vpf.io.VariableIndexInputStream;
import org.geotools.data.vpf.io.VariableIndexRow;
import org.geotools.data.vpf.util.DataUtils;
import org.geotools.feature.AttributeType;
import org.geotools.feature.DefaultFeatureType;
import org.geotools.feature.Feature;
import org.geotools.feature.FeatureType;
import org.geotools.feature.GeometryAttributeType;
import org.geotools.feature.IllegalAttributeException;
import org.geotools.feature.SchemaException;
import org.geotools.feature.type.AnnotationFeatureType;
import org.geotools.filter.CompareFilter;
import org.geotools.filter.Filter;
import org.geotools.filter.LengthFunction;
import org.geotools.filter.LiteralExpression;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateList;
import com.vividsolutions.jts.geom.DefaultCoordinateSequenceFactory;
import com.vividsolutions.jts.geom.GeometryFactory;


/**
 * This class encapsulates VPF files. By implementing the <code>FeatureType</code> interface,
 * it serves as a factory for VPFColumns. Instances of this class should
 * be created by VPFFileFactory.
 *
 * @author <a href="mailto:jeff@ionicenterprise.com">Jeff Yutzler</a>
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/vpf/src/main/java/org/geotools/data/vpf/file/VPFFile.java $
 */
public class VPFFile implements FeatureType, FileConstants, DataTypesDefinition {
    //    private final TableInputStream stream;
    private static String ACCESS_MODE = "r";

    /**
     * Variable <code>byteOrder</code> keeps value of  byte order in which
     * table is written:
     * 
     * <ul>
     * <li>
     * <b>L</b> - least-significant-first
     * </li>
     * <li>
     * <b>M</b> - most-significant-first
     * </li>
     * </ul>
     */
    private char byteOrder = LEAST_SIGNIF_FIRST;
    /**
     * The columns of the file. This list shall contain objects of type <code>VPFColumn</code>
     */
    private final List columns = new Vector();

    /**
     * Variable <code>description</code> keeps value of text description of the
     * table's contents.
     */
    private String description = null;
    /**
     * The contained Feature Type
     */
    private final FeatureType featureType;

    /**
     * Keeps value of length of ASCII header
     * string (i.e., the remaining information after this field)
     */
    private int headerLength = -0;

    /** The associated stream */
    private RandomAccessFile inputStream = null;

    /**
     * Variable <code>narrativeTable</code> keeps value of  an optional
     * narrative file which contains miscellaneous information about the
     * table.
     */
    private String narrativeTable = null;

    /** The path name */
    private final String pathName;

    /** Describe variable <code>variableIndex</code> here. */
    private VPFInputStream variableIndex = null;

    /**
     * Constructor.
     *
     * @param cPathName The path to this file
     *
     * @throws IOException if the path or the file are invalid
     * @throws SchemaException if the contained feature type can not be
     *         constructed
     */
    public VPFFile(String cPathName) throws IOException, SchemaException {
        pathName = cPathName;
        inputStream = new RandomAccessFile(cPathName, ACCESS_MODE);
        readHeader();

        GeometryAttributeType gat = null;
        VPFColumn geometryColumn = null;

        Iterator iter = columns.iterator();
        while (iter.hasNext()) {
            geometryColumn = (VPFColumn) iter.next();

            if (geometryColumn.isGeometry()) {
                gat = geometryColumn.getGeometryAttributeType();

                break;
            }
        }

        Vector superTypes = new Vector();
        // if it's a text geometry feature type add annotation as a super type
        if (pathName.endsWith(TEXT_PRIMITIVE)) {
            superTypes.add( AnnotationFeatureType.ANNOTATION );
        }
          
        featureType = new DefaultFeatureType(cPathName, "VPF", columns,
                superTypes, gat);
    }

    /**
     * Create a new feature from the provided attributes It is unclear why one
     * would want to use this method.
     *
     * @param attributes the attributes to used
     *
     * @return the created feature
     *
     * @throws IllegalAttributeException if any of the attributes is invalid
     */
    public Feature create(Object[] attributes) throws IllegalAttributeException {
        return featureType.create(attributes);
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureFactory#create(java.lang.Object[], java.lang.String)
     */
    public Feature create(Object[] attributes, String featureID)
        throws IllegalAttributeException {
        return featureType.create(attributes, featureID);
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#duplicate(org.geotools.feature.Feature)
     */
    public Feature duplicate(Feature feature) throws IllegalAttributeException {
        return featureType.duplicate(feature);
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#find(org.geotools.feature.AttributeType)
     */
    public int find(AttributeType type) {
        return featureType.find(type);
    }

    /**
     * Gets the value of full length of ASCII header string including
     * <code>headerLength</code> field.
     *
     * @return the value of headerLength
     */
    private int getAdjustedHeaderLength() {
        return this.headerLength + 4;
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#getAncestors()
     */
    public FeatureType[] getAncestors() {
        return featureType.getAncestors();
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#getAttributeCount()
     */
    public int getAttributeCount() {
        return featureType.getAttributeCount();
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#getAttributeType(int)
     */
    public AttributeType getAttributeType(int position) {
        return featureType.getAttributeType(position);
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#getAttributeType(java.lang.String)
     */
    public AttributeType getAttributeType(String xPath) {
        return featureType.getAttributeType(xPath);
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#getAttributeTypes()
     */
    public AttributeType[] getAttributeTypes() {
        return featureType.getAttributeTypes();
    }

    /**
     * Gets the value of byteOrder variable. Byte order in which table is
     * written:
     * 
     * <ul>
     * <li>
     * <b>L</b> - least-significant-first
     * </li>
     * <li>
     * <b>M</b> - most-significant-first
     * </li>
     * </ul>
     * 
     *
     * @return the value of byteOrder
     */
    public char getByteOrder() {
        return byteOrder;
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#getDefaultGeometry()
     */
    public GeometryAttributeType getDefaultGeometry() {
        return featureType.getDefaultGeometry();
    }

    /**
     * Gets the value of the description of table content. This is nice to
     * have, but I don't know how to make use of it.
     *
     * @return the value of description
     */
    public String getDescription() {
        return description;
    }

    /**
     * Returns the directory name for this file by chopping off the file name
     * and the separator.
     *
     * @return the directory name for this file
     */
    public String getDirectoryName() {
        String result = new String();
        int index = pathName.lastIndexOf(File.separator);

        if (index >= 0) {
            result = pathName.substring(0, index);
        }

        return result;
    }

    /**
     * Returns the file name (without path) for the file
     *
     * @return the file name for this file
     */
    public String getFileName() {
        String result = pathName.substring(pathName.lastIndexOf(File.separator)
                + 1);

        return result;
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#getNamespace()
     */
    public URI getNamespace() {
        return featureType.getNamespace();
    }

//    /*
//     *  (non-Javadoc)
//     * @see org.geotools.feature.FeatureType#getNamespace()
//     */
//    public URI getNamespaceURI() {
//        return featureType.getNamespaceURI();
//    }

    /**
     * Gets the value of narrativeTable variable file name.
     *
     * @return the value of narrativeTable
     */
    public String getNarrativeTable() {
        return narrativeTable;
    }

    /**
     * Gets the full path name for this file
     *
     * @return the path name for this file
     */
    public String getPathName() {
        return pathName;
    }

    /**
     * Method <code><code>getRecordSize</code></code> is used to return size in
     * bytes of records stored in this table. If table keeps variable length
     * records <code>-1</code> should be returned.
     *
     * @return an <code><code>int</code></code> value
     */
    protected int getRecordSize() {
        int size = 0;
//        int currentSize;

        Iterator iter = columns.iterator();

        while (iter.hasNext()) {
            VPFColumn column = (VPFColumn) iter.next();
//            currentSize = column.getFieldLength();
            if (column.getRestriction() == Filter.INCLUDE || column.getRestriction() == null) {
//            if (currentSize < 0) {
                return -1;
            } else {
            	int currentSize = 0;
            	if(column.getRestriction() instanceof CompareFilter){
            		try{
            		CompareFilter cf = (CompareFilter)column.getRestriction();
            		if(cf.getLeftValue() instanceof LengthFunction){
            			currentSize = Integer.parseInt(((LiteralExpression)cf.getRightValue()).getLiteral().toString());
            		}else{
                		if(cf.getRightValue() instanceof LengthFunction)
                			currentSize = Integer.parseInt(((LiteralExpression)cf.getLeftValue()).getLiteral().toString());
            		}
            		}catch(Throwable t){}
            	}
                size += currentSize;
            }
        }

        return size;
    }

    /**
     * Returns a row with a matching value for the provided column
     *
     * @param idName The name of the column to look for, such as "id"
     * @param id An identifier for the requested row
     *
     * @return The first row which matches the ID
     *
     * @throws IllegalAttributeException The feature can not be created due to
     *         illegal attributes in the source file
     */
    public Feature getRowFromId(String idName, int id)
        throws IllegalAttributeException {
        Feature result = null;

        try {
            // This speeds things up mightily
            String firstColumnName = ((VPFColumn) columns.get(0)).getName();

            if (idName.equals(firstColumnName)) {
                setPosition(id);
                result = readFeature();

                Number value = (Number) result.getAttribute(idName);

                // Check to make sure we got a primary key match
                if ((value == null) || (value.intValue() != id)) {
                    result = null;
                }
            }

            if (result == null) {
                Iterator joinedIter = readAllRows().iterator();
                result = getRowFromIterator(joinedIter, idName, id);
            }
        } catch (IOException exc) {
            exc.printStackTrace();
        }

        return result;
    }

    /**
     * Returns a single matching row from the Iterator,  ignoring rows that do
     * not match a particular id
     *
     * @param iter the iterator to examine
     * @param idName The name of the column to check
     * @param id The value of the column to check
     *
     * @return a TableRow that matches the other parameters
     */
    private Feature getRowFromIterator(Iterator iter, String idName, int id) {
        Feature result = null;
        Feature currentFeature;
        int value = -1;

        while (iter.hasNext()) {
            currentFeature = (Feature) iter.next();
            try {
                value = Integer.parseInt(currentFeature.getAttribute(idName).toString());

                if (id == value) {
                    result = currentFeature;

                    break;
                }
            } catch (NumberFormatException exc) {
                // If this happens, the data is invalid so dumping a stack trace seems reasonable
                exc.printStackTrace();
            }
        }

        return result;
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#getTypeName()
     */
    public String getTypeName() {
        return featureType.getTypeName();
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#hasAttributeType(java.lang.String)
     */
    public boolean hasAttributeType(String xPath) {
        return featureType.hasAttributeType(xPath);
    }

    /**
     * Determines if the stream contains storage for another object. Who knows
     * how well this will work on variable length objects?
     *
     * @return a <code>boolean</code> 
     */
    public boolean hasNext() {
        boolean result = false;

        try {
            int recordSize = getRecordSize();

            if (recordSize > 0) {
                result = inputStream.length() >= (inputStream.getFilePointer() + recordSize);
            } else {
                result = inputStream.length() >= (inputStream.getFilePointer() + 1);
            }
        } catch (IOException exc) {
            // No idea what to do if this happens
            exc.printStackTrace();
        }

        return result;
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#isAbstract()
     */
    public boolean isAbstract() {
        return featureType.isAbstract();
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#isDescendedFrom(org.geotools.feature.FeatureType)
     */
    public boolean isDescendedFrom(FeatureType type) {
        return featureType.isDescendedFrom(type);
    }

    /*
     *  (non-Javadoc)
     * @see org.geotools.feature.FeatureType#isDescendedFrom(java.lang.String, java.lang.String)
     */
    public boolean isDescendedFrom(URI nsURI, String typeName) {
        return featureType.isDescendedFrom(nsURI, typeName);
    }

    /**
     * Generates a list containing all of the features in the file
     *
     * @return a <code>List</code> value containing Feature objects
     *
     * @exception IOException if an error occurs
     */
    public AbstractList readAllRows() throws IOException {
        AbstractList list = new LinkedList();

        try {
            setPosition(1);
        } catch (IOException exc) {
            // This indicates that there are no rows
            return list;
        }

        try {
            Feature row = readFeature();

            while (row != null) {
                list.add(row);
                if(hasNext()){
                    row = readFeature();
                }else{
                    row = null;
                }
            }
        } catch (IllegalAttributeException exc1) {
            throw new IOException(exc1.getMessage());
        }

        return list;
    }

    /**
     * Reads a single byte as a character value
     *
     * @return a <code>char</code> value
     *
     * @exception IOException if an error occurs
     */
    protected char readChar() throws IOException {
        return (char) inputStream.read();
    }

    /**
     * Reads a column definition from the file header
     *
     * @return a <code>VPFColumn</code> value
     *
     * @exception VPFHeaderFormatException if an error occurs
     * @exception IOException if an error occurs
     * @exception NumberFormatException if an error occurs
     */
    private VPFColumn readColumn()
        throws VPFHeaderFormatException, IOException, NumberFormatException {
        char ctrl = readChar();

        if (ctrl == VPF_RECORD_SEPARATOR) {
            return null;
        }

        String name = ctrl + readString("=");
        char type = readChar();
        ctrl = readChar();

        if (ctrl != VPF_ELEMENT_SEPARATOR) {
            throw new VPFHeaderFormatException(
                "Header format does not fit VPF file definition.");
        }

        String elemStr = readString(new String() + VPF_ELEMENT_SEPARATOR).trim();

        if (elemStr.equals("*")) {
            elemStr = "-1";
        }

        int elements = Integer.parseInt(elemStr);
        char key = readChar();
        ctrl = readChar();

        if (ctrl != VPF_ELEMENT_SEPARATOR) {
            throw new VPFHeaderFormatException(
                "Header format does not fit VPF file definition.");
        }

        String colDesc = readString(new String() + VPF_ELEMENT_SEPARATOR
                + VPF_FIELD_SEPARATOR);
        String descTableName = readString(new String() + VPF_ELEMENT_SEPARATOR
                + VPF_FIELD_SEPARATOR);
        String indexFile = readString(new String() + VPF_ELEMENT_SEPARATOR
                + VPF_FIELD_SEPARATOR);
        String narrTable = readString(new String() + VPF_ELEMENT_SEPARATOR
                + VPF_FIELD_SEPARATOR);

        return new VPFColumn(name, type, elements, key, colDesc, descTableName,
            indexFile, narrTable);
    }
    /**
     * Constructs an object which is an instance of Geometry 
     * by reading values from the file.
     * @param instancesCount number of coordinates to read
     * @param dimensionality either 2 or 3
     * @param readDoubles true: read a double value; false: read a float value
     * @return the constructed object
     * @throws IOException on any file IO errors
     */
    protected Object readGeometry(int instancesCount, int dimensionality,
        boolean readDoubles) throws IOException {
        Object result = null;
        Coordinate coordinate = null;
        CoordinateList coordinates = new CoordinateList();
        GeometryFactory factory = new GeometryFactory();

        for (int inx = 0; inx < instancesCount; inx++) {
            switch (dimensionality) {
            case 2:
                coordinate = new Coordinate(readDoubles ? readDouble()
                                                        : readFloat(),
                        readDoubles ? readDouble() : readFloat());

                break;

            case 3:
                coordinate = new Coordinate(readDoubles ? readDouble()
                                                        : readFloat(),
                        readDoubles ? readDouble() : readFloat(),
                        readDoubles ? readDouble() : readFloat());

                break;

            default:
                //WTF???
            }

            coordinates.add(coordinate);
        }

        // Special handling for text primitives per the VPF spec.
        // The first 2 points are the endpoints of the line, the following
        // points fill in between the first 2 points.  
        if (pathName.endsWith(TEXT_PRIMITIVE) && coordinates.size() > 2) {
            Object o = coordinates.remove(1);
            coordinates.add(o);
        }

        if (instancesCount == 1) {
            result = factory.createPoint(coordinate);
        } else {
            result = factory.createLineString(DefaultCoordinateSequenceFactory.instance()
                                                                              .create(coordinates
                        .toCoordinateArray()));
        }

        return result;
    }

    /**
     * Retrieves a double from the file
     *
     * @return a <code>double</code> value
     *
     * @exception IOException if an error occurs
     */
    protected double readDouble() throws IOException {
        return DataUtils.decodeDouble(readNumber(DATA_LONG_FLOAT_LEN));
    }

    /**
     * Retrieves a feature from the file
     *
     * @return the retieved feature
     *
     * @throws IOException on any file IO errors
     * @throws IllegalAttributeException if any of the attributes retrieved are
     *         illegal
     */
    public Feature readFeature() throws IOException, IllegalAttributeException {
        Feature result = null;
        Iterator iter = columns.iterator();
        VPFColumn column;
        boolean textPrimitive = pathName.endsWith(TEXT_PRIMITIVE);
        int size = columns.size();
        if (textPrimitive) size++;
        Object[] values = new Object[size];
        
        try {
            for (int inx = 0; inx < columns.size(); inx++) {
                column = (VPFColumn) columns.get(inx);

                if (column.getRestriction() == Filter.INCLUDE || column.getRestriction() == null) {
//                    if (column.getFieldLength() < 0) {
                    values[inx] = readVariableSizeData(column.getTypeChar());
                } else {
                    values[inx] = readFixedSizeData(column.getTypeChar(),
                            column.getElementsNumber());
                }
            }
            if (textPrimitive) {
                values[size-1] = "nam";
            }

            result = featureType.create(values);
        } catch (EOFException exp) {
            // Should we be throwing an exception instead of eating it?
            exp.printStackTrace();
        }

        return result;
    }

    /**
     * Retrieves a fixed amount of data from the file
     *
     * @param dataType a <code>char</code> value indicating the data type
     * @param instancesCount an <code>int</code> value indicating the number 
     * of instances to retrieve.
     *
     * @return an <code>Object</code> value
     *
     * @exception IOException if an error occurs
     */
    protected Object readFixedSizeData(char dataType, int instancesCount)
        throws IOException {
        Object result = null;

        switch (dataType) {
        case DATA_TEXT:
        case DATA_LEVEL1_TEXT:
        case DATA_LEVEL2_TEXT:
        case DATA_LEVEL3_TEXT:

            byte[] dataBytes = new byte[instancesCount * DataUtils
                .getDataTypeSize(dataType)];
            inputStream.readFully(dataBytes);
            result = DataUtils.decodeData(dataBytes, dataType);

            break;

        case DATA_SHORT_FLOAT:
            result = new Float(readFloat());

            break;

        case DATA_LONG_FLOAT:
            result = new Double(readDouble());

            break;

        case DATA_SHORT_INTEGER:
            result = new Short(readShort());

            break;

        case DATA_LONG_INTEGER:
            result = new Integer(readInteger());

            break;

        case DATA_NULL_FIELD:
            result = "NULL";

            break;

        case DATA_TRIPLET_ID:
            result = readTripletId();

            break;

        case DATA_2_COORD_F:
            result = readGeometry(instancesCount, 2, false);

            break;

        case DATA_2_COORD_R:
            result = readGeometry(instancesCount, 2, true);

            break;

        case DATA_3_COORD_F:
            result = readGeometry(instancesCount, 3, false);

            break;

        case DATA_3_COORD_R:
            result = readGeometry(instancesCount, 3, true);

            break;

        default:
            break;
        } // end of switch (dataType)

        return result;
    }

    /**
     * Retrieves a floating point number from the file.
     *
     * @return a <code>float</code> value
     *
     * @exception IOException if an error occurs
     */
    protected float readFloat() throws IOException {
        return DataUtils.decodeFloat(readNumber(DATA_SHORT_FLOAT_LEN));
    }

    /**
     * Retrieves a number of attributes from the file header
     *
     * @exception VPFHeaderFormatException if an error occurs
     * @exception IOException if an error occurs
     */
    protected void readHeader() throws VPFHeaderFormatException, IOException {
        byte[] fourBytes = new byte[4];
        inputStream.readFully(fourBytes);

        byteOrder = readChar();

        char ctrl = byteOrder;

        if (byteOrder == VPF_RECORD_SEPARATOR) {
            byteOrder = LITTLE_ENDIAN_ORDER;
        } else {
            ctrl = readChar();
        }

        if (byteOrder == LITTLE_ENDIAN_ORDER) {
            fourBytes = DataUtils.toBigEndian(fourBytes);
        }

        headerLength = DataUtils.decodeInt(fourBytes);

        if (ctrl != VPF_RECORD_SEPARATOR) {
            throw new VPFHeaderFormatException(
                "Header format does not fit VPF file definition.");
        }

        description = readString(new String() + VPF_RECORD_SEPARATOR);
        narrativeTable = readString(new String() + VPF_RECORD_SEPARATOR);

        VPFColumn column = readColumn();

        while (column != null) {
            columns.add(column);
            ctrl = readChar();

            if (ctrl != VPF_FIELD_SEPARATOR) {
                throw new VPFHeaderFormatException(
                    "Header format does not fit VPF file definition.");
            }

            column = readColumn();
        }

        if (getRecordSize() < 0) {
            variableIndex = new VariableIndexInputStream(getVariableIndexFileName(),
                    getByteOrder());
        }
    }

    /**
     * Retrieves an integer value from the file
     *
     * @return an <code>int</code> value
     *
     * @exception IOException if an error occurs
     */
    protected int readInteger() throws IOException {
        return DataUtils.decodeInt(readNumber(DATA_LONG_INTEGER_LEN));
    }

    /**
     * Reads some byte data from the file
     *
     * @param cnt an <code>int</code> value indicating the number of bytes to retrieve
     *
     * @return a <code>byte[]</code> value
     *
     * @throws IOException if an error occurs
     */
    protected byte[] readNumber(int cnt) throws IOException {
        byte[] dataBytes = new byte[cnt];
        inputStream.readFully(dataBytes);

        if (byteOrder == LITTLE_ENDIAN_ORDER) {
            dataBytes = DataUtils.toBigEndian(dataBytes);
        }

        return dataBytes;
    }

    /**
     * Retrieves a short value from the file
     *
     * @return a <code>short</code> value
     *
     * @exception IOException if an error occurs
     */
    protected short readShort() throws IOException {
        return DataUtils.decodeShort(readNumber(DATA_SHORT_INTEGER_LEN));
    }

    /**
     * Reads a string value from the file
     *
     * @param terminators a <code>String</code> value indicating the terminators to look for
     *
     * @return a <code>String</code> value
     *
     * @exception IOException if an error occurs
     */
    protected String readString(String terminators) throws IOException {
        StringBuffer text = new StringBuffer();
        char ctrl = readChar();

        if (terminators.indexOf(ctrl) != -1) {
            if (ctrl == VPF_FIELD_SEPARATOR) {
                unread(1);
            }

            return null;
        }

        while (terminators.indexOf(ctrl) == -1) {
            text.append(ctrl);
            ctrl = readChar();
        }

        if (text.toString().equals(STRING_NULL_VALUE)) {
            return null;
        } else {
            return text.toString();
        }
    }

    /**
     * Retrieves a triplet object from the file
     *
     * @return a <code>TripletId</code> value
     * 
     * @throws IOException on any IO errors
     */
    protected TripletId readTripletId() throws IOException {
// TODO: does this take into account byte order properly?
        byte tripletDef = (byte) inputStream.read();
        int dataSize = TripletId.calculateDataSize(tripletDef);
        byte[] tripletData = new byte[dataSize + 1];
        tripletData[0] = tripletDef;

        if (dataSize > 0) {
            inputStream.readFully(tripletData, 1, dataSize);
        }

        return new TripletId(tripletData);
    }

    /**
     * Retrieves variable sized data from the file by first reading an integer
     * which indicates how many instances of the data type to retrieve
     *
     * @param dataType a <code>char</code> value indicating the data type
     *
     * @return an <code>Object</code> value
     *
     * @exception IOException if an error occurs
     */
    protected Object readVariableSizeData(char dataType)
        throws IOException {
        int instances = readInteger();

        return readFixedSizeData(dataType, instances);
    }

    /**
     * Resets the file stream by setting its pointer 
     * to the first position after the header.
     *
     */
    public void reset() {
        try {
            setPosition(1);
        } catch (IOException exc) {
            // This just means there is nothing in the table
        }
    }
    /**
     * Close the input stream pointed to by the object
     *  @throws IOException in some unlikely situation
     */
    public void close() throws IOException{
        inputStream.close();
        if (variableIndex != null) {
            variableIndex.close();
        }
    }
    /**
     * Sets the position in the stream
     *
     * @param pos A 1-indexed position
     *
     * @throws IOException on any IO failures
     */
    protected void setPosition(long pos) throws IOException {
        if (getRecordSize() < 0) {
            VariableIndexRow varRow = (VariableIndexRow) variableIndex.readRow((int) pos);
            inputStream.seek(varRow.getOffset());
        } else {
            inputStream.seek(getAdjustedHeaderLength()
                + ((pos - 1) * getRecordSize()));
        }
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    public String toString() {
        return featureType.toString();
    }

    /**
     * Back up a specified number of bytes in the file stream
     *
     * @param bytes a <code>long</code> value
     *
     * @exception IOException if an error occurs
     */
    protected void unread(long bytes) throws IOException {
        inputStream.seek(inputStream.getFilePointer() - bytes);
    }

    /**
     * Returns the full path of the variable index associated with the current file
     *
     * @return a <code>String</code> value
     */
    private String getVariableIndexFileName() {
        String result = null;
        String fileName = getFileName();

        if (fileName.toLowerCase().equals(FEATURE_CLASS_SCHEMA_TABLE)) {
            result = getDirectoryName().concat(File.separator).concat("fcz");
        } else {
            result = getDirectoryName().concat(File.separator).concat(fileName
                    .substring(0, fileName.length() - 1) + "x");
        }

        return result;
    }

	/* (non-Javadoc)
	 * @see org.geotools.feature.FeatureType#find(java.lang.String)
	 */
	public int find(String attName) {
        return featureType.find(attName);
	}
}
