package jscheme;
import java.io.*;

/** InputPort is to Scheme as InputStream is to Java. 
 * @author Peter Norvig, peter@norvig.com http://www.norvig.com  
 * Copyright 1998 Peter Norvig, see http://www.norvig.com/license.html **/

public class InputPort extends SchemeUtils {

  static  String EOF = "#!EOF";
  boolean isPushedToken = false;
  boolean isPushedChar = false;
  Object  pushedToken = null;
  int     pushedChar = -1;
  Reader  in;
  StringBuffer buff = new StringBuffer();

  /** Construct an InputPort from an InputStream. **/
  public InputPort(InputStream in) { this.in = new InputStreamReader(in);}

  /** Construct an InputPort from a Reader. **/
  public InputPort(Reader in) { this.in = in;}

  /** Read and return a Scheme character or EOF. **/
  public Object readChar() {
    try {
      if (isPushedChar) {
	isPushedChar = false;
	if (pushedChar == -1) return EOF; else return chr((char)pushedChar);
      } else {
	int ch = in.read();
	if (ch == -1) return EOF; else return chr((char)ch);
      }
    } catch (IOException e) {
      warn("On input, exception: " + e);
      return EOF;
    }
  }

  /** Peek at and return the next Scheme character (or EOF).
   * However, don't consume the character. **/
  public Object peekChar() {
    int p = peekCh();
    if (p == -1) return EOF; else return chr((char)p);
  }

  /** Push a character back to be re-used later. **/
  int pushChar(int ch) {
    isPushedChar = true;
    return pushedChar = ch;
  }

  /** Pop off the previously pushed character. **/
  int popChar() {
    isPushedChar = false;
    return pushedChar;
  }

  /** Peek at and return the next Scheme character as an int, -1 for EOF.
   * However, don't consume the character. **/
  public int peekCh() {
    try { return isPushedChar ? pushedChar : pushChar(in.read()); }
    catch (IOException e) {
      warn("On input, exception: " + e);
      return -1;
    }
  }

  /** Read and return a Scheme expression, or EOF. **/
  public Object read() {
    try {
      Object token = nextToken(); 
      if (token == "(")
	return readTail(false);
      else if (token == ")")
	{ warn("Extra ) ignored."); return read(); }
      else if (token == ".")
	{ warn("Extra . ignored."); return read(); }
      else if (token == "'")
	return list("quote", read());
      else if (token == "`")
	return list("quasiquote", read());
      else if (token == ",")
	return list("unquote", read());
      else if (token == ",@")
	return list("unquote-splicing", read());
      else 
	return token;
    } catch (IOException e) {
      warn("On input, exception: " + e);
      return EOF;
    }
  }

  /** Close the port.  Return TRUE if ok. **/
  public Object close() {
    try { this.in.close(); return TRUE; }
    catch (IOException e) { return error("IOException: " + e); }
  }

  /** Is the argument the EOF object? **/
  public static boolean isEOF(Object x) { return x == EOF; }

  Object readTail(boolean dotOK) throws IOException {
    Object token = nextToken(); 
    if (token == EOF)
      return error("EOF during read.");
    else if (token == ")")
      return null;
    else if (token == ".") {
      Object result = read();
      token = nextToken(); 
      if (token != ")") warn("Where's the ')'? Got " +
			     token + " after .");
      return result;
    } else {
      isPushedToken = true;
      pushedToken = token;
      return cons(read(), readTail(true));
    }
  }

  Object nextToken() throws IOException {
    int ch;

    // See if we should re-use a pushed char or token
    if (isPushedToken) {
      isPushedToken = false;
      return pushedToken;
    } else if (isPushedChar) {
      ch = popChar();
    } else {
      ch = in.read();
    }

    // Skip whitespace
    while (Character.isWhitespace((char)ch)) ch = in.read();

    // See what kind of non-white character we got
    switch(ch) {
    case -1: return EOF;
    case '(' : return "(";
    case ')':  return ")";
    case '\'': return "'";
    case '`':  return "`";
    case ',': 
      ch = in.read();
      if (ch == '@') return ",@";
      else { pushChar(ch); return ","; }
    case ';': 
      // Comment: skip to end of line and then read next token
      while(ch != -1 && ch != '\n' && ch != '\r') ch = in.read();
      return nextToken();
    case '"':
      // Strings are represented as char[]
      buff.setLength(0);
      while ((ch = in.read()) != '"' && ch != -1) {
	buff.append((char) ((ch == '\\') ? in.read() : ch));
      }
      if (ch == -1) warn("EOF inside of a string.");
      return buff.toString().toCharArray(); 
    case '#':
      switch (ch = in.read()) {
      case 't': case 'T': return TRUE;
      case 'f': case 'F': return FALSE;
      case '(':
	pushChar('(');
	return listToVector(read());
      case '\\': 
	ch = in.read();
	if (ch == 's' || ch == 'S' || ch == 'n' || ch == 'N') {
	  pushChar(ch);
	  Object token = nextToken();
	  if (token == "space") return chr(' ');
	  else if (token == "newline") return chr('\n');
	  else {
	    isPushedToken = true;
	    pushedToken = token;
	    return chr((char)ch);
	  }
	} else {
	  return chr((char)ch);
	}
      case 'e': case 'i': case 'd': return nextToken();
      case 'b': case 'o': case 'x':
	warn("#" + ((char)ch) + " not implemented, ignored."); 
	return nextToken();
      default: 
	warn("#" + ((char)ch) + " not recognized, ignored."); 
	return nextToken();
      }
    default: 
      buff.setLength(0);
      int c = ch;
      do { 
	buff.append((char)ch);
	ch = in.read();
      } while (!Character.isWhitespace((char)ch) && ch != -1 &&
	       ch != '(' && ch != ')' && ch != '\'' && ch != ';'
	       && ch != '"' && ch != ',' && ch != '`');
      pushChar(ch);
      // Try potential numbers, but catch any format errors.
      if (c == '.' || c == '+' || c == '-' || (c >= '0' && c <= '9')) {
	try { return new Double(buff.toString()); }
	catch (NumberFormatException e) { ; }
      }
      return buff.toString().toLowerCase().intern();
    }
  }
}
