/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.fontbox.afm;

import java.io.InputStream;
import java.io.IOException;

import java.util.StringTokenizer;

import org.apache.fontbox.util.BoundingBox;

/**
 * This class is used to parse AFM(Adobe Font Metrics) documents.
 *
 * @see <A href="http://partners.adobe.com/asn/developer/type/">AFM Documentation</A>
 *
 * @author Ben Litchfield (ben@benlitchfield.com)
 * @version $Revision: 1.1 $
 */
public class AFMParser
{
    /**
     * This is a comment in a AFM file.
     */
    public static final String COMMENT = "Comment";
    /**
     * This is the constant used in the AFM file to start a font metrics item.
     */
    public static final String START_FONT_METRICS = "StartFontMetrics";
    /**
     * This is the constant used in the AFM file to end a font metrics item.
     */
    public static final String END_FONT_METRICS = "EndFontMetrics";
    /**
     * This is the font name.
     */
    public static final String FONT_NAME = "FontName";
    /**
     * This is the full name.
     */
    public static final String FULL_NAME = "FullName";
    /**
     * This is the Family name.
     */
    public static final String FAMILY_NAME = "FamilyName";
    /**
     * This is the weight.
     */
    public static final String WEIGHT = "Weight";
    /**
     * This is the font bounding box.
     */
    public static final String FONT_BBOX = "FontBBox";
    /**
     * This is the version of the font.
     */
    public static final String VERSION = "Version";
    /**
     * This is the notice.
     */
    public static final String NOTICE = "Notice";
    /**
     * This is the encoding scheme.
     */
    public static final String ENCODING_SCHEME = "EncodingScheme";
    /**
     * This is the mapping scheme.
     */
    public static final String MAPPING_SCHEME = "MappingScheme";
    /**
     * This is the escape character.
     */
    public static final String ESC_CHAR = "EscChar";
    /**
     * This is the character set.
     */
    public static final String CHARACTER_SET = "CharacterSet";
    /**
     * This is the characters attribute.
     */
    public static final String CHARACTERS = "Characters";
    /**
     * This will determine if this is a base font.
     */
    public static final String IS_BASE_FONT = "IsBaseFont";
    /**
     * This is the V Vector attribute.
     */
    public static final String V_VECTOR = "VVector";
    /**
     * This will tell if the V is fixed.
     */
    public static final String IS_FIXED_V = "IsFixedV";
    /**
     * This is the cap height attribute.
     */
    public static final String CAP_HEIGHT = "CapHeight";
    /**
     * This is the X height.
     */
    public static final String X_HEIGHT = "XHeight";
    /**
     * This is ascender attribute.
     */
    public static final String ASCENDER = "Ascender";
    /**
     * This is the descender attribute.
     */
    public static final String DESCENDER = "Descender";

