package cnp.ew.converter;
import java.awt.*;
import java.util.*;
import java.io.*;
/**
 * Converts a date to a string using a predefined format string.
 * Format strings can be literal text with predefined expandible tokens
 * contained with it; if the literal text needs to be tokens that
 * are actually expandible tokens, quotes ("") may be used to force the
 * text to be interpretted literally.
 *
 * Example usage:
 *<pre>
 *      CpDateToStringConverter converter = new CpDateToStringConverter();
 *      converter.setFormat("dddd, mmmm d, yyyy");
 *      String convertedToString = converter.convert(new Date("1/1/96"));
 *</pre>
 *
 * Note that setting the format is a relatively expensive operation; it is
 * recommended that this be done during initialization.
 *
 * Possible expandible tokens on a date are as follows:
 *
 * d	Day of the month in one or two numeric digits, as needed (1 to 31).
 * dd	Day of the month in two numeric digits (01 to 31).
 * ddd	First three letters of the weekday (Sun to Sat).
 * dddd	Full name of the weekday (Sunday to Saturday).
 * w	Day of the week (1 to 7).
 * ww	Week of the year (1 to 53).
 * m	Month of the year in one or two numeric digits, as needed (1 to 12).
 * mm	Month of the year in two numeric digits (01 to 12).
 * mmm	First three letters of the month (Jan to Dec).
 * mmmm	Full name of the month (January to December).
 * q	Date displayed as the quarter of the year (1 to 4).
 * y	Number of the day of the year (1 to 366).
 * yy	Last two digits of the year (01 to 99).
 * yyyy	Full year (0100 to 9999).
 * h	Hour in one or two digits, as needed (0 to 23).
 * hh	Hour in two digits (00 to 23).
 * n	Minute in one or two digits, as needed (0 to 59).
 * nn	Minute in two digits (00 to 59).
 * s	Second in one or two digits, as needed (0 to 59).
 * ss	Second in two digits (00 to 59).
 * AM/PM	Twelve-hour clock with the uppercase letters AM or PM, as appropriate.
 * am/pm	Twelve-hour clock with the lowercase letters am or pm, as appropriate.
 * A/P	Twelve-hour clock with the uppercase letter A or P, as appropriate.
 * a/p	Twelve-hour clock with the lowercase letter a or p, as appropriate.
 *
 * @version        $Version$
 * @author         $Author: Ken $
 */
public class CpDateToStringConverter extends CpFormattedToStringConverter
{

    private Vector fields;

    static final int STATE_START=0;
    static final int STATE_INDAY=1;
    static final int STATE_INMONTH=2;
    static final int STATE_INYEAR=3;
    static final int STATE_INLITERAL=4;
    static final int STATE_INQUOTEDLITERAL=5;
    static final int STATE_INHOUR=6;
    static final int STATE_INMINUTE=7;
    static final int STATE_INSECOND=8;
    static final int STATE_INQUARTER=9;
    static final int STATE_INWEEK=10;
    static final int STATE_INAMPM=11;

    /**
     * Creates a new converter.
     */
    public CpDateToStringConverter()
    {
        this("mm/dd/yy");
    }

    public CpDateToStringConverter(String format)
    {
        super(format);
    }


