// $Id: TextInput.java,v 1.9 2004/03/05 17:05:44 m31 Exp $
//
// This file is part of the program suite "Principia Mathematica II"
// which is a working prototype for the main project:
// "Hilbert II" - http://www.qedeq.org
//
// Copyright 2001-2004  Michael Meyling <mime@qedeq.org>.
//
// "Principia Mathematica II" is free software; you can redistribute
// it and/or modify it under the terms of the GNU General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

package com.meyling.principia.io;


/**
 * This class provides convenient methods for parsing input.
 *
 * TODO offer input stream constructor
 *
 * @version $Revision: 1.9 $
 * @author  Michael Meyling
 */
public class TextInput {

    /** char marking end of data */
    private final static char EOF = Character.MAX_VALUE;

    /** char marking end of input line */
    private final static char CR = '\n';

    /** string for marking current reading position */
    private final static String MARKER = "<<<<";

    /** holds the data */
    private final StringBuffer source;

    /** current line number (starting with 0) */
    private int lineNumber = 0;

    /** current column (starting with 0) */
    private int column = 0;

    /** current reading position */
    private int position = 0;

    /** address of input, for identifing address */
    private final String address;

    /** local address of input, for identifing address */
    private final String localAddress;


    /**
     * Constructor using <code>StringBuffer</code> source.
     *
     * @param   source  data source
     * @param   address for identifing source
     * @param   localAddress   source address
     * @throws  IllegalArgumentException if argument was
     *          an nullpointer
     */
    public TextInput(final StringBuffer source, final String address,
            final String localAddress) {
        if (source == null | address == null | localAddress == null) {
            throw new IllegalArgumentException(
                IoConstants.NULLPOINTER_PARAMETER);
        }
        this.source = source;
        this.address = address;
        this.localAddress = localAddress;
        int i = 0;
    }


    /**
     * Constructor using <code>String</code> source.
     *
     * @param   source  data source
     * @param   address for identifing source
     * @param   localAddress   source address
     * @throws  IllegalArgumentException if argument was
     *          an nullpointer
     */
    public TextInput(String source, final String address,
            final String localAddress) {
        if (source == null | address == null | localAddress == null) {
            throw new IllegalArgumentException(
                IoConstants.NULLPOINTER_PARAMETER);
        }
        this.source = new StringBuffer(source);
        this.address = address;
        this.localAddress = localAddress;
        int i = 0;
    }


    /**
     * Reads a single character and increments the reading position
     * by one.
     *
     * @return  character read, if there are no more chars
     *          <code>Character.MAX_VALUE</code> is returned
     *
     */
    public final char readChar() {
        if (position >= source.length()) {
            return EOF;
        } else {
            char c = source.charAt(position++);
            if (c == CR) {
                lineNumber++;
                column = 0;
            } else {
                column++;
            }
            return c;
        }
    }


    /**
     * Reads a single character and does not change the reading
     * position.
     *
     * @return  character read, if there are no more chars
     *          <code>Character.MAX_VALUE</code> is returned
     *
     */
    public final char getChar() {
        if (position >= source.length()) {
            return EOF;
        } else {
            return source.charAt(position);
        }
    }


    /**
     * Reads a single character and does not change the reading
     * position.
     *
     * @param   i   offset from current reading position.
     * @return  character read, if there are no more chars
     *          <code>Character.MAX_VALUE</code> is returned
     *
     */
    public final char getChar(int i) {
        if (i < 0 || position + i >= source.length()) {
            return EOF;
        } else {
            return source.charAt(position + i);
        }
    }


    /**
     * Skips white space, beginning from reading position.
     * Changes reading position to next non white space
     * character.
     */
    public final void skipWhiteSpace() {
        while (!isEmpty() && Character.isWhitespace(getChar())) {
            readChar();
        }
    }


    /**
     * Is there no data left for reading?
     *
     * @return  is all data read?
     */
    public final boolean isEmpty() {
        return position >= source.length();
    }


    /**
     * Reads the next string containing only letters or digits,
     * leading whitespace is skipped.
     * Changes reading position.
     *
     * @return  read string
     * @throws  IllegalArgumentException if no such characters could
     *          be found
     */
    public final String readLetterDigitString() {
        skipWhiteSpace();
        if (isEmpty() || !Character.isLetterOrDigit(getChar())) {
            readChar();     // for showing correct position
            throw new IllegalArgumentException(
                IoConstants.NO_LETTER_DIGIT_STRING_FOUND);
        }
        StringBuffer buffer = new StringBuffer();
        while (!isEmpty() && Character.isLetterOrDigit(getChar())) {
            buffer.append(readChar());
        }
        return buffer.toString();
    }


    /**
     * Reads the next latex string containing
     * Changes reading position.
     *
     * @return  read string
     */
    public final String readLatexEnvironment() {
        skipWhiteSpace();
        int counter = 1;
        StringBuffer buffer = new StringBuffer();
        while (!isEmpty()) {
            if (getChar() == '}') {
                counter--;
                if (counter <= 0) {
                    break;
                }
            } else if (getChar() == '{') {
                counter++;
            }
            buffer.append(readChar());
        }
        return buffer.toString();
    }


    /**
     * Reads the next word string containing
     * Changes reading position.
     *
     * @return  read string
     */
    public final String readLatexWord() {
        skipWhiteSpace();
        StringBuffer buffer = new StringBuffer();
BRLS:   while (!isEmpty() && !Character.isWhitespace(getChar())
            && Character.isLetterOrDigit(getChar())) {
/*
            switch (getChar()) {
                case '{':
                case '}':
                case '[':
                case ']':
                case '^':
                case '_':
                case '$':
                case '~':
                case '%':
                case '\\':
                case '#':
                case '&':
                    break BRLS;
            }
*/
            buffer.append(readChar());
        }
        return buffer.toString();
    }