    /**
     * The underline position.
     */
    public static final String UNDERLINE_POSITION = "UnderlinePosition";
    /**
     * This is the Underline thickness.
     */
    public static final String UNDERLINE_THICKNESS = "UnderlineThickness";
    /**
     * This is the italic angle.
     */
    public static final String ITALIC_ANGLE = "ItalicAngle";
    /**
     * This is the char width.
     */
    public static final String CHAR_WIDTH = "CharWidth";
    /**
     * This will determine if this is fixed pitch.
     */
    public static final String IS_FIXED_PITCH = "IsFixedPitch";
    /**
     * This is the start of character metrics.
     */
    public static final String START_CHAR_METRICS = "StartCharMetrics";
    /**
     * This is the end of character metrics.
     */
    public static final String END_CHAR_METRICS = "EndCharMetrics";
    /**
     * The character metrics c value.
     */
    public static final String CHARMETRICS_C = "C";
    /**
     * The character metrics c value.
     */
    public static final String CHARMETRICS_CH = "CH";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_WX = "WX";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_W0X = "W0X";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_W1X = "W1X";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_WY = "WY";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_W0Y = "W0Y";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_W1Y = "W1Y";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_W = "W";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_W0 = "W0";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_W1 = "W1";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_VV = "VV";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_N = "N";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_B = "B";
    /**
     * The character metrics value.
     */
    public static final String CHARMETRICS_L = "L";
    /**
     * The character metrics value.
     */
    public static final String STD_HW = "StdHW";
    /**
     * The character metrics value.
     */
    public static final String STD_VW = "StdVW";
    /**
     * This is the start of track kern data.
     */
    public static final String START_TRACK_KERN = "StartTrackKern";
    /**
     * This is the end of track kern data.
     */
    public static final String END_TRACK_KERN = "EndTrackKern";
    /**
     * This is the start of kern data.
     */
    public static final String START_KERN_DATA = "StartKernData";
    /**
     * This is the end of kern data.
     */
    public static final String END_KERN_DATA = "EndKernData";
    /**
     * This is the start of kern pairs data.
     */
    public static final String START_KERN_PAIRS = "StartKernPairs";
    /**
     * This is the end of kern pairs data.
     */
    public static final String END_KERN_PAIRS = "EndKernPairs";
    /**
     * This is the start of kern pairs data.
     */
    public static final String START_KERN_PAIRS0 = "StartKernPairs0";
    /**
     * This is the start of kern pairs data.
     */
    public static final String START_KERN_PAIRS1 = "StartKernPairs1";
    /**
     * This is the start compisites data section.
     */
    public static final String START_COMPOSITES = "StartComposites";
    /**
     * This is the end compisites data section.
     */
    public static final String END_COMPOSITES = "EndComposites";
    /**
     * This is a composite character.
     */
    public static final String CC = "CC";
    /**
     * This is a composite charater part.
     */
    public static final String PCC = "PCC";
    /**
     * This is a kern pair.
     */
    public static final String KERN_PAIR_KP = "KP";
    /**
     * This is a kern pair.
     */
    public static final String KERN_PAIR_KPH = "KPH";
    /**
     * This is a kern pair.
     */
    public static final String KERN_PAIR_KPX = "KPX";
    /**
     * This is a kern pair.
     */
    public static final String KERN_PAIR_KPY = "KPY";

    private static final int BITS_IN_HEX = 16;


    private InputStream input;
    private FontMetric result;

    /**
     * A method to test parsing of all AFM documents in the resources
     * directory.
     *
     * @param args Ignored.
     *
     * @throws IOException If there is an error parsing one of the documents.
     */
    public static void main( String[] args ) throws IOException
    {
        java.io.File afmDir = new java.io.File( "Resources/afm" );
        java.io.File[] files = afmDir.listFiles();
        for( int i=0; i< files.length; i++ )
        {
            if( files[i].getPath().toUpperCase().endsWith( ".AFM" ) )
            {
                long start = System.currentTimeMillis();
                java.io.FileInputStream input = new java.io.FileInputStream( files[i] );
                AFMParser parser = new AFMParser( input );
                parser.parse();
                long stop = System.currentTimeMillis();
                System.out.println( "Parsing:" + files[i].getPath() + " " + (stop-start) );
            }
        }
    }

    /**
     * Constructor.
     *
     * @param in The input stream to read the AFM document from.
     */
    public AFMParser( InputStream in )
    {
        input = in;
    }

    /**
     * This will parse the AFM document.  This will close the Input stream
     * when the parsing is finished.
     *
     * @throws IOException If there is an IO error reading the document.
     */
    public void parse() throws IOException
    {
        result = parseFontMetric();
    }

    /**
     * This will get the result of the parsing.
     *
     * @return The parsed java object.
     */
    public FontMetric getResult()
    {
        return result;
    }

