// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
//
// Copyright (C) 2003-2004 Nominum, Inc.
// 
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
// OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//

package org.xbill.DNS;

import java.io.*;
import java.net.*;

import org.xbill.DNS.utils.*;

/**
 * Tokenizer is used to parse DNS records and zones from text format,
 *
 * @author Brian Wellington
 * @author Bob Halley
 */

public class Tokenizer {

    private static String delim = " \t\n;()\"";
    private static String quotes = "\"";

    /**
     * End of file
     */
    public static final int EOF = 0;

    /**
     * End of line
     */
    public static final int EOL = 1;

    /**
     * Whitespace; only returned when wantWhitespace is set
     */
    public static final int WHITESPACE = 2;

    /**
     * An identifier (unquoted string)
     */
    public static final int IDENTIFIER = 3;

    /**
     * A quoted string
     */
    public static final int QUOTED_STRING = 4;

    /**
     * A comment; only returned when wantComment is set
     */
    public static final int COMMENT = 5;

    private PushbackInputStream is;
    private boolean ungottenToken;
    private int multiline;
    private boolean quoting;
    private String delimiters;
    private Token current;
    private StringBuffer sb;
    private boolean wantClose;

    private String filename;
    private int line;

    public static class Token {
        /**
         * The type of token.
         */
        public int type;

        /**
         * The value of the token, or null for tokens without values.
         */
        public String value;

        private Token() {
            type = -1;
            value = null;
        }

        private Token
        set(int type, StringBuffer value) {
            if (type < 0)
                throw new IllegalArgumentException();
            this.type = type;
            this.value = value == null ? null : value.toString();
            return this;
        }

        /**
         * Converts the token to a string containing a representation useful
         * for debugging.
         */
        public String
        toString() {
            switch (type) {
                case EOF:
                    return "<eof>";
                case EOL:
                    return "<eol>";
                case WHITESPACE:
                    return "<whitespace>";
                case IDENTIFIER:
                    return "<identifier: " + value + ">";
                case QUOTED_STRING:
                    return "<quoted_string: " + value + ">";
                case COMMENT:
                    return "<comment: " + value + ">";
                default:
                    return "<unknown>";
            }
        }

        /**
         * Indicates whether this token contains a string.
         */
        public boolean
        isString() {
            return (type == IDENTIFIER || type == QUOTED_STRING);
        }

        /**
         * Indicates whether this token contains an EOL or EOF.
         */
        public boolean
        isEOL() {
            return (type == EOL || type == EOF);
        }
    }

    static class TokenizerException extends TextParseException {
        String message;

        public TokenizerException(String filename, int line, String message) {
            super(filename + ":" + line + ": " + message);
            this.message = message;
        }

        public String
        getBaseMessage() {
            return message;
        }
    }

    /**
     * Creates a Tokenizer from an arbitrary input stream.
     *
     * @param is The InputStream to tokenize.
     */
    public Tokenizer(InputStream is) {
        if (!(is instanceof BufferedInputStream))
            is = new BufferedInputStream(is);
        this.is = new PushbackInputStream(is, 2);
        ungottenToken = false;
        multiline = 0;
        quoting = false;
        delimiters = delim;
        current = new Token();
        sb = new StringBuffer();
        filename = "<none>";
        line = 1;
    }

    /**
     * Creates a Tokenizer from a string.
     *
     * @param s The String to tokenize.
     */
    public Tokenizer(String s) {
        this(new ByteArrayInputStream(s.getBytes()));
    }

    /**
     * Creates a Tokenizer from a file.
     *
     * @param f The File to tokenize.
     */
    public Tokenizer(File f) throws FileNotFoundException {
        this(new FileInputStream(f));
        wantClose = true;
        filename = f.getName();
    }

    private int
    getChar() throws IOException {
        int c = is.read();
        if (c == '\r') {
            int next = is.read();
            if (next != '\n')
                is.unread(next);
            c = '\n';
        }
        if (c == '\n')
            line++;
        return c;
    }

    private void
    ungetChar(int c) throws IOException {
        if (c == -1)
            return;
        is.unread(c);
        if (c == '\n')
            line--;
    }

    private int
    skipWhitespace() throws IOException {
        int skipped = 0;
        while (true) {
            int c = getChar();
            if (c != ' ' && c != '\t') {
                if (!(c == '\n' && multiline > 0)) {
                    ungetChar(c);
                    return skipped;
                }
            }
            skipped++;
        }
    }