    /**
     * Reads the next positive integer made completly out of digits,
     * leading whitespace is skipped.
     * Changes reading position.
     *
     * @return  read integer
     * @throws  IllegalArgumentException if no digits where found or
     *          the number was to big for an <code>int</code>
     */
    public final int readCounter() {
        skipWhiteSpace();
        final int begin = getPosition();
        if (isEmpty()) {
            throw new IllegalArgumentException(
                IoConstants.NO_POSITIVE_INTEGER_FOUND);
        }
        if (!Character.isDigit(getChar())) {
            readChar();     // for showing correct position
            throw new IllegalArgumentException(
                IoConstants.NO_POSITIVE_INTEGER_FOUND);
        }
        StringBuffer buffer = new StringBuffer();
        while (!isEmpty() && Character.isDigit(getChar())) {
            buffer.append(readChar());
        }
        if (buffer.length() >= 2 && '0' == buffer.charAt(0)) {
            setPosition(begin);     // for showing correct position
            throw new IllegalArgumentException(
                IoConstants.NO_LEADING_ZERO_ALLOWED);
        }
        int i = 0;
        try {   // TODO better parse themself?
            i = Integer.parseInt(buffer.toString());
        } catch (NumberFormatException nfe) {
            setPosition(begin);     // for showing correct position
            throw new IllegalArgumentException(IoConstants.INTEGER_TOO_BIG);
        }
        return i;
    }


    /**
     * Reads the next quoted string, leading whitespace is skipped.
     * A correctly quoted string could be created by adding a leading and
     * a trailing quote character and doubling each other quote character.
     * The resulting string is dequoted.
     * Changes reading position.
     *
     * @return  deqoted read string
     * @throws  IllegalArgumentException if no correctly quoted
     *          string was found
     */
    public final String readQuoted() throws IllegalArgumentException {
        skipWhiteSpace();
        if (isEmpty() || readChar() != '\"') {
            throw new IllegalArgumentException(
                IoConstants.NO_QUOTED_STRING);
        }
        StringBuffer unquoted = new StringBuffer();
        char c;
        do {
            if (isEmpty()) {
                throw new IllegalArgumentException(
                    IoConstants.NO_QUOTE_TERMINATION);
            }
            c = readChar();
            if (c != '\"') {
                unquoted.append(c);
            } else {        // c == '\"'
                if (isEmpty() || getChar() != '\"') {
                    break;  // success
                } else {
                    unquoted.append(readChar());
                }
            }
        } while (true);
        return unquoted.toString();
    }


    /**
     * Returns the current line number.
     *
     * @return  current line number (starting with line 1)
     */
    public final int getRow() {
        return lineNumber + 1;
    }


    /**
     * Returns the current column number.
     *
     * @return  current column number (starting with line 1)
     */
    public final int getColumn() {
        return column + 1;
    }


    /**
     * Returns the current line.
     *
     * @return  current line
     */
    public final String getLine() {
        StringBuffer line = new StringBuffer();
        int min =  this.position - 1;
        while (min >= 0 && this.source.charAt(min) != CR) {
            min--;
        }
        int max = this.position;
        while (max < this.source.length()
                && this.source.charAt(max) != CR) {
            max++;
        }
        if (min + 1 < max) {
            return this.source.substring(min + 1, max);
        } else {
            return "";
        }
    }


    /**
     * Returns the current position.
     *
     * @return  current position
     */
    public final int getPosition() {
        return position;
    }


    /**
     * Returns the highest position number possible. This is equal
     * to the length of the source.
     *
     * @return  maximum position
     */
    public final int getMaximumPosition() {
        return source.length();
    }


    /**
     * Sets the current position (and indirectly the line number).
     *
     * @param  position set current position
     */
    public final void setPosition(final int position) {
        if (position >= source.length()) {
            this.position = source.length();
        } else if (this.position != position) {
            this.position = 0;
            this.lineNumber = 0;
            this.column = 0;
            for (int i = 0; i < position; i++) {    // Q & D
                readChar();
            }
        }
    }


    /**
     * Get address (or something to identify it) of input source.
     *
     * @return  address of input source
     */
    public final String getAddress() {
        return this.address;
    }


    /**
     * Get local address (or something to identify it) of input source.
     *
     * @return  local address of input source
     */
    public final String getLocalAddress() {
        return this.localAddress;
    }


    /**
     * Show reading position.
     *
     * @return  current line with mark at current reading position
     */
    public final String showLinePosition() {
        final String line = getLine();
        final StringBuffer buffer = new StringBuffer();
        final int col = getColumn() - 1;
        if (col > 0) {
            if (col < line.length()) {
                buffer.append(line.substring(0, col));
            } else {
                buffer.append(line);
            }
        }
        buffer.append(MARKER);
        if (col < line.length()) {
            buffer.append(line.substring(col));
        }
        return buffer.toString();
    }

/*
    public final void mark() {
        savedPosition = position;
        savedLineNumber = lineNumber;

        savedLine.setLength(0);
// TODO inefficent:
        savedLine.append(line.toString());
    }


    public final void reset() {
        position = savedPosition;
        lineNumber = savedLineNumber;

        line.setLength(0);
// TODO inefficent:
        line.append(savedLine.toString());
    }
*/


}
