// (jEdit options) :folding=explicit:collapseFolds=1:
//{{{ Package, imports
package king.io;
import king.core.*;
import king.points.*;

//import java.awt.*;
//import java.awt.event.*;
import java.io.*;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.*;
//import java.util.regex.*;
//import javax.swing.*;
import driftwood.util.SoftLog;
//}}}
/**
* <code>KinfileParser</code> is a second-generation kinemage
* file loader. It contains the logic for interpretting all
* the tokens in the file, and returns one or more new kinemages
* when it's finished.
*
* <p>Copyright (C) 2003-2007 by Ian W. Davis. All rights reserved.
* <br>Begun on Wed Apr  9 08:23:25 EDT 2003
*/
public class KinfileParser //extends ... implements ...
{
//{{{ Constants
    public static final String      DEFAULT_KINEMAGE_NAME   = "Kinemage #";
    static final String             DEFAULT_GROUP_NAME      = "";
    static final String             IMPLIED_GROUP_NAME      = "(implied)";
    static final int                MAX_ERRORS_REPORTED     = 30;
//}}}

//{{{ Variable definitions
//##################################################################################################
    // Needed for basic function
    LineNumberReader        input;
    KinfileTokenizer        token;
    
    // For tracking our position in the hierarchy
    Kinemage                kinemage        = null;
    KGroup                  group           = null;
    KGroup                  subgroup        = null;
    KList                   list            = null;
    
    // Info generated by this parser
    Collection<Kinemage>    kinemages       = new ArrayList<Kinemage>();
    StringBuffer            atText          = new StringBuffer();
    Map<String,KView>       viewMap         = null; // created when kinemage is
    // Used for implementing clone= and instance=
    Map<String,KGroup>      groupsByName    = null; // created when kinemage is
    Map<String,KGroup>      subgroupsByName = null; // created when kinemage is
    Map<String,KList>       listsByName     = null; // created when kinemage is

    // Used for storing bondrots
    TreeMap<Integer,BondRot> bondRots = null;
    ArrayList<BondRot> closedBondRots = null;
    int nonIntCount = 1;
    
    // Used for high-dimensional kinemages
    int groupDimension      = 3;
    int subgroupDimension   = 3;
    
//}}}

//{{{ Constructor(s), parse
//##################################################################################################
    /**
    * Creates a new KinfileParser.
    * Call parse() to read and interpret a file.
    */
    public KinfileParser()
    {
        input = null;
        token = null;
    }
    
    /**
    * Reads and interprets the kinemage file supplied.
    *
    * @throws IOException if input stream throws any such exceptions during parsing
    */
    public void parse(LineNumberReader in) throws IOException
    {
        input = in;
        token = new KinfileTokenizer(input);
        
        keywordLoop();
        
        for(Kinemage k : kinemages) k.initAll();
    }
//}}}

//{{{ getKinemages, getText, getErrors, error, getCharsRead
//##################################################################################################
    /** Returns an unmodifiable Collection of Kinemage objects contained in this file */
    public Collection<Kinemage> getKinemages()
    { return Collections.unmodifiableCollection(kinemages); }
    
    /** Returns the concatenated result of all @text sections in this file */
    public String getText()
    { return atText.toString(); }
    
    void error(String msg)
    {
        SoftLog.err.println("[line "+(input.getLineNumber()+1)+"] "+msg);
    }
    
