/*
 * ScanCmd.java
 *
 * Copyright (c) 1997 Sun Microsystems, Inc.
 *
 * See the file "license.terms" for information on usage and
 * redistribution of this file, and for a DISCLAIMER OF ALL
 * WARRANTIES.
 * 
 * RCS: @(#) $Id: ScanCmd.java,v 1.1.1.1 1998/10/14 21:09:18 cvsadmin Exp $
 *
 */

package tcl.lang;
import java.util.*;

/**
 * This class implements the built-in "scan" command in Tcl.
 * 
 */

class ScanCmd implements Command {
    /**
     * This procedure is invoked to process the "scan" Tcl command.
     * See the user documentation for details on what it does.
     *
     * Each iteration of the cmdProc compares the scanArr's current index to 
     * the frmtArr's index.  If the chars are equal then the indicies are
     * incremented.  If a '%' is found in the frmtArr, the formatSpecifier 
     * is parced from the frmtArr, the corresponding value is extracted from 
     * the scanArr, and that value is set in the Tcl Interp.
     * 
     * If the chars are not equal, or the conversion fails, the boolean 
     * scanArrDone is set to true, indicating the scanArr is not to be 
     * parced and no new values are to be set.  However the frmtArr is still 
     * parced because of the priority of error messages.  In the C version 
     * of Tcl, bad format specifiers throw errors before incorrect argument 
     * input or other scan errors.  Thus we need to parce the entire frmtArr 
     * to verify correct formating.  This is dumb and inefficient but it is 
     * consistent w/ the current C-version of Tcl.
     */