    /**
     * Parse the format string, converting the text of the format string into
     * a Vector of CpDateUnits.
     *
     * Implementation: uses a state machine for converting the text into tokens.
     * For tokens that are full words, string comparison is used.  For tokens that
     * consist of one or more of a character, the token is recognized as the largest
     * number of that character. Handles quoted literal substrings, including escaped (doubled)
     * quotes within a quoted literal.
     *
     * After the fields are created, they are scanned to determine if one of them is an AM/PM
     * field; if so, any hour fields that are included in the fields are told to display in
     * 12 hour format.
     */
    void parseFormatString()
    {
        char currentCharacter;
        int currentState=STATE_START;
        int charTypeCount = 0;
        int nextPosition = 0;
        int length = formatString.length();
        boolean usesAMPM=false;

        String currentToken = "";
        fields = new Vector(5);

        while (nextPosition < length) {
            currentCharacter = formatString.charAt(nextPosition++);
            switch (currentState) {
            case STATE_START:
                switch(currentCharacter) {
                case 'd':
                case 'D':
                    currentState = STATE_INDAY;
                    currentToken = currentToken + currentCharacter;
                    break;
                case 'm':
                case 'M':
                    currentState = STATE_INMONTH;
                    currentToken = currentToken + currentCharacter;
                    break;
                case 'y':
                case 'Y':
                    currentState = STATE_INYEAR;
                    currentToken = currentToken + currentCharacter;
                    break;
                case 'w':
                case 'W':
                    currentState = STATE_INWEEK;
                    currentToken = currentToken + currentCharacter;
                    break;
                case 'h':
                case 'H':
                    currentState = STATE_INHOUR;
                    currentToken = currentToken + currentCharacter;
                    break;
                case 'n':
                case 'N':
                    currentState = STATE_INMINUTE;
                    currentToken = currentToken + currentCharacter;
                    break;
                case 's':
                case 'S':
                    currentState = STATE_INSECOND;
                    currentToken = currentToken + currentCharacter;
                    break;
                case 'q':
                case 'Q':
                    currentState = STATE_INQUARTER;
                    currentToken = currentToken + currentCharacter;
                    break;
                case '"':
                    currentState = STATE_INQUOTEDLITERAL;
                    break;
                case 'a':
                    if (formatString.startsWith("am/pm", nextPosition - 1)) {
                        recognizeToken("am/pm", STATE_INAMPM);
                        nextPosition += 4;
                        usesAMPM=true;
                    } else {
                        if (formatString.startsWith("a/p", nextPosition - 1)) {
                            recognizeToken("a/p", STATE_INAMPM);
                            nextPosition += 2;
                            usesAMPM=true;
                        } else {
                            currentToken = currentToken + currentCharacter;
                            currentState = STATE_INLITERAL;
                        }
                    }
                    break;
                case 'A':
                    if (formatString.startsWith("AM/PM", nextPosition - 1)) {
                        recognizeToken("AM/PM", STATE_INAMPM);
                        nextPosition += 4;
                        usesAMPM=true;
                    } else {
                        if (formatString.startsWith("A/P", nextPosition - 1)) {
                            recognizeToken("A/P", STATE_INAMPM);
                            nextPosition += 2;
                            usesAMPM=true;
                        } else {
                            currentToken = currentToken + currentCharacter;
                            currentState = STATE_INLITERAL;
                        }
                    }
                    break;
                default:
                    currentState = STATE_INLITERAL;
                    currentToken = currentToken + currentCharacter;
                    break;
                }
                break;
            case STATE_INDAY:
            case STATE_INMONTH:
            case STATE_INYEAR:
            case STATE_INHOUR:
            case STATE_INMINUTE:
            case STATE_INSECOND:
            case STATE_INLITERAL:
            case STATE_INQUARTER:
            case STATE_INWEEK:
                if (isValidCharForState(currentCharacter, currentState)) {
                    currentToken = currentToken + currentCharacter;
                } else {
                    nextPosition--;
                    recognizeToken(currentToken, currentState);
                    currentToken = "";
                    currentState = STATE_START;
                }
                break;
            case STATE_INQUOTEDLITERAL:
                if (currentCharacter == '"') {
                    // Peek ahead to see if this isn't an embedded literal quote...
                    if (nextPosition < length) {
                        if (formatString.charAt(nextPosition) == '"') {
                            currentToken = currentToken + currentCharacter;
                        } else {
                            recognizeToken(currentToken, currentState);
                            currentToken = "";
                            currentState = STATE_START;
                        }
                    }
                } else {
                    currentToken = currentToken + currentCharacter;
                }
                break;
            }
        }
        // Make sure there isn't a partially finished token remaining in the buffer.
        if (currentToken.length() > 0) {
            recognizeToken(currentToken, currentState);
        }

        if (usesAMPM) {
            Enumeration e = fields.elements();
            Object field;
            while (e.hasMoreElements()) {
                field = e.nextElement();
                if (field instanceof CpHourToString) {
                    ((CpHourToString)field).is12HourClock(true);
                }
            }
        }

    }

    /**
     * A token has been recognized.  Use currentState to determine which kind
     * of CpDateUnit to create, and pass in the token as its format string.
     */
    void recognizeToken(String token, int currentState)
    {
        switch (currentState) {
        case STATE_INDAY:
            fields.addElement(new CpDayToString(token));
            break;
        case STATE_INMONTH:
            fields.addElement(new CpMonthToString(token));
            break;
        case STATE_INYEAR:
            fields.addElement(new CpYearToString(token));
            break;
        case STATE_INWEEK:
            fields.addElement(new CpWeekToString(token));
            break;
        case STATE_INHOUR:
            fields.addElement(new CpHourToString(token));
            break;
        case STATE_INMINUTE:
            fields.addElement(new CpMinuteToString(token));
            break;
        case STATE_INSECOND:
            fields.addElement(new CpSecondToString(token));
            break;
        case STATE_INQUARTER:
            fields.addElement(new CpQuarterToString(token));
            break;
        case STATE_INAMPM:
            fields.addElement(new CpAmpmToString(token));
            break;
        default:
            fields.addElement(new CpStringToString(token));
        }
    }

    /**
     * Answer whether or not the given character should keep the scanner
     * in the state it is currently in (curState).
     */

    boolean isValidCharForState(char curChar, int curState) {
        switch(curState) {
        case STATE_INDAY:
            return (curChar == 'd') || (curChar == 'D');
        case STATE_INMONTH:
            return (curChar == 'm') || (curChar == 'M');
        case STATE_INYEAR:
            return (curChar == 'y') || (curChar == 'Y');
        case STATE_INHOUR:
            return (curChar == 'h') || (curChar == 'H');
        case STATE_INMINUTE:
            return (curChar == 'n') || (curChar == 'N');
        case STATE_INSECOND:
            return (curChar == 's') || (curChar == 'S');
        case STATE_INQUARTER:
            return (curChar == 'q') || (curChar == 'Q');
        case STATE_INWEEK:
            return (curChar == 'w') || (curChar == 'W');
        case STATE_INLITERAL:
            String tokenCharacters = "dDmMyYhHnNsSqQwWaA\"";
            return (tokenCharacters.indexOf((int)curChar) < 0);

        // This should never happen; for the compiler.
        default:
            return false;
        }
    }


    /**
     * Convert a Date into a string, using the current format string.
     */
    public String convert(Object o)
    {
        Date d = (Date)o;
        StringBuffer buf = new StringBuffer("");
        for (int f = 0; f < fields.size(); f++) {
            CpToStringConverter field = (CpToStringConverter)fields.elementAt(f);
            buf.append(field.convert(d));
        }
        return buf.toString();
    }
}
