import java.io.File;
import java.io.FileReader;
import java.io.IOException;

import java.util.NoSuchElementException;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;

/** Represents a stream of English words coming from a file.
 * Each English word is taken to be any combination of
 * characters beginning with a letter, followed by letters,
 * apostrophes, and hyphens, where each apostrophe and hyphen
 * must be followed by a letter. The punctuation and white
 * space separating words is not available through this class.
 */
public class WordReader {
    private static final int MAX_WORD_LEN = 128; // maximum word length
    private static final int BUFFER_LEN = 1024;  // length of buffer
    private static JFileChooser chooser = null;

    /** Prompts the user for a file to open. The dialog's title
     * and its button's text will be as specified. */
    public static WordReader promptUser(String title, String button) {
        while(true) {
            if(chooser == null) chooser = new JFileChooser();
            chooser.setDialogTitle(title);
            int action = chooser.showDialog(null, button);
            if(action != JFileChooser.APPROVE_OPTION) {
                return null;
            } else {
                try {
                    return new WordReader(chooser.getSelectedFile());
                } catch(IOException ex) {
                    JOptionPane.showMessageDialog(null,
                            "File could not be opened. Try Again.",
                            "Could Not Open File",
                            JOptionPane.ERROR_MESSAGE);
                }
            }
        }
    }

    private FileReader in; // file to read from
    private char[] buffer; // characters currently being processed
    private int bufferLen; // number of characters in buffer
    private int bufferPos; // position within the buffer
    private char[] curWord; // characters found in current word
    private int curWordLen; // number of characters in current word

    /** Constructs a WordReader stream within the file. The
     * first word within the file (if it exists) will be returned by
     * the first call to nextToken. */
    public WordReader(File f) throws IOException {
        in = new FileReader(f);

        buffer = new char[BUFFER_LEN];
        bufferPos = 0;
        bufferLen = 0;

        curWord = new char[MAX_WORD_LEN];
        curWordLen = 0;
    }

    /** Returns true if the stream has more words within it. */
    public boolean hasMoreTokens() {
        ensureNextWord();
        return curWordLen > 0;
    }

    /** Returns the next word in the stream. Note that, like an
     * iterator, this will retrieve the next word each time. If
     * there are no words left in the stream, it throws a
     * NoSuchElementException (but this will not happen if one
     * uses hasMoreTokens to confirm the call each time). */
    public String nextToken() {
        ensureNextWord();
        if(curWordLen <= 0) {
            throw new NoSuchElementException("no more words");
        } else {
            String ret = new String(curWord, 0, curWordLen);
            curWordLen = 0;
            return ret;
        }
    }

    /** Reads the next word in the file, filling up curWord
     * with the word's characters and setting curWordLen to be
     * the number of characters. If there are no more words
     * (either because the end of file was reached, or because
     * there was an IOException), then curWordLen will be set
     * to 0. */
    private void ensureNextWord() {
        if(curWordLen != 0) return; // word is already present
        if(in == null) return;   // EOF passed over

        int state = 0;
        char punct = '_';
        while(true) {
            while(bufferPos >= bufferLen) {
                try {
                    bufferLen = in.read(buffer);
                    bufferPos = 0;
                } catch(IOException e) {
                    bufferLen = -1;
                }
                if(bufferLen < 0) {
                    try { in.close(); } catch(IOException e) { }
                    in = null;
                    return;
                }
            }

            char c = buffer[bufferPos];
            ++bufferPos;

            switch(state) {
            case 0:
                if(Character.isLetter(c)) {
                    addToCurWord(c);
                    state = 1;
                }
                break;
            case 1:
                if(Character.isLetter(c)) {
                    addToCurWord(c);
                } else if(c == '\'' || c == '-') {
                    punct = c;
                    state = 2;
                } else {
                    return;
                }
                break;
            case 2:
                if(Character.isLetter(c)) {
                    addToCurWord(punct);
                    addToCurWord(c);
                    state = 1;
                } else {
                    return;
                }
                break;
            }
        }
    }

    /** Adds the given character to the current word stored in
     * curWord. */
    private void addToCurWord(char c) {
        if(curWordLen < curWord.length) {
            curWord[curWordLen] = Character.toLowerCase(c);
            curWordLen++;
        }
    }
}