    public void cmdProc(Interp interp, TclObject argv[])
            throws TclException {
	
        if (argv.length < 3) {
	    throw new TclNumArgsException(interp, 1, argv, 
	            "string format ?varName varName ...?");
	};

	StrtoulResult strul;      /* Return value for parcing the scanArr when
				   * extracting integers/longs */
	StrtodResult  strd;;      /* Return value for parcing the scanArr when
				   * extracting doubles */
	char[]  scanArr;          /* Array containing parce info */
	char[]  frmtArr;          /* Array containing info on how to 
				   * parse the scanArr */
	int     scanIndex;        /* Index into the scan array */
	int     frmtIndex;        /* Index into the frmt array */
	int     tempIndex;        /* Temporary index holder */
	int     argIndex;         /* Index into the current arg */
	int     width;            /* Stores the user specified result width */
	int     base;             /* Base of the integer being converted */
	int     numUnMatched;	  /* Number of fields actually set. */
	int     numMatched;	  /* Number of fields actually matched. */
	int     suppress;         /* Current field is assignment-suppressed. */
	int     negateScan;       /* Mult by result, set to -1 if true */
	int     i;                /* Generic variable */
	char    ch;               /* Generic variable */
	boolean cont;             /* Used in loops to indicate when to stop */
	boolean scanOK;           /* Set to false if strtoul/strtod fails */
	boolean scanArrDone;      /* Set to false if strtoul/strtod fails */
	boolean widthFlag;        /* True is width is specified */
	boolean discardFlag;      /* If a "%*" is in the formatString dont 
				   * write output to arg */

	scanArr     = argv[1].toString().toCharArray();
	frmtArr     = argv[2].toString().toCharArray();
	width       = base = numMatched = numUnMatched = 0;
	scanIndex   = frmtIndex = 0;
	scanOK      = true;
	scanArrDone = false;
	argIndex    = 3;

	/*
	 * Skip all (if any) of the white space before getting to a char
	 */

	frmtIndex = skipWhiteSpace(frmtArr, frmtIndex);	 

	/*
	 * Search through the frmtArr.  If the next char is a '%' parse the
	 * next chars and determine the type (if any) of the format specifier.
	 * If the scanArr has been fully searched, do nothing but incerment
	 * "numUnMatched".  The reason to continue the frmtArr search is for 
	 * consistency in output.  Previously scan format errors were reported
	 * before arg input mismatch, so this maintains the same level of error
	 * checking.
	 */

	while (frmtIndex < frmtArr.length){
	    discardFlag = widthFlag = false;
	    negateScan  = 1;
	    cont        = true;

	    /*
	     * Parce the format array and read in the correct value from the 
	     * scan array.  When the correct value is retrieved, set the 
	     * variable (from argv) in the interp.
	     */

	    if (frmtArr[frmtIndex] == '%') {

		frmtIndex++;
		checkOverFlow(interp, frmtArr, frmtIndex);
		
		/*
		 * Two '%'s in a row, do nothing...
		 */

		if (frmtArr[frmtIndex] == '%') {
		    frmtIndex++;
		    scanIndex++;
		    continue;
		}

		/*
		 * Check for a discard field flag
		 */

		if (frmtArr[frmtIndex] == '*') {
		    discardFlag = true;
		    frmtIndex++;
		    checkOverFlow(interp, frmtArr, frmtIndex);
		}

		/*
		 * Check for a width field and accept the 'h', 'l', 'L'
		 * characters, but do nothing with them.
		 *
		 * Note: The order of the width specifier and the other
		 * chars is unordered, so we need to iterate until all
		 * of the specifiers are identified.
		 */

		while (cont) {
		    cont = false;

		    switch(frmtArr[frmtIndex]) {
		        case 'h': 
		        case 'l': 
		        case 'L': {
			    /*
			     * Just ignore these values
			     */

		            frmtIndex++;
		            cont = true;
			    break;
		        }
		        default: {
		            if (Character.isDigit(frmtArr[frmtIndex])) {
			        strul = Util.strtoul(new String(frmtArr), 
			                frmtIndex, base);
			        frmtIndex = strul.index;
			        width = (int)strul.value;
			        widthFlag = true;
			        cont = true;
		    	    }
			}
		    }
		    checkOverFlow(interp, frmtArr, frmtIndex);
		}

		/*
		 * On all conversion specifiers except 'c', move the
		 * scanIndex to the next non-whitespace.
		 */

		ch = frmtArr[frmtIndex];
		if ((ch != 'c') && (ch != '[') && !scanArrDone) {
		    scanIndex = skipWhiteSpace(scanArr, scanIndex);
		}
		if (scanIndex >= scanArr.length) {
		    scanArrDone = true;
		}

		if ((scanIndex < scanArr.length) && (ch != 'c') 
                        && (ch != '[')) {
		    /*
		     * Since strtoul dosent take signed numbers, make the
		     * value positive and store the sign.
		     */

		    if (scanArr[scanIndex] == '-') {
		        negateScan = -1;
			scanIndex++;
			width--;
		    } else if (scanArr[scanIndex] == '+') {
		        scanIndex++;
			width--;
		    }

		    /*
		     * The width+scanIndex might be greater than
		     * the scanArr so we need to re-adjust when this
		     * happens.
		     */

		    if (widthFlag && (width+scanIndex > scanArr.length)) {
		        width = scanArr.length - scanIndex;
		    }
		}

		if (scanIndex >= scanArr.length) {
		    scanArrDone = true;
		}
		  
		/* 
		 * Foreach iteration we want strul and strd to be
		 * null since we error check on this case.
		 */

		strul = null;
		strd  = null;

		switch (ch) {
		    case 'd': 
		    case 'o': 
		    case 'x': {

			if (!scanArrDone) {
			  
			    if (ch == 'd') {
			        base = 10;
			    } else if (ch == 'o') {
 			        base = 8;
			    } else {
			        base = 16;
			    }
			
			    /*
			     * If the widthFlag is set then convert only 
			     * "width" characters to an ascii representation, 
			     * else read in until the end of the integer.  The 
			     * scanIndex is moved to the point where we stop
			     * reading in.
			     */

			    if (widthFlag) {
			        strul = Util.strtoul(new String(scanArr,
                                        0, width+scanIndex),
                                        scanIndex, base);
			    } else {
			        strul = Util.strtoul(new String(scanArr),
                                        scanIndex, base);
			    }
			    if (strul.errno != 0) {
			        scanOK = false;
				break;
			    }
			    scanIndex = strul.index;

			    if (!discardFlag) {			    
			        i = (int)strul.value * negateScan;
				testAndSetVar(interp, argv, argIndex++, 
			                TclInteger.newInstance(i));
			    }
			}
			break;
		    }
		    case 'c': {
			if (widthFlag) {
			    errorCharFieldWidth(interp);
			}
			if (!discardFlag && !scanArrDone) {
			    testAndSetVar(interp, argv, argIndex++, 
			            TclInteger.newInstance(
                                    scanArr[scanIndex++]));
			}
		        break;
		    }
		    case 's': {
			if (!scanArrDone) {
			    /*
			     * If the wisthFlag is set then read only "width"
			     * characters into the string, else read in until 
			     * the first whitespace or endArr is found.  The 
			     * scanIndex is moved to the point where we stop 
			     * reading in.
			     */
			    
			    tempIndex = scanIndex;			    
			    if (!widthFlag) {
			        width = scanArr.length;
			    }
			    for(i=0; (scanIndex < scanArr.length)
                                    && (i < width); i++) {
				ch = scanArr[scanIndex];
				if ((ch == ' ') || (ch == '\n') || 
                                        (ch == '\r') || (ch == '\t') || 
                                        (ch == '\f')) {
				    break;
				}
				scanIndex++;
			    }

			    if (!discardFlag) {
			        String str = new String(scanArr,tempIndex,
			                scanIndex-tempIndex);
				testAndSetVar(interp, argv, argIndex++, 
			                TclString.newInstance(str));
			    }
			}
		        break;
		    }
		    case 'e': 
		    case 'f': 
		    case 'g': {
		        if (!scanArrDone) {
			    /*
			     * If the wisthFlag is set then read only "width"
			     * characters into the string, else read in until 
			     * the first whitespace or endArr is found.  The 
			     * scanIndex is moved to the point where we stop 
			     * reading in.
			     */

			    if (widthFlag) {
			        strd = Util.strtod(new String(scanArr,
                                          0, width+scanIndex), scanIndex);
			    } else {
			        strd = Util.strtod(new String(scanArr), 
				        scanIndex);
			    }
			    if (strd.errno != 0) {
			        scanOK = false;
				break;
			    }
			    scanIndex = strd.index;

			    if (!discardFlag) {
			        double d = strd.value * negateScan;
				testAndSetVar(interp, argv, argIndex++, 
			                  TclDouble.newInstance(d));
			    }
			}
		        break;
		    }
		    case '[': {
		        boolean charMatchFound = false;
			boolean charNotMatch = false;
			char[] tempArr;
			int startIndex;
			int endIndex;
			String unmatched = "unmatched [ in format string";

			if ((++frmtIndex) >= frmtArr.length) {
			    throw new TclException(interp, unmatched);
			}

			if (frmtArr[frmtIndex] == '^') {
			    charNotMatch = true;
			    frmtIndex += 2;
			} else {
			    frmtIndex++;
			}		  
			tempIndex = frmtIndex-1;

			if (frmtIndex >= frmtArr.length) {
			    throw new TclException(interp, unmatched);
			}

			/* 
			 * Extract the list of chars for matching.
			 */

			while (frmtArr[frmtIndex] != ']') {
			    if ((++frmtIndex) >= frmtArr.length) {
			        throw new TclException(interp, unmatched);
			    }
			}
			tempArr = new String(frmtArr, tempIndex, 
				frmtIndex - tempIndex).toCharArray();

			startIndex = scanIndex;
			if (charNotMatch) {
			    /*
			     * Format specifier contained a '^' so interate
			     * until one of the chars in tempArr is found.
			     */

			    while (scanOK && !charMatchFound) {
			        if (scanIndex >= scanArr.length) {
				    scanOK = false;
				    break;
				}
				for(i=0; i < tempArr.length; i++) {
				    if (tempArr[i] == scanArr[scanIndex]) {
				        charMatchFound = true;
					break;
				    }
				}
				if (widthFlag && 
				        ((scanIndex - startIndex) >= width)) {
				    break;
				}
				if (!charMatchFound) {
				    scanIndex++;
				}
			    }
			} else {	
			    /*
			     * Iterate until the char in the scanArr is not 
			     * in the tempArr.
			     */

			     charMatchFound = true;
			     while (scanOK && charMatchFound) {
			        if (scanIndex >= scanArr.length) {
				    scanOK = false;
				    break;
				}
				charMatchFound = false;
				for(i=0; i<tempArr.length; i++) {
				    if (tempArr[i] == scanArr[scanIndex]) {
				      charMatchFound = true;
				      break;
				    }
				}
				if (widthFlag && 
				        (scanIndex-startIndex) >= width) {
				    break;
				}
				if (charMatchFound) {
				    scanIndex++;
				}

			    }
			}
		
			/*
			 * Indicates nothing was found.
			 */

			endIndex = scanIndex-startIndex;
			if (endIndex <= 0) {
			    scanOK = false;
			    break;
			}
			    
			if (!discardFlag) {
			    String str = new String(scanArr,startIndex,
			            endIndex);
			    testAndSetVar(interp, argv, argIndex++, 
   			            TclString.newInstance(str));
			}
			break;
		    }
		    default: {
		        errorBadField(interp, ch);
		    }
		}

		/*
		 * As long as the scan was successful (scanOK), the format
		 * specifier did not contain a '*' (discardFlag), and
		 * we are not at the end of the scanArr (scanArrDone);
		 * increment the num of vars set in the interp.  Otherwise
		 * increment the number of valid format specifiers.
		 */

		if (scanOK && !discardFlag && !scanArrDone) {
		    numMatched++;
		} else if ((scanArrDone || !scanOK) && !discardFlag) {
		    numUnMatched++;
		}
		frmtIndex++;


	    } else if (scanIndex < scanArr.length &&
                      scanArr[scanIndex] == frmtArr[frmtIndex]) {
	        /*
		 * No '%' was found, but the characters matched
		 */

	        scanIndex++;
	        frmtIndex++;

	    } else {
	        /*
		 * No '%' found and the characters int frmtArr & scanArr
		 * did not match.
		 */

		 frmtIndex++;
		
	    }

	}

	/*
	 * The numMatched is the return value: a count of the num of vars set.
	 * While the numUnMatched is the number of formatSpecifiers that
	 * passed the parsing stage, but did not match anything in the scanArr.
	 */

	if ((numMatched + numUnMatched) != (argv.length - 3)) {
	    errorDiffVars(interp);
	}
 	interp.setResult(TclInteger.newInstance(numMatched));

    }