    /** Returns the number of characters read in thus far */
    public long getCharsRead()
    {
        if(token != null)   return token.getCharsRead();
        else                return 0;
    }
//}}}

//{{{ keywordLoop
//##################################################################################################
    void keywordLoop() throws IOException
    {
        String s;
        while(!token.isEOF())
        {
            // Kinder to user to be case insensitive for keywords, at least
            s = token.getString().toLowerCase();
            if(!token.isKeyword())
            {
                error("Dangling token '"+s+"' outside any keywords");
                token.advanceToKeyword();
            }
            // METADATA
            else if(s.equals("@text"))                  doText();
            else if(s.equals("@title"))                 doTitle();
            else if(s.equals("@copyright"))             doCopyright();
            else if(s.equals("@caption"))               doCaption();
            else if(s.equals("@mage"))                  token.advanceToKeyword();//ignored
            else if(s.equals("@prekin"))                token.advanceToKeyword();//ignored
            else if(s.equals("@pdbfile"))               doPdbFile();
            else if(s.equals("@command"))               doCommand();
            else if(s.equals("@dimensions"))            doDimensions();
            else if(s.equals("@dimension"))             doDimensions(); //deprecated
            else if(s.equals("@dimminmax"))             doDimMinMax();
            else if(s.equals("@dimscale"))              doDimScale();
            else if(s.equals("@dimoffset"))             doDimOffset();
            // VIEWS
            else if(s.endsWith("viewid"))               doViewID();
            else if(s.endsWith("zoom"))                 doZoom();
            else if(s.endsWith("span"))                 doSpan();
            else if(s.endsWith("zslab"))                doClip();
            else if(s.endsWith("zclip"))                doClip(); //deprecated
            else if(s.endsWith("ztran"))                token.advanceToKeyword(); //ignored
            else if(s.endsWith("center"))               doCenter();
            else if(s.endsWith("matrix"))               doMatrix();
            else if(s.endsWith("axischoice"))           doAxisChoice();
            // DISPLAY OPTIONS
            else if(s.equals("@whitebackground"))       doWhiteBackground();
            else if(s.equals("@whiteback"))             doWhiteBackground(); //deprecated
            else if(s.equals("@whitebkg"))              doWhiteBackground(); //deprecated
            else if(s.equals("@onewidth"))              doOnewidth();
            else if(s.equals("@thinline"))              doThinline();
            else if(s.equals("@perspective"))           doPerspective();
            else if(s.equals("@flat"))                  doFlat();
            else if(s.equals("@flatland"))              doFlat(); //deprecated
            else if(s.equals("@xytranslation"))         doFlat(); //deprecated
            else if(s.equals("@listcolordominant"))     doListColorDominant();
            else if(s.equals("@listcolordom"))          doListColorDominant(); //deprecated
            else if(s.equals("@lens"))                  doLens();
            // MASTERS, ASPECTS, AND COLORS
            else if(s.endsWith("aspect"))               doAspect();
            else if(s.equals("@master"))                doMaster();
            else if(s.equals("@pointmaster"))           doPointmaster();
            else if(s.equals("@colorset"))              doColorset();
            else if(s.equals("@hsvcolor"))              doHsvColor();
            else if(s.equals("@hsvcolour"))             doHsvColor();
            // KINEMAGES, GROUPS, AND SUBGROUPS
            else if(s.equals("@kinemage"))              doKinemage();
            else if(s.equals("@group"))                 doGroup();
            else if(s.equals("@subgroup"))              doSubgroup();
            else if(s.equals("@set"))                   doSubgroup(); //deprecated
            // LISTS
            else if(s.equals("@vectorlist"))            doList(KList.VECTOR);
            else if(s.equals("@vector"))                doList(KList.VECTOR); //deprecated
            else if(s.equals("@labellist"))             doList(KList.LABEL);
            else if(s.equals("@label"))                 doList(KList.LABEL); //deprecated
            else if(s.equals("@dotlist"))               doList(KList.DOT);
            else if(s.equals("@dot"))                   doList(KList.DOT); //deprecated
            else if(s.equals("@ringlist"))              doList(KList.RING);
            else if(s.equals("@ring"))                  doList(KList.RING); //deprecated
            else if(s.equals("@balllist"))              doList(KList.BALL);
            else if(s.equals("@ball"))                  doList(KList.BALL); //deprecated
            else if(s.equals("@spherelist"))            doList(KList.SPHERE);
            else if(s.equals("@sphere"))                doList(KList.SPHERE); //deprecated
            else if(s.equals("@trianglelist"))          doList(KList.TRIANGLE);
            else if(s.equals("@triangle"))              doList(KList.TRIANGLE); //deprecated
            else if(s.equals("@ribbonlist"))            doList(KList.RIBBON);
            else if(s.equals("@ribbon"))                doList(KList.RIBBON); //deprecated
            else if(s.equals("@marklist"))              doList(KList.MARK);
            else if(s.equals("@mark"))                  doList(KList.MARK); //deprecated
            else if(s.equals("@arrowlist"))             doList(KList.ARROW);
            else if(s.equals("@arrow"))                 doList(KList.ARROW); //deprecated
            else
            {
                // In the future, we'd like to save this as-is.
                error("Unrecognized keyword '"+s+"' will be ignored");
                token.advanceToKeyword();
            }
        }//while not EOF
        if(kinemage != null)
            kinemage.setBondRots(closeBondRots());
    }
//}}}

//{{{ doKinemage, checkKinemage
//##################################################################################################
    void doKinemage() throws IOException
    {
        if(kinemage != null)
            kinemage.setBondRots(closeBondRots());
        
        kinemage = new Kinemage(DEFAULT_KINEMAGE_NAME+(kinemages.size()+1));
        kinemages.add(kinemage);
        group           = null;
        subgroup        = null;
        list            = null;
        viewMap         = new HashMap<String,KView>();
        groupsByName    = new HashMap<String,KGroup>();
        subgroupsByName = new HashMap<String,KGroup>();
        listsByName     = new HashMap<String,KList>();

        bondRots = new TreeMap<Integer,BondRot>();
        closedBondRots = new ArrayList<BondRot>();
        
        token.advance();
        while(!token.isEOF() && !token.isKeyword())
        {
            if(token.isInteger())           kinemage.setName(DEFAULT_KINEMAGE_NAME+token.getInt());
            else if(token.isIdentifier())   kinemage.setName(token.getString()); //KiNG extension
            else error("Unrecognized token '"+token.getString()+"' will be ignored");
            token.advance();
        }
    }
    