    private void
    checkUnbalancedParens() throws TextParseException {
        if (multiline > 0)
            throw exception("unbalanced parentheses");
    }

    /**
     * Gets the next token from a tokenizer.
     *
     * @param wantWhitespace If true, leading whitespace will be returned as a
     *                       token.
     * @param wantComment    If true, comments are returned as tokens.
     * @return The next token in the stream.
     * @throws TextParseException The input was invalid.
     * @throws IOException        An I/O error occurred.
     */
    public Token
    get(boolean wantWhitespace, boolean wantComment) throws IOException {
        int type;
        int c;

        if (ungottenToken) {
            ungottenToken = false;
            if (current.type == WHITESPACE) {
                if (wantWhitespace)
                    return current;
            } else if (current.type == COMMENT) {
                if (wantComment)
                    return current;
            } else {
                if (current.type == EOL)
                    line++;
                return current;
            }
        }
        int skipped = skipWhitespace();
        if (skipped > 0 && wantWhitespace)
            return current.set(WHITESPACE, null);
        type = IDENTIFIER;
        sb.setLength(0);
        while (true) {
            c = getChar();
            if (c == -1 || delimiters.indexOf(c) != -1) {
                if (c == -1) {
                    if (quoting)
                        throw exception("EOF in " +
                                "quoted string");
                    else if (sb.length() == 0)
                        return current.set(EOF, null);
                    else
                        return current.set(type, sb);
                }
                if (sb.length() == 0 && type != QUOTED_STRING) {
                    if (c == '(') {
                        multiline++;
                        skipWhitespace();
                        continue;
                    } else if (c == ')') {
                        if (multiline <= 0)
                            throw exception("invalid " +
                                    "close " +
                                    "parenthesis");
                        multiline--;
                        skipWhitespace();
                        continue;
                    } else if (c == '"') {
                        if (!quoting) {
                            quoting = true;
                            delimiters = quotes;
                            type = QUOTED_STRING;
                        } else {
                            quoting = false;
                            delimiters = delim;
                            skipWhitespace();
                        }
                        continue;
                    } else if (c == '\n') {
                        return current.set(EOL, null);
                    } else if (c == ';') {
                        while (true) {
                            c = getChar();
                            if (c == '\n' || c == -1)
                                break;
                            sb.append((char) c);
                        }
                        if (wantComment) {
                            ungetChar(c);
                            return current.set(COMMENT, sb);
                        } else if (c == -1 &&
                                type != QUOTED_STRING) {
                            checkUnbalancedParens();
                            return current.set(EOF, null);
                        } else if (multiline > 0) {
                            skipWhitespace();
                            sb.setLength(0);
                            continue;
                        } else
                            return current.set(EOL, null);
                    } else
                        throw new IllegalStateException();
                } else
                    ungetChar(c);
                break;
            } else if (c == '\\') {
                c = getChar();
                if (c == -1)
                    throw exception("unterminated escape sequence");
                sb.append('\\');
            } else if (quoting && c == '\n') {
                throw exception("newline in quoted string");
            }
            sb.append((char) c);
        }
        if (sb.length() == 0 && type != QUOTED_STRING) {
            checkUnbalancedParens();
            return current.set(EOF, null);
        }
        return current.set(type, sb);
    }

    /**
     * Gets the next token from a tokenizer, ignoring whitespace and comments.
     *
     * @return The next token in the stream.
     * @throws TextParseException The input was invalid.
     * @throws IOException        An I/O error occurred.
     */
    public Token
    get() throws IOException {
        return get(false, false);
    }

    /**
     * Returns a token to the stream, so that it will be returned by the next call
     * to get().
     *
     * @throws IllegalStateException There are already ungotten tokens.
     */
    public void
    unget() {
        if (ungottenToken)
            throw new IllegalStateException
                    ("Cannot unget multiple tokens");
        if (current.type == EOL)
            line--;
        ungottenToken = true;
    }

    /**
     * Gets the next token from a tokenizer and converts it to a string.
     *
     * @return The next token in the stream, as a string.
     * @throws TextParseException The input was invalid or not a string.
     * @throws IOException        An I/O error occurred.
     */
    public String
    getString() throws IOException {
        Token next = get();
        if (!next.isString()) {
            throw exception("expected a string");
        }
        return next.value;
    }

    private String
    _getIdentifier(String expected) throws IOException {
        Token next = get();
        if (next.type != IDENTIFIER)
            throw exception("expected " + expected);
        return next.value;
    }