    /**
     * This will parse a font metrics item.
     *
     * @return The parse font metrics item.
     *
     * @throws IOException If there is an error reading the AFM file.
     */
    private FontMetric parseFontMetric() throws IOException
    {
        FontMetric fontMetrics = new FontMetric();
        String startFontMetrics = readString();
        if( !START_FONT_METRICS.equals( startFontMetrics ) )
        {
            throw new IOException( "Error: The AFM file should start with " + START_FONT_METRICS +
                                   " and not '" + startFontMetrics + "'" );
        }
        fontMetrics.setAFMVersion( readFloat() );
        String nextCommand = null;
        while( !END_FONT_METRICS.equals( (nextCommand = readString() ) ) )
        {
            if( FONT_NAME.equals( nextCommand ) )
            {
                fontMetrics.setFontName( readLine() );
            }
            else if( FULL_NAME.equals( nextCommand ) )
            {
                fontMetrics.setFullName( readLine() );
            }
            else if( FAMILY_NAME.equals( nextCommand ) )
            {
                fontMetrics.setFamilyName( readLine() );
            }
            else if( WEIGHT.equals( nextCommand ) )
            {
                fontMetrics.setWeight( readLine() );
            }
            else if( FONT_BBOX.equals( nextCommand ) )
            {
                BoundingBox bBox = new BoundingBox();
                bBox.setLowerLeftX( readFloat() );
                bBox.setLowerLeftY( readFloat() );
                bBox.setUpperRightX( readFloat() );
                bBox.setUpperRightY( readFloat() );
                fontMetrics.setFontBBox( bBox );
            }
            else if( VERSION.equals( nextCommand ) )
            {
                fontMetrics.setFontVersion( readLine() );
            }
            else if( NOTICE.equals( nextCommand ) )
            {
                fontMetrics.setNotice( readLine() );
            }
            else if( ENCODING_SCHEME.equals( nextCommand ) )
            {
                fontMetrics.setEncodingScheme( readLine() );
            }
            else if( MAPPING_SCHEME.equals( nextCommand ) )
            {
                fontMetrics.setMappingScheme( readInt() );
            }
            else if( ESC_CHAR.equals( nextCommand ) )
            {
                fontMetrics.setEscChar( readInt() );
            }
            else if( CHARACTER_SET.equals( nextCommand ) )
            {
                fontMetrics.setCharacterSet( readLine() );
            }
            else if( CHARACTERS.equals( nextCommand ) )
            {
                fontMetrics.setCharacters( readInt() );
            }
            else if( IS_BASE_FONT.equals( nextCommand ) )
            {
                fontMetrics.setIsBaseFont( readBoolean() );
            }
            else if( V_VECTOR.equals( nextCommand ) )
            {
                float[] vector = new float[2];
                vector[0] = readFloat();
                vector[1] = readFloat();
                fontMetrics.setVVector( vector );
            }
            else if( IS_FIXED_V.equals( nextCommand ) )
            {
                fontMetrics.setIsFixedV( readBoolean() );
            }
            else if( CAP_HEIGHT.equals( nextCommand ) )
            {
                fontMetrics.setCapHeight( readFloat() );
            }
            else if( X_HEIGHT.equals( nextCommand ) )
            {
                fontMetrics.setXHeight( readFloat() );
            }
            else if( ASCENDER.equals( nextCommand ) )
            {
                fontMetrics.setAscender( readFloat() );
            }
            else if( DESCENDER.equals( nextCommand ) )
            {
                fontMetrics.setDescender( readFloat() );
            }
            else if( STD_HW.equals( nextCommand ) )
            {
                fontMetrics.setStandardHorizontalWidth( readFloat() );
            }
            else if( STD_VW.equals( nextCommand ) )
            {
                fontMetrics.setStandardVerticalWidth( readFloat() );
            }
            else if( COMMENT.equals( nextCommand ) )
            {
                fontMetrics.addComment( readLine() );
            }
            else if( UNDERLINE_POSITION.equals( nextCommand ) )
            {
                fontMetrics.setUnderlinePosition( readFloat() );
            }
            else if( UNDERLINE_THICKNESS.equals( nextCommand ) )
            {
                fontMetrics.setUnderlineThickness( readFloat() );
            }
            else if( ITALIC_ANGLE.equals( nextCommand ) )
            {
                fontMetrics.setItalicAngle( readFloat() );
            }
            else if( CHAR_WIDTH.equals( nextCommand ) )
            {
                float[] widths = new float[2];
                widths[0] = readFloat();
                widths[1] = readFloat();
                fontMetrics.setCharWidth( widths );
            }
            else if( IS_FIXED_PITCH.equals( nextCommand ) )
            {
                fontMetrics.setFixedPitch( readBoolean() );
            }
            else if( START_CHAR_METRICS.equals( nextCommand ) )
            {
                int count = readInt();
                for( int i=0; i<count; i++ )
                {
                    CharMetric charMetric = parseCharMetric();
                    fontMetrics.addCharMetric( charMetric );
                }
                String end = readString();
                if( !end.equals( END_CHAR_METRICS ) )
                {
                    throw new IOException( "Error: Expected '" + END_CHAR_METRICS + "' actual '" +
                                                end + "'" );
                }
            }
            else if( START_COMPOSITES.equals( nextCommand ) )
            {
                int count = readInt();
                for( int i=0; i<count; i++ )
                {
                    Composite part = parseComposite();
                    fontMetrics.addComposite( part );
                }
                String end = readString();
                if( !end.equals( END_COMPOSITES ) )
                {
                    throw new IOException( "Error: Expected '" + END_COMPOSITES + "' actual '" +
                                                end + "'" );
                }
            }
            else if( START_KERN_DATA.equals( nextCommand ) )
            {
                parseKernData( fontMetrics );
            }
            else
            {
                throw new IOException( "Unknown AFM key '" + nextCommand + "'" );
            }
        }
        return fontMetrics;
    }