    void checkKinemage()
    {
        if(kinemage == null)
        {
            kinemage = new Kinemage(DEFAULT_KINEMAGE_NAME+(kinemages.size()+1));
            kinemages.add(kinemage);
            group           = null;
            subgroup        = null;
            list            = null;
            viewMap         = new HashMap<String,KView>();
            groupsByName    = new HashMap<String,KGroup>();
            subgroupsByName = new HashMap<String,KGroup>();
            listsByName     = new HashMap<String,KList>();

            bondRots = new TreeMap<Integer,BondRot>();
            closedBondRots = new ArrayList<BondRot>();

            error("'"+token.getString()+"' was found before encountering @kinemage");
        }
    }
//}}}

//{{{ doGroup, checkGroup
//##################################################################################################
    void doGroup() throws IOException
    {
        checkKinemage();
        group = new KGroup(DEFAULT_GROUP_NAME);
        kinemage.add(group);
        subgroup    = null;
        list        = null;
        groupDimension = 3;
        
        token.advance();
        // Entire @group must be on a single line
        while(!token.isEOF() && !token.isKeyword() && !token.isBOL())
        {
            if(token.isIdentifier())    { group.setName(token.getString()); token.advance(); }
            else if(token.isLiteral())
            {
                String s = token.getString();
                     if(s.equals("animate"))        group.setAnimate(true);
                else if(s.equals("2animate"))       group.set2Animate(true);
                else if(s.equals("select"))         group.setSelect(true);
                else if(s.equals("off"))            group.setOn(false);
                else if(s.equals("dominant"))       group.setDominant(true);
                else if(s.equals("recessiveon")
                    || s.equals("collapsable")
                    || s.equals("collapsible"))     group.setCollapsible(true);
                else if(s.equals("nobutton"))       group.setHasButton(false);
                else if(s.equals("lens"))           group.setLens(true);
                else error("Unrecognized literal '"+s+"' will be ignored");
                token.advance();
            }
            else if(token.isProperty())
            {
                String s = token.getString();
                token.advance(); // past key
                if(s.equals("master="))
                {
                    if(token.isIdentifier())
                    {
                        kinemage.ensureMasterExists(token.getString());
                        group.addMaster(token.getString());
                    }
                    else error("master= was not followed by an identifier");
                }
                // Clone is a full or "deep" copy of the original
                else if(s.equals("clone="))
                {
                    if(token.isIdentifier())
                    {
                        KGroup template = groupsByName.get(token.getString());
                        if(template != null) group.getChildren().addAll( ((KGroup)template.clone(true)).getChildren() );
                    }
                    else error(s+" was not followed by an identifier");
                }
                // Instance is a lightweight copy that uses the same underlying point data
                else if(s.equals("instance="))
                {
                    if(token.isIdentifier())
                    {
                        KGroup template = groupsByName.get(token.getString());
                        if(template != null) group.setInstance(template);
                    }
                    else error("instance= was not followed by an identifier");
                }
                else if(s.equals("dimension="))
                {
                    if(token.isInteger()) groupDimension = token.getInt();
                    else error(s+" was not followed by an integer");
                }
                else if(s.equals("moview=")) {
                  if(token.isInteger())   group.setMoview(token.getInt());
                  else error(s+" was not followed by an integer");
                }
                else error("Unrecognized property '"+s+" "+token.getString()+"' will be ignored");
                token.advance(); // past value
            }
            else { error("Unrecognized token '"+token.getString()+"' will be ignored"); token.advance(); }
        }
        // Done last, after we have our final name
        groupsByName.put(group.getName(), group);
    }
    
    void checkGroup()
    {
        checkKinemage();
        if(group == null)
        {
            group = new KGroup(IMPLIED_GROUP_NAME);
            group.setHasButton(false);
            kinemage.add(group);
            subgroup    = null;
            list        = null;
            groupDimension = 3;
        }
    }
//}}}

//{{{ doSubgroup, checkSubgroup
//##################################################################################################
    void doSubgroup() throws IOException
    {
        checkGroup();
        subgroup = new KGroup(DEFAULT_GROUP_NAME);
        group.add(subgroup);
        list        = null;
        subgroupDimension = 3;
        
        token.advance();
        // Entire @subgroup must be on a single line
        while(!token.isEOF() && !token.isKeyword() && !token.isBOL())
        {
            if(token.isIdentifier())    { subgroup.setName(token.getString()); token.advance(); }
            else if(token.isLiteral())
            {
                String s = token.getString();
                     if(s.equals("off"))            subgroup.setOn(false);
                else if(s.equals("dominant"))       subgroup.setDominant(true);
                else if(s.equals("recessiveon")
                    || s.equals("collapsable")
                    || s.equals("collapsible"))     subgroup.setCollapsible(true);
                else if(s.equals("nobutton"))       subgroup.setHasButton(false);
                else if(s.equals("lens"))           subgroup.setLens(true);
                else error("Unrecognized literal '"+s+"' will be ignored");
                token.advance();
            }
            else if(token.isProperty())
            {
                String s = token.getString();
                token.advance(); // past key
                if(s.equals("master="))
                {
                    if(token.isIdentifier())
                    {
                        kinemage.ensureMasterExists(token.getString());
                        subgroup.addMaster(token.getString());
                    }
                    else error("master= was not followed by an identifier");
                }
                // Clone is a full or "deep" copy of the original
                else if(s.equals("clone="))
                {
                    if(token.isIdentifier())
                    {
                        KGroup template = subgroupsByName.get(token.getString());
                        if(template != null) subgroup.getChildren().addAll( ((KGroup)template.clone(true)).getChildren() );
                    }
                    else error(s+" was not followed by an identifier");
                }
                // Instance is a lightweight copy that uses the same underlying point data
                else if(s.equals("instance="))
                {
                    if(token.isIdentifier())
                    {
                        KGroup template = subgroupsByName.get(token.getString());
                        if(template != null) subgroup.setInstance(template);
                    }
                    else error("instance= was not followed by an identifier");
                }
                else if(s.equals("dimension="))
                {
                    if(token.isInteger()) subgroupDimension = token.getInt();
                    else error(s+" was not followed by an integer");
                }
                else error("Unrecognized property '"+s+" "+token.getString()+"' will be ignored");
                token.advance(); // past value
            }
            else { error("Unrecognized token '"+token.getString()+"' will be ignored"); token.advance(); }
        }
        // Done last, after we have our final name
        subgroupsByName.put(subgroup.getName(), subgroup);
    }
    
