// Copyright (c) 1996-2002 Brian D. Carlstrom

package bdc.scheme;

import bdc.scheme.exception.ParseException;
import bdc.scheme.procedure.String2Number;
import bdc.util.Fmt;
import java.io.IOException;
import java.io.StreamTokenizer;

/**
    The Scheme reader.

    Implemented using the Java StreamTokenizer.
*/
public class Reader extends StreamTokenizer
{
    /**
        filename for debugging messages
    */
    private String filename;

    /**
        We keep the input so we can close it when we are done reading.
    */
    private java.io.Reader input;

    /**
        Create a reader object for an input stream.

        The filename is used only for debugging,
        so it maybe another descriptive String.
    */
    public Reader (String filename, java.io.Reader input)
    {
        super(input);
        this.filename = filename;
        this.input    = input;
        resetSyntax();
        wordChars(33, 127);
        wordChars(128 + 32, 255);
        whitespaceChars(0, ' ');
        commentChar(';');
        quoteChar('"');
        quoteChar('\'');
        ordinaryChar('(');
        ordinaryChar(')');
        ordinaryChar('\'');
        ordinaryChar('`');
        ordinaryChar(',');
        ordinaryChar('@');
    }

    /**
        The recursive reader
    */
    public Object read () throws ParseException
    {
        /* top level switch */
        switch (readNextToken()) {

            /*
                End of file
            */
          case TT_EOF:
            try {
                input.close();
                return Scheme.EOFObject;
            }
            catch (IOException ioe) {
                throw new ParseException(filename,
                                         lineno(),
                                         ioe.toString());
            }

            /*
                Reader macros
            */
          case '"':
            /* A quoted string */
            return sval.toCharArray();
          case '\'':
            return readQuoted("quote", read());
          case '`':
            return readQuoted("quasiquote", read());
          case ',':
            if (readNextToken() == '@') {
                return readQuoted("unquote-splicing", read());
            }
            pushBack();
            return readQuoted("unquote", read());

            /*
                An actual token of some kind. See below for details.
            */
          case TT_WORD:
            return readString (sval);

            /*
                The start of a list structure
            */
          case '(':
            /*
                root contains the root of the list we are making.
                pair contains a pointer to the last pair we added.
                newPair contains the pair we are creating.
            */
            Pair root = null;
            Pair pair = null;
            while (true) {
                Pair newPair;
                /* read the next part of the subexpression */
                switch (readNextToken()) {
                    /*
                        In the nested case EOF is an error
                    */
                  case TT_EOF:
                    throw new ParseException(filename,
                                             lineno(),
                                             "Unexpected EOF in list");
                    /*
                        Quoted strings and read macros are treated
                        almost the the same as above except the value
                        isn't returned but stored in the pair we are
                        constructing. I'm probably missing an
                        generalization of some kind here.
                    */
                  case '"':
                    newPair = pair(sval.toCharArray(), null);
                    break;
                  case '\'':
                    newPair = pair(readQuoted("quote", read()),
                                   null);
                    break;
                  case '`':
                    newPair = pair(readQuoted("quasiquote", read()),
                                   null);
                    break;
                  case ',':
                    if (readNextToken() == '@') {
                        newPair = pair(readQuoted("unquote-splicing",
                                                  read()),
                                       null);
                        break;
                    }
                    pushBack();
                    newPair = pair(readQuoted("unquote", read()),
                                   null);
                    break;

                    /*
                        Here things get a little different from the top level.

                        We handle the token "." so that we can
                        properly read in dotted pairs. We better
                        already have a pair under constuction if we
                        are filing in the cdr this way.

                        Otherwise we use readString to handle the
                        token as above.
                    */
                  case TT_WORD:
                    if (sval.equals(".")) {
                        if (pair == null || pair.car == null) {
                            throw new ParseException(filename,
                                                     lineno(),
                                                     "Malformed dotted pair");
                        }
                        pair.cdr = read();
                        if (pair.cdr == null) {
                            throw new ParseException(
                                filename,
                                lineno(),
                                "Unexpected EOF after dot");
                        }
                        if (readNextToken() != ')') {
                            throw new ParseException(
                                filename,
                                lineno(),
                                "Missing ')' after dotted pair");
                        }
                        return root;
                    }

                    newPair = pair(readString(sval), null);
                    break;

                    /*
                        Handle sub-sub lists  by backing off
                        and calling ourselves recursively.
                    */
                  case '(':
                    pushBack();
                    newPair = pair(read(), null);
                    if (newPair.car == null) {
                        throw new ParseException(filename,
                                                 lineno(),
                                                 "Expected closing ')'");
                    }
                    break;

                    /*
                        If we have a closing paren and we haven't had
                        any pairs created we have the empty list.
                        Otherwise we terminate the list we were
                        constructing and return the root of the list.
                    */
                  case ')':
                    if (root == null) {
                        return Scheme.Null;
                    }
                    pair.cdr = Scheme.Null;
                    return root;

                    /*
                        If we get here its our own fault
                    */
                  default:
                    throw new ParseException(filename,
                                             lineno(),
                                             Fmt.S("Unexpected character: %s",
                                             String.valueOf(((char)ttype))));
                }

                /*
                    Prepare to iterate.

                    If this is the first time through initialize the
                    root and tha pair pointer to our new pair.

                    Otherwise, advance store our newPair and advance
                    the pair pointer to it.
                */
                if (root == null) {
                    root = newPair;
                    pair = newPair;
                }
                else {
                    pair.cdr = newPair;
                    pair = (Pair)pair.cdr;
                }
            }

            /*
                If we get here its our own fault
            */
          default:
            throw new ParseException(filename,
                                     lineno(),
                                     Fmt.S("Unexpected character: %s",
                                           String.valueOf(((char)ttype))));
        }
    }