    /**
     * This will parse the kern data.
     *
     * @param fontMetrics The metrics class to put the parsed data into.
     *
     * @throws IOException If there is an error parsing the data.
     */
    private void parseKernData( FontMetric fontMetrics ) throws IOException
    {
        String nextCommand = null;
        while( !(nextCommand = readString()).equals( END_KERN_DATA ) )
        {
            if( START_TRACK_KERN.equals( nextCommand ) )
            {
                int count = readInt();
                for( int i=0; i<count; i++ )
                {
                    TrackKern kern = new TrackKern();
                    kern.setDegree( readInt() );
                    kern.setMinPointSize( readFloat() );
                    kern.setMinKern( readFloat() );
                    kern.setMaxPointSize( readFloat() );
                    kern.setMaxKern( readFloat() );
                    fontMetrics.addTrackKern( kern );
                }
                String end = readString();
                if( !end.equals( END_TRACK_KERN ) )
                {
                    throw new IOException( "Error: Expected '" + END_TRACK_KERN + "' actual '" +
                                                end + "'" );
                }
            }
            else if( START_KERN_PAIRS.equals( nextCommand ) )
            {
                int count = readInt();
                for( int i=0; i<count; i++ )
                {
                    KernPair pair = parseKernPair();
                    fontMetrics.addKernPair( pair );
                }
                String end = readString();
                if( !end.equals( END_KERN_PAIRS ) )
                {
                    throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" +
                                                end + "'" );
                }
            }
            else if( START_KERN_PAIRS0.equals( nextCommand ) )
            {
                int count = readInt();
                for( int i=0; i<count; i++ )
                {
                    KernPair pair = parseKernPair();
                    fontMetrics.addKernPair0( pair );
                }
                String end = readString();
                if( !end.equals( END_KERN_PAIRS ) )
                {
                    throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" +
                                                end + "'" );
                }
            }
            else if( START_KERN_PAIRS1.equals( nextCommand ) )
            {
                int count = readInt();
                for( int i=0; i<count; i++ )
                {
                    KernPair pair = parseKernPair();
                    fontMetrics.addKernPair1( pair );
                }
                String end = readString();
                if( !end.equals( END_KERN_PAIRS ) )
                {
                    throw new IOException( "Error: Expected '" + END_KERN_PAIRS + "' actual '" +
                                                end + "'" );
                }
            }
            else
            {
                throw new IOException( "Unknown kerning data type '" + nextCommand + "'" );
            }
        }
    }

    /**
     * This will parse a kern pair from the data stream.
     *
     * @return The kern pair that was parsed from the stream.
     *
     * @throws IOException If there is an error reading from the stream.
     */
    private KernPair parseKernPair() throws IOException
    {
        KernPair kernPair = new KernPair();
        String cmd = readString();
        if( KERN_PAIR_KP.equals( cmd ) )
        {
            String first = readString();
            String second = readString();
            float x = readFloat();
            float y = readFloat();
            kernPair.setFirstKernCharacter( first );
            kernPair.setSecondKernCharacter( second );
            kernPair.setX( x );
            kernPair.setY( y );
        }
        else if( KERN_PAIR_KPH.equals( cmd ) )
        {
            String first = hexToString( readString() );
            String second = hexToString( readString() );
            float x = readFloat();
            float y = readFloat();
            kernPair.setFirstKernCharacter( first );
            kernPair.setSecondKernCharacter( second );
            kernPair.setX( x );
            kernPair.setY( y );
        }
        else if( KERN_PAIR_KPX.equals( cmd ) )
        {
            String first = readString();
            String second = readString();
            float x = readFloat();
            kernPair.setFirstKernCharacter( first );
            kernPair.setSecondKernCharacter( second );
            kernPair.setX( x );
            kernPair.setY( 0 );
        }
        else if( KERN_PAIR_KPY.equals( cmd ) )
        {
            String first = readString();
            String second = readString();
            float y = readFloat();
            kernPair.setFirstKernCharacter( first );
            kernPair.setSecondKernCharacter( second );
            kernPair.setX( 0 );
            kernPair.setY( y );
        }
        else
        {
            throw new IOException( "Error expected kern pair command actual='" + cmd + "'" );
        }
        return kernPair;
    }

    /**
     * This will convert and angle bracket hex string to a string.
     *
     * @param hexString An angle bracket string.
     *
     * @return The bytes of the hex string.
     *
     * @throws IOException If the string is in an invalid format.
     */
    private String hexToString( String hexString ) throws IOException
    {
        if( hexString.length() < 2 )
        {
            throw new IOException( "Error: Expected hex string of length >= 2 not='" + hexString );
        }
        if( hexString.charAt( 0 ) != '<' ||
            hexString.charAt( hexString.length() -1 ) != '>' )
        {
            throw new IOException( "String should be enclosed by angle brackets '" + hexString+ "'" );
        }
        hexString = hexString.substring( 1, hexString.length() -1 );
        byte[] data = new byte[ (hexString.length() / 2) ];
        for( int i=0; i<hexString.length(); i+=2 )
        {
            String hex = "" + hexString.charAt( i ) + hexString.charAt( i+1 );
            try
            {
                data[ i / 2 ] = (byte)Integer.parseInt( hex, BITS_IN_HEX );
            }
            catch( NumberFormatException e )
            {
                throw new IOException( "Error parsing AFM file:" + e );
            }
        }
        return new String( data, "ISO-8859-1" );
    }

    /**
     * This will parse a composite part from the stream.
     *
     * @return The composite.
     *
     * @throws IOException If there is an error parsing the composite.
     */
    private Composite parseComposite() throws IOException
    {
        Composite composite = new Composite();
        String partData = readLine();
        StringTokenizer tokenizer = new StringTokenizer( partData, " ;" );


        String cc = tokenizer.nextToken();
        if( !cc.equals( CC ) )
        {
            throw new IOException( "Expected '" + CC + "' actual='" + cc + "'" );
        }
        String name = tokenizer.nextToken();
        composite.setName( name );

        int partCount;
        try
        {
            partCount = Integer.parseInt( tokenizer.nextToken() );
        }
        catch( NumberFormatException e )
        {
            throw new IOException( "Error parsing AFM document:" + e );
        }
        for( int i=0; i<partCount; i++ )
        {
            CompositePart part = new CompositePart();
            String pcc = tokenizer.nextToken();
            if( !pcc.equals( PCC ) )
            {
                throw new IOException( "Expected '" + PCC + "' actual='" + pcc + "'" );
            }
            String partName = tokenizer.nextToken();
            try
            {
                int x = Integer.parseInt( tokenizer.nextToken() );
                int y = Integer.parseInt( tokenizer.nextToken() );

                part.setName( partName );
                part.setXDisplacement( x );
                part.setYDisplacement( y );
                composite.addPart( part );
            }
            catch( NumberFormatException e )
            {
                throw new IOException( "Error parsing AFM document:" + e );
            }
        }
        return composite;
    }

    /**
     * This will parse a single CharMetric object from the stream.
     *
     * @return The next char metric in the stream.
     *
     * @throws IOException If there is an error reading from the stream.
     */
    private CharMetric parseCharMetric() throws IOException
    {
        CharMetric charMetric = new CharMetric();
        String metrics = readLine();
        StringTokenizer metricsTokenizer = new StringTokenizer( metrics );
        try
        {
            while( metricsTokenizer.hasMoreTokens() )
            {
                String nextCommand = metricsTokenizer.nextToken();
                if( nextCommand.equals( CHARMETRICS_C ) )
                {
                    String charCode = metricsTokenizer.nextToken();
                    charMetric.setCharacterCode( Integer.parseInt( charCode ) );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_CH ) )
                {
                    //Is the hex string <FF> or FF, the spec is a little
                    //unclear, wait and see if it breaks anything.
                    String charCode = metricsTokenizer.nextToken();
                    charMetric.setCharacterCode( Integer.parseInt( charCode, BITS_IN_HEX ) );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_WX ) )
                {
                    String wx = metricsTokenizer.nextToken();
                    charMetric.setWx( Float.parseFloat( wx ) );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_W0X ) )
                {
                    String w0x = metricsTokenizer.nextToken();
                    charMetric.setW0x( Float.parseFloat( w0x ) );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_W1X ) )
                {
                    String w1x = metricsTokenizer.nextToken();
                    charMetric.setW0x( Float.parseFloat( w1x ) );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_WY ) )
                {
                    String wy = metricsTokenizer.nextToken();
                    charMetric.setWy( Float.parseFloat( wy ) );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_W0Y ) )
                {
                    String w0y = metricsTokenizer.nextToken();
                    charMetric.setW0y( Float.parseFloat( w0y ) );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_W1Y ) )
                {
                    String w1y = metricsTokenizer.nextToken();
                    charMetric.setW0y( Float.parseFloat( w1y ) );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_W ) )
                {
                    String w0 = metricsTokenizer.nextToken();
                    String w1 = metricsTokenizer.nextToken();
                    float[] w = new float[2];
                    w[0] = Float.parseFloat( w0 );
                    w[1] = Float.parseFloat( w1 );
                    charMetric.setW( w );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_W0 ) )
                {
                    String w00 = metricsTokenizer.nextToken();
                    String w01 = metricsTokenizer.nextToken();
                    float[] w0 = new float[2];
                    w0[0] = Float.parseFloat( w00 );
                    w0[1] = Float.parseFloat( w01 );
                    charMetric.setW0( w0 );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_W1 ) )
                {
                    String w10 = metricsTokenizer.nextToken();
                    String w11 = metricsTokenizer.nextToken();
                    float[] w1 = new float[2];
                    w1[0] = Float.parseFloat( w10 );
                    w1[1] = Float.parseFloat( w11 );
                    charMetric.setW1( w1 );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_VV ) )
                {
                    String vv0 = metricsTokenizer.nextToken();
                    String vv1 = metricsTokenizer.nextToken();
                    float[] vv = new float[2];
                    vv[0] = Float.parseFloat( vv0 );
                    vv[1] = Float.parseFloat( vv1 );
                    charMetric.setVv( vv );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_N ) )
                {
                    String name = metricsTokenizer.nextToken();
                    charMetric.setName( name );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_B ) )
                {
                    String llx = metricsTokenizer.nextToken();
                    String lly = metricsTokenizer.nextToken();
                    String urx = metricsTokenizer.nextToken();
                    String ury = metricsTokenizer.nextToken();
                    BoundingBox box = new BoundingBox();
                    box.setLowerLeftX( Float.parseFloat( llx ) );
                    box.setLowerLeftY( Float.parseFloat( lly ) );
                    box.setUpperRightX( Float.parseFloat( urx ) );
                    box.setUpperRightY( Float.parseFloat( ury ) );
                    charMetric.setBoundingBox( box );
                    verifySemicolon( metricsTokenizer );
                }
                else if( nextCommand.equals( CHARMETRICS_L ) )
                {
                    String successor = metricsTokenizer.nextToken();
                    String ligature = metricsTokenizer.nextToken();
                    Ligature lig = new Ligature();
                    lig.setSuccessor( successor );
                    lig.setLigature( ligature );
                    charMetric.addLigature( lig );
                    verifySemicolon( metricsTokenizer );
                }
                else
                {
                    throw new IOException( "Unknown CharMetrics command '" + nextCommand + "'" );
                }
            }
        }
        catch( NumberFormatException e )
        {
            throw new IOException( "Error: Corrupt AFM document:"  + e );
        }
        return charMetric;
    }

    /**
     * This is used to verify that a semicolon is the next token in the stream.
     *
     * @param tokenizer The tokenizer to read from.
     *
     * @throws IOException If the semicolon is missing.
     */
    private void verifySemicolon( StringTokenizer tokenizer ) throws IOException
    {
        if( tokenizer.hasMoreTokens() )
        {
            String semicolon = tokenizer.nextToken();
            if( !semicolon.equals( ";" ) )
            {
                throw new IOException( "Error: Expected semicolon in stream actual='" +
                                            semicolon + "'" );
            }
        }
        else
        {
            throw new IOException( "CharMetrics is missing a semicolon after a command" );
        }
    }

    /**
     * This will read a boolean from the stream.
     *
     * @return The boolean in the stream.
     */
    private boolean readBoolean() throws IOException
    {
        String theBoolean = readString();
        return Boolean.valueOf( theBoolean ).booleanValue();
    }

    /**
     * This will read an integer from the stream.
     *
     * @return The integer in the stream.
     */
    private int readInt() throws IOException
    {
        String theInt = readString();
        try
        {
            return Integer.parseInt( theInt );
        }
        catch( NumberFormatException e )
        {
            throw new IOException( "Error parsing AFM document:" + e );
        }
    }

    /**
     * This will read a float from the stream.
     *
     * @return The float in the stream.
     */
    private float readFloat() throws IOException
    {
        String theFloat = readString();
        return Float.parseFloat( theFloat );
    }

    /**
     * This will read until the end of a line.
     *
     * @return The string that is read.
     */
    private String readLine() throws IOException
    {
        //First skip the whitespace
        StringBuffer buf = new StringBuffer();
        int nextByte = input.read();
        while( isWhitespace( nextByte ) )
        {
            nextByte = input.read();
            //do nothing just skip the whitespace.
        }
        buf.append( (char)nextByte );

        //now read the data
        nextByte = input.read();
        while (nextByte != -1 && !isEOL(nextByte))
        {
            buf.append((char) nextByte);
            nextByte = input.read();
        }
        return buf.toString();
    }

    /**
     * This will read a string from the input stream and stop at any whitespace.
     *
     * @return The string read from the stream.
     *
     * @throws IOException If an IO error occurs when reading from the stream.
     */
    private String readString() throws IOException
    {
        //First skip the whitespace
        StringBuffer buf = new StringBuffer();
        int nextByte = input.read();
        while( isWhitespace( nextByte ) )
        {
            nextByte = input.read();
            //do nothing just skip the whitespace.
        }
        buf.append( (char)nextByte );

        //now read the data
        nextByte = input.read();
        while (nextByte != -1 && !isWhitespace(nextByte))
        {
            buf.append((char) nextByte);
            nextByte = input.read();
        }
        return buf.toString();
    }

    /**
     * This will determine if the byte is a whitespace character or not.
     *
     * @param character The character to test for whitespace.
     *
     * @return true If the character is whitespace as defined by the AFM spec.
     */
    private boolean isEOL( int character )
    {
        return character == 0x0D ||
               character == 0x0A;
    }

    /**
     * This will determine if the byte is a whitespace character or not.
     *
     * @param character The character to test for whitespace.
     *
     * @return true If the character is whitespace as defined by the AFM spec.
     */
    private boolean isWhitespace( int character )
    {
        return character == ' ' ||
               character == '\t' ||
               character == 0x0D ||
               character == 0x0A;
    }
}