    void checkSubgroup()
    {
        checkGroup();
        if(subgroup == null)
        {
            subgroup = new KGroup(IMPLIED_GROUP_NAME);
            subgroup.setHasButton(false);
            group.add(subgroup);
            list        = null;
            subgroupDimension = 3;
        }
    }
//}}}

//{{{ doList
//##################################################################################################
    void doList(String kListType) throws IOException
    {
        checkSubgroup();
        list = new KList(kListType, DEFAULT_GROUP_NAME);
        subgroup.add(list);
        
        list.setDimension(3);
        if(subgroupDimension != 3)      list.setDimension(subgroupDimension);
        else if(groupDimension != 3)    list.setDimension(groupDimension);
        
        if(kListType == KList.MARK)     list.setStyle(MarkerPoint.SQUARE_L);
        
        token.advance();
        // Entire @list must be on a single line and may be terminated by the first point ID
        boolean listIdFound = false;
        boolean numberFound = false;
        while(!token.isEOF() && !token.isKeyword() && (!token.isIdentifier() || !listIdFound) && !token.isBOL() && !numberFound)
        {
            if(token.isIdentifier())
            {
                list.setName(token.getString());
                listIdFound = true;
                token.advance();
            }
            else if(token.isLiteral())
            {
                String s = token.getString();
                     if(s.equals("off"))        list.setOn(false);
                else if(s.equals("nobutton"))   list.setHasButton(false);
                else if(s.equals("lens"))       list.setLens(true);
                else if(s.startsWith("nohi"))   list.setNoHighlight(true);
                // for doing bondrots
                else if(s.endsWith("bondrot")) {
                    double angle = 0;
                    char firstChar = s.charAt(0);
                    int bondNum = -1;
                    if (Character.isDigit(firstChar)) {
                        bondNum = Character.getNumericValue(firstChar);
                    } else {
                        bondNum = nonIntCount;
                        nonIntCount++;
                    }
                    token.advance();
                    if(token.isNumber()) {
                        angle = token.getFloat();
                    } else {
                        error("angle for bondrot not number");
                    }
                    storeBondRot(bondNum, list.getName(), angle);
                }
                else if(s.startsWith("screen")) list.setScreen(true);  // DAK 090212
                else if(s.startsWith("rear")) list.setRear(true);
                else if(s.startsWith("fore")) list.setFore(true);
                else error("Unrecognized literal '"+s+"' will be ignored");
                token.advance();
            }
            else if(token.isProperty())
            {
                String s = token.getString();
                token.advance(); // past key
                if(s.equals("color=") || s.equals("colour="))
                {
                    //   e.g. red             e.g. @colorset {HelixCap}
                    if( (token.isLiteral() || token.isIdentifier())
                    && kinemage.getAllPaintMap().containsKey(token.getString()) )
                    {
                        list.setColor(kinemage.getPaintForName(token.getString()));
                    }
                    else error("color= was followed by unknown color '"+token.getString()+"'");
                }
                else if(s.equals("master="))
                {
                    if(token.isIdentifier())
                    {
                        kinemage.ensureMasterExists(token.getString());
                        list.addMaster(token.getString());
                        if(token.getString().equals("alpha") || token.getString().equals("beta")) // (ARK Spring2010)
                        {
                        	list.setSecStruc(token.getString()); // (ARK Spring2010)	
                        }
                    }
                    else error("master= was not followed by an identifier");
                }
                // Clone is a full or "deep" copy of the original
                else if(s.equals("clone="))
                {
                    if(token.isIdentifier())
                    {
                        KList template = listsByName.get(token.getString());
                        if(template != null) list.getChildren().addAll( ((KList)template.clone(true)).getChildren() );
                    }
                    else error("clone= was not followed by an identifier");
                }
                // Instance is a lightweight copy that uses the same underlying point data
                else if(s.equals("instance="))
                {
                    if(token.isIdentifier())
                    {
                        KList template = listsByName.get(token.getString());
                        if(template != null) list.setInstance(template);
                    }
                    else error("instance= was not followed by an identifier");
                }
                else if(s.equals("radius="))
                {
                    if(token.isNumber()) list.setRadius(token.getFloat());
                    else error("radius= was not followed by a number");
                }
                else if(s.equals("angle="))
                {
                    if(token.isNumber()) list.setAngle(token.getFloat());
                    else error("angle= was not followed by a number");
                }
                else if(s.equals("alpha=")) // opacity, from 1 (opaque) to 0 (transparent)
                {
                    if(token.isNumber())
                    {
                        double alpha = token.getFloat();
                        if(alpha < 0) alpha = 0;
                        else if(alpha > 1) alpha = 1;
                        list.setAlpha((int)(alpha*255 + 0.5));
                    }
                    else error("alpha= was not followed by a number");
                }
                else if(s.equals("width="))
                {
                    if(token.isInteger()) list.setWidth(token.getInt());
                    else error("width= was not followed by an integer");
                }
                else if(s.equals("size="))
                {
                    if(token.isInteger()) {} // useless property from legacy kins
                    else error("size= was not followed by an integer");
                }
                else if(s.equals("dimension="))
                {
                    if(token.isInteger()) list.setDimension(token.getInt());
                    else error(s+" was not followed by an integer");
                }
                else error("Unrecognized property '"+s+" "+token.getString()+"' will be ignored");
                token.advance(); // past value
            }
            else if (token.isNumber()) {
              error("Lone number list token without property detected; switching to points");
              numberFound = true;
            }
            else { error("Unrecognized list token '"+token.getString()+"' will be ignored"); token.advance(); }
        }
        // Done last, after we have our final name
        listsByName.put(list.getName(), list);

        // Read in points until we hit another keyword or EOF
        KPoint prevPoint = null;
        while(!token.isEOF() && !token.isKeyword())
        {
            prevPoint = doPoint(kListType, prevPoint, list.getDimension());
        }

        // only stores list as bondRot if bondRot mode is on.
        if (rotModeIsOn()) {
            storeRotList(list);
        }
    }
//}}}

//{{{ doPoint
//##################################################################################################
    /** Only called by doList() */
    KPoint doPoint(String kListType, KPoint prevPoint, int dimension) throws IOException
    {
        KPoint point;
        String defaultPointID;
        if(prevPoint == null)   defaultPointID = list.getName();
        else                    defaultPointID = prevPoint.getName();

             if(kListType.equals(KList.VECTOR))     point = new VectorPoint(defaultPointID, (VectorPoint)prevPoint);
        else if(kListType.equals(KList.DOT))        point = new DotPoint(defaultPointID);
        else if(kListType.equals(KList.MARK))       point = new MarkerPoint(defaultPointID);
        else if(kListType.equals(KList.LABEL))      point = new LabelPoint(defaultPointID);
        else if(kListType.equals(KList.TRIANGLE))   point = new TrianglePoint(defaultPointID, (TrianglePoint)prevPoint);
        else if(kListType.equals(KList.RIBBON))     point = new TrianglePoint(defaultPointID, (TrianglePoint)prevPoint);
        else if(kListType.equals(KList.RING))       point = new RingPoint(defaultPointID);
        else if(kListType.equals(KList.BALL))       point = new BallPoint(defaultPointID);
        else if(kListType.equals(KList.SPHERE))     point = new SpherePoint(defaultPointID);
        else if(kListType.equals(KList.ARROW))      point = new ArrowPoint(defaultPointID, (VectorPoint)prevPoint);
        else throw new IllegalArgumentException("Unrecognized list type '"+kListType+"'");

        float[] allCoords = null;
        if(dimension > 3)
        {
            allCoords = new float[dimension];
            point.setAllCoords(allCoords);
        }

        boolean pointIdFound    = false;
        int     coordsFound     = 0;
        
        while(!token.isEOF() && !token.isKeyword() && (!token.isIdentifier() || !pointIdFound) && coordsFound < dimension)
        {
            if(token.isIdentifier())
            {
                pointIdFound = true;
                if(!token.getString().equals("\"")) point.setName(token.getString());
            }
            else if(token.isNumber())
            {
                float f = token.getFloat();
                     if(coordsFound == 0)   point.setX(f);
                else if(coordsFound == 1)   point.setY(f);
                else if(coordsFound == 2)   point.setZ(f);
                if(allCoords != null) allCoords[coordsFound] = f;
                coordsFound++;
            }
            else if(token.isAspect())       point.setAspects(token.getString());
            else if(token.isSingleQuote())  point.setPmMask(kinemage.toPmBitmask(token.getString(), true, true));
            else if(token.isLiteral())
            {
                String s = token.getString();
                if(s.equals("P") || s.equals("p") || s.equals("M") || s.equals("m"))
                {
                    if(kListType.equals(KList.TRIANGLE) || kListType.equals(KList.RIBBON)) {} // see "X" flag, below
                    else point.setPrev(null);
                }
                else if(s.equals("X") || s.equals("x"))         point.setPrev(null); // P doesn't work for triangle, ribbon
                else if(s.equals("L") || s.equals("l") || s.equals("D") || s.equals("d")) {}
                else if(s.equals("T") || s.equals("t")) {} // to avoid error messages for Mage ribbon/triangle lists
                else if(s.equals("U") || s.equals("u"))         point.setUnpickable(true);
                else if(s.equals("ghost"))                      point.setGhost(true);
                else if(s.startsWith("width"))
                {
                    try { point.setWidth(Integer.parseInt(s.substring(5))); }
                    catch(NumberFormatException ex) {}
                }
                else if(kinemage.getAllPaintMap().containsKey(s)) point.setColor(kinemage.getPaintForName(s));
                else error("Unrecognized literal '"+s+"' will be ignored");
            }
            else if(token.isProperty())
            {
                String s = token.getString();
                token.advance(); // past key
                if(s.equals("r="))
                {
                    if(token.isNumber()) point.setRadius(token.getFloat());
                    else error("r= was not followed by a number");
                }
                else error("Unrecognized property '"+s+" "+token.getString()+"' will be ignored");
            }
            else if(token.isComment())
            {
                point.setComment(token.getString());
            }
            else error("Unrecognized token '"+token.getString()+"' will be ignored");
            token.advance();
        }
        
        // Avoid creating bogus points from trailing junk, like empty comments.
        if(pointIdFound || coordsFound > 0)
        {
            list.add(point);
            return point;
        }
        else
        {
            error("Junk point will be ignored (no ID, no coordinates)");
            return null; // point not added to list, so it's GC'd
        }
    }
//}}}

//{{{ do{Text, Caption, Title, Copyright, PdbFile, Command}
//##################################################################################################
    void doText() throws IOException
    {
        atText.append(token.advanceToKeyword().trim()).append("\n\n");
    }
    