    /**
     * Gets the next token from a tokenizer, ensures it is an unquoted string,
     * and converts it to a string.
     *
     * @return The next token in the stream, as a string.
     * @throws TextParseException The input was invalid or not an unquoted string.
     * @throws IOException        An I/O error occurred.
     */
    public String
    getIdentifier() throws IOException {
        return _getIdentifier("an identifier");
    }

    /**
     * Gets the next token from a tokenizer and converts it to a long.
     *
     * @return The next token in the stream, as a long.
     * @throws TextParseException The input was invalid or not a long.
     * @throws IOException        An I/O error occurred.
     */
    public long
    getLong() throws IOException {
        String next = _getIdentifier("an integer");
        if (!Character.isDigit(next.charAt(0)))
            throw exception("expected an integer");
        try {
            return Long.parseLong(next);
        } catch (NumberFormatException e) {
            throw exception("expected an integer");
        }
    }

    /**
     * Gets the next token from a tokenizer and converts it to an unsigned 32 bit
     * integer.
     *
     * @return The next token in the stream, as an unsigned 32 bit integer.
     * @throws TextParseException The input was invalid or not an unsigned 32
     *                            bit integer.
     * @throws IOException        An I/O error occurred.
     */
    public long
    getUInt32() throws IOException {
        long l = getLong();
        if (l < 0 || l > 0xFFFFFFFFL)
            throw exception("expected an 32 bit unsigned integer");
        return l;
    }

    /**
     * Gets the next token from a tokenizer and converts it to an unsigned 16 bit
     * integer.
     *
     * @return The next token in the stream, as an unsigned 16 bit integer.
     * @throws TextParseException The input was invalid or not an unsigned 16
     *                            bit integer.
     * @throws IOException        An I/O error occurred.
     */
    public int
    getUInt16() throws IOException {
        long l = getLong();
        if (l < 0 || l > 0xFFFFL)
            throw exception("expected an 16 bit unsigned integer");
        return (int) l;
    }

    /**
     * Gets the next token from a tokenizer and converts it to an unsigned 8 bit
     * integer.
     *
     * @return The next token in the stream, as an unsigned 8 bit integer.
     * @throws TextParseException The input was invalid or not an unsigned 8
     *                            bit integer.
     * @throws IOException        An I/O error occurred.
     */
    public int
    getUInt8() throws IOException {
        long l = getLong();
        if (l < 0 || l > 0xFFL)
            throw exception("expected an 8 bit unsigned integer");
        return (int) l;
    }

    /**
     * Gets the next token from a tokenizer and parses it as a TTL.
     *
     * @return The next token in the stream, as an unsigned 32 bit integer.
     * @throws TextParseException The input was not valid.
     * @throws IOException        An I/O error occurred.
     * @see TTL
     */
    public long
    getTTL() throws IOException {
        String next = _getIdentifier("a TTL value");
        try {
            return TTL.parseTTL(next);
        } catch (NumberFormatException e) {
            throw exception("expected a TTL value");
        }
    }

    /**
     * Gets the next token from a tokenizer and parses it as if it were a TTL.
     *
     * @return The next token in the stream, as an unsigned 32 bit integer.
     * @throws TextParseException The input was not valid.
     * @throws IOException        An I/O error occurred.
     * @see TTL
     */
    public long
    getTTLLike() throws IOException {
        String next = _getIdentifier("a TTL-like value");
        try {
            return TTL.parse(next, false);
        } catch (NumberFormatException e) {
            throw exception("expected a TTL-like value");
        }
    }

    /**
     * Gets the next token from a tokenizer and converts it to a name.
     *
     * @param origin The origin to append to relative names.
     * @return The next token in the stream, as a name.
     * @throws TextParseException    The input was invalid or not a valid name.
     * @throws IOException           An I/O error occurred.
     * @throws RelativeNameException The parsed name was relative, even with the
     *                               origin.
     * @see Name
     */
    public Name
    getName(Name origin) throws IOException {
        String next = _getIdentifier("a name");
        try {
            Name name = Name.fromString(next, origin);
            if (!name.isAbsolute())
                throw new RelativeNameException(name);
            return name;
        } catch (TextParseException e) {
            throw exception(e.getMessage());
        }
    }

    /**
     * Gets the next token from a tokenizer and converts it to an IP Address.
     *
     * @param family The address family.
     * @return The next token in the stream, as an InetAddress
     * @throws TextParseException The input was invalid or not a valid address.
     * @throws IOException        An I/O error occurred.
     * @see Address
     */
    public InetAddress
    getAddress(int family) throws IOException {
        String next = _getIdentifier("an address");
        try {
            return Address.getByAddress(next, family);
        } catch (UnknownHostException e) {
            throw exception(e.getMessage());
        }
    }