    /**
        helper function for the many reader quoting mechanisms
    */
    private Object readQuoted (String string, Object object)
    {
        return pair(Symbol.get(string),
                    pair(object,
                         Scheme.Null));
    }

    /**
        readString knows how to take what the StreamTokenizer thinks
        is a String and potential returns Symbols, Characters,
        Booleans, or Numbers. Most of this work is because the
        StreamTokenizer isn't very good.
    */
    private Object readString (String sval) throws ParseException
    {
        /* Special case - so it doesn't think its a negative number */
        if (sval.equals("-")) {
            return Symbol.get(sval);
        }
        /* See if we have a character constant */
        if (sval.startsWith("#\\")) {
            /* see if it one of the really special ones... */
            if (sval.equalsIgnoreCase("#\\space")) {
                return new Character(' ');
            }
            if (sval.equalsIgnoreCase("#\\newline")) {
                return new Character('\n');
            }
            /* ... or if it just an ordinary one */
            if (sval.length() < 3) {
                /*
                    temporarily instruct the StreamTokenizer to treat
                    the comment character and whitespace so we can
                    have character constants for them.
                */
                ordinaryChar(';');
                ordinaryChars(0, ' ');
                int i = readNextToken();
                commentChar(';');
                whitespaceChars(0, ' ');
                return new Character((char)i);
            }
            /* hmm, I'm not sure why we'd reach here. What does #\foo mean? */
            return new Character (sval.charAt(2));
        }
        /* See if we have a Boolean constant */
        if (sval.equalsIgnoreCase("#t")) {
            return Boolean.TRUE;
        }
        if (sval.equalsIgnoreCase("#f")) {
            return Boolean.FALSE;
        }

        /* See if we have a Number */
        Object object = String2Number.string2Number (sval);
        if (object != Boolean.FALSE) {
            return object;
        }

        /* Just a ordinary Symbol */
        return Symbol.get(sval);
    }

    /**
        readNextToken is a wrapper around nextToken.
        It converts IOExceptions to ParseException.
        Its also a nice place to put debugging output
    */
    private int readNextToken () throws ParseException
    {
        try {
            return nextToken();
        }
        catch (IOException ioe) {
            throw new ParseException(filename,
                                     lineno(),
                                     ioe.toString());
        }
    }

    /**
        A pair factory so its easy to change the type of pair used
    */
    private Pair pair (Object car,
                       Object cdr)
    {
        return new DebugPair(car, cdr, filename, lineno());
    }
}