    void doCaption() throws IOException
    {
        checkKinemage();
        atText.append("CAPTION for "+kinemage.getName()+":\n    ");
        atText.append(token.advanceToKeyword().trim()).append("\n\n");
    }
    
    void doTitle() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isIdentifier()) { kinemage.setName(token.getString()); token.advance(); }
        else error("@title was not followed by an identifier; found '"+token.getString()+"' instead");
    }
    
    void doCopyright() throws IOException
    {
        token.advance();
        if(token.isIdentifier())
        {
            if(kinemage != null) atText.append("'"+kinemage.getName()+"' is ");
            atText.append("COPYRIGHT (C) ").append(token.getString()).append("\n\n");
            token.advance();
        }
        else error("@copyright was not followed by an identifier; found '"+token.getString()+"' instead");
    }
    
    void doPdbFile() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isIdentifier()) { kinemage.atPdbfile = token.getString(); token.advance(); }
        else error("@pdbfile was not followed by an identifier; found '"+token.getString()+"' instead");
    }
    
    void doCommand() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isIdentifier()) { kinemage.atCommand = token.getString(); token.advance(); }
        else error("@command was not followed by an identifier; found '"+token.getString()+"' instead");
    }
//}}}

//{{{ getView, do{ViewID, Zoom, Span, Clip}
//##################################################################################################
    KView getView()
    {
        checkKinemage();
        String s = token.getString();
        StringBuffer keybuf = new StringBuffer();
        int i;
        for(char c = s.charAt(i = 1); Character.isDigit(c); c = s.charAt(++i))
        { keybuf.append(c); }
        
        String key = keybuf.toString();
        if(key.equals("")) key = "1";
        
        KView view = viewMap.get(key);
        if(view == null)
        {
            view = new KView(kinemage);
            kinemage.addView(view);
            viewMap.put(key, view);
        }
        return view;
    }

    void doViewID() throws IOException
    {
        KView view = getView();
        token.advance();
        if(token.isIdentifier()) { view.setName(token.getString()); token.advance(); }
        else error("@viewid was not followed by an identifier; found '"+token.getString()+"' instead");
    }

    void doZoom() throws IOException
    {
        KView view = getView();
        token.advance();
        if(token.isNumber()) { view.setZoom(token.getFloat()); token.advance(); }
        else error("@zoom was not followed by a number; found '"+token.getString()+"' instead");
    }

    void doSpan() throws IOException
    {
        KView view = getView();
        token.advance();
        if(token.isNumber()) { view.setSpan(token.getFloat()); token.advance(); }
        else error("@span was not followed by a number; found '"+token.getString()+"' instead");
    }
    
    void doClip() throws IOException
    {
        KView view = getView();
        token.advance();
        if(token.isNumber()) { view.setClip(token.getFloat() / 200f); token.advance(); }
        else error("@zslab was not followed by a number; found '"+token.getString()+"' instead");
    }