    /**
     * Gets the next token from a tokenizer, which must be an EOL or EOF.
     *
     * @throws TextParseException The input was invalid or not an EOL or EOF token.
     * @throws IOException        An I/O error occurred.
     */
    public void
    getEOL() throws IOException {
        Token next = get();
        if (next.type != EOL && next.type != EOF) {
            throw exception("expected EOL or EOF");
        }
    }

    /**
     * Returns a concatenation of the remaining strings from a Tokenizer.
     */
    private String
    remainingStrings() throws IOException {
        StringBuffer buffer = null;
        while (true) {
            Tokenizer.Token t = get();
            if (!t.isString())
                break;
            if (buffer == null)
                buffer = new StringBuffer();
            buffer.append(t.value);
        }
        unget();
        if (buffer == null)
            return null;
        return buffer.toString();
    }

    /**
     * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
     * them together, and converts the base64 encoded data to a byte array.
     *
     * @param required If true, an exception will be thrown if no strings remain;
     *                 otherwise null be be returned.
     * @return The byte array containing the decoded strings, or null if there
     * were no strings to decode.
     * @throws TextParseException The input was invalid.
     * @throws IOException        An I/O error occurred.
     */
    public byte[]
    getBase64(boolean required) throws IOException {
        String s = remainingStrings();
        if (s == null) {
            if (required)
                throw exception("expected base64 encoded string");
            else
                return null;
        }
        byte[] array = base64.fromString(s);
        if (array == null)
            throw exception("invalid base64 encoding");
        return array;
    }

    /**
     * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
     * them together, and converts the base64 encoded data to a byte array.
     *
     * @return The byte array containing the decoded strings, or null if there
     * were no strings to decode.
     * @throws TextParseException The input was invalid.
     * @throws IOException        An I/O error occurred.
     */
    public byte[]
    getBase64() throws IOException {
        return getBase64(false);
    }

    /**
     * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
     * them together, and converts the hex encoded data to a byte array.
     *
     * @param required If true, an exception will be thrown if no strings remain;
     *                 otherwise null be be returned.
     * @return The byte array containing the decoded strings, or null if there
     * were no strings to decode.
     * @throws TextParseException The input was invalid.
     * @throws IOException        An I/O error occurred.
     */
    public byte[]
    getHex(boolean required) throws IOException {
        String s = remainingStrings();
        if (s == null) {
            if (required)
                throw exception("expected hex encoded string");
            else
                return null;
        }
        byte[] array = base16.fromString(s);
        if (array == null)
            throw exception("invalid hex encoding");
        return array;
    }

    /**
     * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
     * them together, and converts the hex encoded data to a byte array.
     *
     * @return The byte array containing the decoded strings, or null if there
     * were no strings to decode.
     * @throws TextParseException The input was invalid.
     * @throws IOException        An I/O error occurred.
     */
    public byte[]
    getHex() throws IOException {
        return getHex(false);
    }

    /**
     * Gets the next token from a tokenizer and decodes it as hex.
     *
     * @return The byte array containing the decoded string.
     * @throws TextParseException The input was invalid.
     * @throws IOException        An I/O error occurred.
     */
    public byte[]
    getHexString() throws IOException {
        String next = _getIdentifier("a hex string");
        byte[] array = base16.fromString(next);
        if (array == null)
            throw exception("invalid hex encoding");
        return array;
    }

    /**
     * Gets the next token from a tokenizer and decodes it as base32.
     *
     * @param b32 The base32 context to decode with.
     * @return The byte array containing the decoded string.
     * @throws TextParseException The input was invalid.
     * @throws IOException        An I/O error occurred.
     */
    public byte[]
    getBase32String(base32 b32) throws IOException {
        String next = _getIdentifier("a base32 string");
        byte[] array = b32.fromString(next);
        if (array == null)
            throw exception("invalid base32 encoding");
        return array;
    }

    /**
     * Creates an exception which includes the current state in the error message
     *
     * @param s The error message to include.
     * @return The exception to be thrown
     */
    public TextParseException
    exception(String s) {
        return new TokenizerException(filename, line, s);
    }

    /**
     * Closes any files opened by this tokenizer.
     */
    public void
    close() {
        if (wantClose) {
            try {
                is.close();
            } catch (IOException e) {
            }
        }
    }

    protected void
    finalize() {
        close();
    }

}