    /**
     * Given an array and an index into it, move the index forward
     * until a non-whitespace char is found.
     *
     * @param arr   - the array to search
     * @param index - where to begin the search
     * @return The index value where the whitespace ends.
     */

    private int skipWhiteSpace(char[] arr, int index) {
        boolean cont;
	do {
	    if (index >= arr.length) {
	        return index;
	    }		  
	    cont = false;
	    switch (arr[index]) {
	        case '\t':
	        case '\n':
		case '\r':
		case '\f':
		case ' ': {
		    cont = true;
		    index++;
		}
	    }
	} while (cont);
	
	return index;
    }


    /**
     * Called whenever the cmdProc wants to set an interp value.  
     * This method <ol>
     * <li> verifies that there exisits a varName from the argv array, 
     * <li> that the variable either dosent exisit or is of type scalar
     * <li> set the variable in interp if (1) and (2) are OK
     * </ol>
     * 
     * @param interp   - the Tcl interpreter
     * @param argv     - the argument array
     * @param argIndex - the current index into the argv array
     * @param tobj     - the TclObject that the varName equals
     * 
     */

    private static void testAndSetVar(Interp interp, TclObject[] argv, 
            int argIndex, TclObject tobj) throws TclException { 
        if ( argIndex < argv.length ) {
	    try {
		interp.setVar(argv[argIndex].toString(), tobj, 0);
	    } catch (TclException e) {
	        throw new TclException(interp,
		    "couldn't set variable \"" + argv[argIndex].toString() +
		             "\"");
	    }
	} else {
	    errorDiffVars(interp);
	}
    }