//}}}

//{{{ do{Center, Matrix, AxisChoice}
//##################################################################################################
    void doCenter() throws IOException
    {
        KView view = getView();
        try
        {
            float cx, cy, cz;
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            cx = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            cy = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            cz = token.getFloat();
            token.advance();
            view.setCenter(cx, cy, cz);
        }
        catch(IllegalArgumentException ex)
        { error("@center was not followed by 3 numbers; found '"+token.getString()+"' instead"); }
    }

    void doAxisChoice() throws IOException
    {
        KView view = getView();
        try
        {
            int ix, iy, iz;
            token.advance(); if(!token.isInteger()) throw new IllegalArgumentException();
            ix = token.getInt();
            token.advance(); if(!token.isInteger()) throw new IllegalArgumentException();
            iy = token.getInt();
            token.advance(); if(!token.isInteger()) throw new IllegalArgumentException();
            iz = token.getInt();
            token.advance();
            view.setViewingAxes(new int[] {
                Math.max(ix-1, 0),
                Math.max(iy-1, 0),
                Math.max(iz-1, 0)
            });
        }
        catch(IllegalArgumentException ex)
        { error("@axischoice was not followed by 3 integers; found '"+token.getString()+"' instead"); }
    }

    void doMatrix() throws IOException
    {
        KView view = getView();
        try
        {
            // This KiNG-style (premultiplied) transformation matrix
            // is the transpose of the Mage-style (postmultiplied) matrix.
            float[][] km = new float[3][3];
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[0][0] = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[1][0] = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[2][0] = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[0][1] = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[1][1] = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[2][1] = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[0][2] = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[1][2] = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            km[2][2] = token.getFloat();
            token.advance();
            view.setMatrix(km);
        }
        catch(IllegalArgumentException ex)
        { error("@matrix was not followed by 9 numbers; found '"+token.getString()+"' instead"); }
    }
//}}}

//{{{ do{WhiteBackground, Onewidth, Thinline, Perspective, Flat, ListColorDominant, Lens}
//##################################################################################################
    void doWhiteBackground() throws IOException
    {
        checkKinemage();
        kinemage.atWhitebackground = true;
        token.advance();
    }

    void doOnewidth() throws IOException
    {
        checkKinemage();
        kinemage.atOnewidth = true;
        token.advance();
    }

    void doThinline() throws IOException
    {
        checkKinemage();
        kinemage.atThinline = true;
        token.advance();
    }

    void doPerspective() throws IOException
    {
        checkKinemage();
        kinemage.atPerspective = true;
        token.advance();
    }

    void doFlat() throws IOException
    {
        checkKinemage();
        kinemage.atFlat = true;
        token.advance();
    }

    void doListColorDominant() throws IOException
    {
        checkKinemage();
        kinemage.atListcolordominant = true;
        token.advance();
    }
    
    void doLens() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isNumber()) { kinemage.atLens = token.getDouble(); token.advance(); }
        else error("@lens was not followed by a number; found '"+token.getString()+"' instead");
    }
//}}}

//{{{ do{Colorset, HsvColor}
//##################################################################################################
    void doColorset() throws IOException
    {
        checkKinemage();
        Map<String,KPaint> pmap = kinemage.getAllPaintMap();
        try
        {
            String cset, color;
            token.advance(); if(!token.isIdentifier()) throw new IllegalArgumentException();
            cset = token.getString();
            token.advance(); if(!token.isLiteral()) throw new IllegalArgumentException();
            color = token.getString();
            if(!pmap.containsKey(color)) throw new IllegalArgumentException();
            token.advance();
            KPaint paint = KPaint.createAlias(cset, kinemage.getPaintForName(color));
            kinemage.addPaint(paint);
        }
        catch(IllegalArgumentException ex)
        { error("@colorset was not followed by an identifier and a recognized color; found '"+token.getString()+"' instead"); }
    }
    
    void doHsvColor() throws IOException
    {
        checkKinemage();
        try
        {
            String colorName;
            float bHue, bSat, bVal, wHue, wSat, wVal;
            token.advance(); if(!token.isIdentifier()) throw new IllegalArgumentException();
            colorName = token.getString();
            
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            bHue = wHue = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            bSat = wSat = token.getFloat();
            token.advance(); if(!token.isNumber()) throw new IllegalArgumentException();
            bVal = wVal = token.getFloat();

            token.advance(); if(token.isNumber())
            {
                wHue = token.getFloat();
                token.advance(); if(token.isNumber())
                {
                    wSat = token.getFloat();
                    token.advance(); if(token.isNumber())
                    {
                        wVal = token.getFloat();
                        token.advance();
                    }
                }
            }

            KPaint paint = KPaint.createLightweightHSV(colorName, bHue, bSat, bVal, wHue, wSat, wVal);
            kinemage.addPaint(paint);
        }
        catch(IllegalArgumentException ex)
        { error("@hsvcolor was not followed by an identifier and 3 - 6 numbers; found '"+token.getString()+"' instead"); }
    }
//}}}

//{{{ do{Master, Pointmaster}
//##################################################################################################
    // This just creates the master button ahead of time,
    // so it will appear in the specified order later on.
    void doMaster() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isIdentifier())
        {
            MasterGroup master = kinemage.getMasterByName(token.getString());
            token.advance();
            while(!token.isEOF() && !token.isKeyword())
            {
                if(token.isLiteral())
                {
                    if(token.getString().equals("indent"))      master.setIndent(true);
                    else if(token.getString().equals("on"))     master.setOnForced(true); // will turn on ALL target groups during sync
                    else if(token.getString().equals("off"))    master.setOnForced(false); // will turn off ALL target groups during sync
                    else error("Unrecognized literal '"+token.getString()+"' will be ignored");
                }
                else error("Unrecognized token '"+token.getString()+"' will be ignored");
                token.advance();
            }
        }
        else error("@master was not followed by an identifier; found '"+token.getString()+"' instead");
    }
    
    // This both creates the master and sets its pointmaster bitflags
    // Syntax:
    //  @pointmaster 'x' {Button name} [indent]
    // Possible to do the following (but why?):
    //  @pointmaster 'abc' {Multi-master} [indent]
    void doPointmaster() throws IOException
    {
        checkKinemage();
        try
        {
            String charFlags, masterName;
            token.advance(); if(!token.isSingleQuote()) throw new IllegalArgumentException();
            charFlags = token.getString();
            token.advance(); if(!token.isIdentifier()) throw new IllegalArgumentException();
            masterName = token.getString();
            token.advance();
            
            MasterGroup master = kinemage.getMasterByName(masterName);
            master.setPmMask(charFlags);
            
            while(!token.isEOF() && !token.isKeyword())
            {
                if(token.isLiteral())
                {
                    if(token.getString().equals("indent"))      master.setIndent(true);
                    else if(token.getString().equals("on"))     master.setOnForced(true); // will turn on ALL target pts during sync (no effect)
                    else if(token.getString().equals("off"))    master.setOnForced(false); // will turn off ALL target pts during sync
                    else error("Unrecognized literal '"+token.getString()+"' will be ignored");
                }
                else error("Unrecognized token '"+token.getString()+"' will be ignored");
                token.advance();
            }
        }
        catch(IllegalArgumentException ex)
        { error("@pointmaster was not followed by a single-quoted string and an identifier; found '"+token.getString()+"' instead"); }
    }
//}}}

//{{{ doAspect
//##################################################################################################
    void doAspect() throws IOException
    {
        checkKinemage();

        String s = token.getString();
        StringBuffer keybuf = new StringBuffer();
        int i;
        for(char c = s.charAt(i = 1); Character.isDigit(c); c = s.charAt(++i))
        { keybuf.append(c); }
        
        String key = keybuf.toString();
        if(key.equals("")) key = "1";
        
        try
        {
            int index = Integer.parseInt(key);
            token.advance(); if(!token.isIdentifier()) throw new IllegalArgumentException();
            kinemage.createAspect(token.getString(), new Integer(index));
            token.advance();
        }
        catch(NumberFormatException ex)
        { error("@aspect was not recognized; '"+key+"' is not an integer"); }
        catch(IllegalArgumentException ex)
        { error("@aspect was not followed by an identifier; found '"+token.getString()+"' instead"); }
    }