    /**
     * Called whenever the frmtIndex in the cmdProc is changed.  It verifies
     * the the array index is still within the bounds of the array.  If no
     * throw error.
     * @param interp  - The TclInterp which called the cmdProc method .
     * @param arr     - The array to be checked.
     * @param index   - The new value for the array index.
     */

    private static final void checkOverFlow(Interp interp, char[] arr, 
            int index) throws TclException {      
        if ((index >= arr.length) || (index < 0)) {
	    throw new TclException(interp,
                    "\"%n$\" argument index out of range");
	}    
    }


    /**
     * Called whenever the number of varName args do not match the number
     * of found and valid formatSpecifiers (matched and unmatched).
     *
     * @param interp  - The TclInterp which called the cmdProc method .
     */

     private static final void errorDiffVars(Interp interp) 
            throws TclException {

        throw new TclException(interp,
            "different numbers of variable names and field specifiers");
    }


    /**
     * Called whenever the current char in the frmtArr is erroneous
     *
     * @param interp  - The TclInterp which called the cmdProc method .
     * @param fieldSpecifier  - The erroneous character
     */

    private static final void errorBadField(Interp interp, char fieldSpecifier)
            throws TclException {      
        throw new TclException(interp, "bad scan conversion character \""
	        + fieldSpecifier + "\"");
    }


    /**
     * Called whenever the a width field is used in a char ('c') format 
     * specifier
     *
     * @param interp  - The TclInterp which called the cmdProc method .
     */

    private static final void errorCharFieldWidth(Interp interp) 
            throws TclException {      
        throw new TclException(interp, 
                "field width may not be specified in %c conversion");
    }
}