//}}}

//{{{ doDimensions, doDimMinMax, doDimScale, doDimOffset
//##################################################################################################
    void doDimensions() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isIdentifier()) while(token.isIdentifier())
        {
            kinemage.dimensionNames.add(token.getString());
            token.advance();
        }
        else error("@dimensions was not followed by 1+ identifiers; found '"+token.getString()+"' instead");
    }

    void doDimMinMax() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isNumber()) while(token.isNumber())
        {
            kinemage.dimensionMinMax.add(new Double(token.getDouble()));
            token.advance();
        }
        else error("@dimminmax was not followed by 1+ numbers; found '"+token.getString()+"' instead");
    }

    void doDimScale() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isNumber()) while(token.isNumber())
        {
            kinemage.dimensionScale.add(new Double(token.getDouble()));
            token.advance();
        }
        else error("@dimscale was not followed by 1+ numbers; found '"+token.getString()+"' instead");
    }

    void doDimOffset() throws IOException
    {
        checkKinemage();
        token.advance();
        if(token.isNumber()) while(token.isNumber())
        {
            kinemage.dimensionOffset.add(new Double(token.getDouble()));
            token.advance();
        }
        else error("@dimoffset was not followed by 1+ numbers; found '"+token.getString()+"' instead");
    }
//}}}

//{{{ storeBondRot, storeRotList
//##################################################################################################
    /** 
    * Private functions used to store bondrots.  Numbered BondRots work by creating a new bond rot
    * everytime a #bondrot is encountered in the kinemage.  A bondrot is closed whenever a new #bondrot
    * is encountered with a # less than or equal to the bondrot.  
    */
    private void storeBondRot(int bondNum, String nm, double angle)
    {
        Integer bondInt = new Integer(bondNum);
        Map<Integer,BondRot> higherRots = new TreeMap<Integer,BondRot>();
        if(bondNum != -1)
            higherRots = bondRots.tailMap(bondInt);

        if(!higherRots.isEmpty())
        {
            for(BondRot toBeClosed : higherRots.values())
            {
                toBeClosed.setOpen(false);
                closedBondRots.add(toBeClosed);
                //System.out.println("Bond rots less than or equal to " + bondInt + " closed");
            }
        }
        
         // to clear map of all bondrots with higher numbers
         if(bondNum != -1)
            bondRots = new TreeMap<Integer,BondRot>(bondRots.headMap(bondInt));

        BondRot newRot = new BondRot(bondNum, nm, angle);
        bondRots.put(bondInt, newRot);
    }
    
    // for putting a klist into all currently open bondrots.
    private void storeRotList(KList list)
    {
        for(BondRot rot : bondRots.values())
            if(rot.isOpen())
                rot.add(list);
    }

//}}}

//{{{ closeBondRots, rotModeIsOn
//##################################################################################################
    private ArrayList<BondRot> closeBondRots()
    {
        // need to close all open bondrots
        if(bondRots != null)
        {
            for(BondRot toBeClosed : bondRots.values())
            {
                toBeClosed.setOpen(false);
                closedBondRots.add(toBeClosed);
            }
            bondRots = null;
        }
        return closedBondRots;
    }

    /**
    * Returns whether bondRots have been encountered in the kinemage.
    */
    public boolean rotModeIsOn()
    {
        return !bondRots.isEmpty();
    }
//}}}

//{{{ empty_code_segment
//##################################################################################################
//}}}

//{{{ Main, main
//##################################################################################################
    /**
    * Main() function for running as an application.
    * Takes a kinemage on stdin and writes tokens to stdout
    */
    public void Main()
    {
        System.out.println("Read "+kinemages.size()+" kinemages:");
        for(Kinemage kin : kinemages)
        {
            for(AGE age : KIterator.allNonPoints(kin))
            {
                if(age instanceof Kinemage)
                    System.out.println("  Kinemage: "+age.getName()+" ("+age.getChildren().size()+" groups)");
                else if(age instanceof KGroup && age.getDepth() == 1)
                    System.out.println("    Group: "+age.getName()+" ("+age.getChildren().size()+" subgroups)");
                else if(age instanceof KGroup && age.getDepth() == 2)
                    System.out.println("      Subgroup: "+age.getName()+" ("+age.getChildren().size()+" lists)");
                else if(age instanceof KList)
                    System.out.println("        List: "+age.getName()+" ("+age.getChildren().size()+" points)");
            }
            System.out.println();
        }
        System.out.println();
    }

    public static void main(String[] args)
    {
        try
        {
            Reader input = new InputStreamReader(System.in);
            if(args.length > 0)
            {
                System.err.println("*** Takes a kinemage on stdin or as first arg and writes structure to stdout.");
                input = new FileReader(args[0]);
            }

            long time = System.currentTimeMillis();
            KinfileParser parser = new KinfileParser();
            parser.parse(new LineNumberReader(input));
            time = System.currentTimeMillis() - time;
            System.out.println("END OF FILE ("+time+" ms)");
            System.gc();
            System.out.println("Using "+(Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory())+" bytes");
            parser.Main();
        }
        catch(Throwable t)
        { t.printStackTrace(SoftLog.err); }
    }
//}}}
}//class

