// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)

package org.xbill.DNS;

import java.io.*;
import java.util.*;

/**
 * A DNS master file parser.  This incrementally parses the file, returning
 * one record at a time.  When directives are seen, they are added to the
 * state and used when parsing future records.
 *
 * @author Brian Wellington
 */

public class Master {

    private Name origin;
    private File file;
    private Record last = null;
    private long defaultTTL;
    private Master included = null;
    private Tokenizer st;
    private int currentType;
    private int currentDClass;
    private long currentTTL;
    private boolean needSOATTL;

    private Generator generator;
    private List generators;
    private boolean noExpandGenerate;

    Master(File file, Name origin, long initialTTL) throws IOException {
        if (origin != null && !origin.isAbsolute()) {
            throw new RelativeNameException(origin);
        }
        this.file = file;
        st = new Tokenizer(file);
        this.origin = origin;
        defaultTTL = initialTTL;
    }

    /**
     * Initializes the master file reader and opens the specified master file.
     *
     * @param filename The master file.
     * @param origin   The initial origin to append to relative names.
     * @param ttl      The initial default TTL.
     * @throws IOException The master file could not be opened.
     */
    public Master(String filename, Name origin, long ttl) throws IOException {
        this(new File(filename), origin, ttl);
    }

    /**
     * Initializes the master file reader and opens the specified master file.
     *
     * @param filename The master file.
     * @param origin   The initial origin to append to relative names.
     * @throws IOException The master file could not be opened.
     */
    public Master(String filename, Name origin) throws IOException {
        this(new File(filename), origin, -1);
    }

    /**
     * Initializes the master file reader and opens the specified master file.
     *
     * @param filename The master file.
     * @throws IOException The master file could not be opened.
     */
    public Master(String filename) throws IOException {
        this(new File(filename), null, -1);
    }

    /**
     * Initializes the master file reader.
     *
     * @param in     The input stream containing a master file.
     * @param origin The initial origin to append to relative names.
     * @param ttl    The initial default TTL.
     */
    public Master(InputStream in, Name origin, long ttl) {
        if (origin != null && !origin.isAbsolute()) {
            throw new RelativeNameException(origin);
        }
        st = new Tokenizer(in);
        this.origin = origin;
        defaultTTL = ttl;
    }

    /**
     * Initializes the master file reader.
     *
     * @param in     The input stream containing a master file.
     * @param origin The initial origin to append to relative names.
     */
    public Master(InputStream in, Name origin) {
        this(in, origin, -1);
    }

    /**
     * Initializes the master file reader.
     *
     * @param in The input stream containing a master file.
     */
    public Master(InputStream in) {
        this(in, null, -1);
    }

    private Name
    parseName(String s, Name origin) throws TextParseException {
        try {
            return Name.fromString(s, origin);
        } catch (TextParseException e) {
            throw st.exception(e.getMessage());
        }
    }

    private void
    parseTTLClassAndType() throws IOException {
        String s;
        boolean seen_class = false;


        // This is a bit messy, since any of the following are legal:
        //   class ttl type
        //   ttl class type
        //   class type
        //   ttl type
        //   type
        seen_class = false;
        s = st.getString();
        if ((currentDClass = DClass.value(s)) >= 0) {
            s = st.getString();
            seen_class = true;
        }

        currentTTL = -1;
        try {
            currentTTL = TTL.parseTTL(s);
            s = st.getString();
        } catch (NumberFormatException e) {
            if (defaultTTL >= 0)
                currentTTL = defaultTTL;
            else if (last != null)
                currentTTL = last.getTTL();
        }

        if (!seen_class) {
            if ((currentDClass = DClass.value(s)) >= 0) {
                s = st.getString();
            } else {
                currentDClass = DClass.IN;
            }
        }

        if ((currentType = Type.value(s)) < 0)
            throw st.exception("Invalid type '" + s + "'");

        // BIND allows a missing TTL for the initial SOA record, and uses
        // the SOA minimum value.  If the SOA is not the first record,
        // this is an error.
        if (currentTTL < 0) {
            if (currentType != Type.SOA)
                throw st.exception("missing TTL");
            needSOATTL = true;
            currentTTL = 0;
        }
    }

    private long
    parseUInt32(String s) {
        if (!Character.isDigit(s.charAt(0)))
            return -1;
        try {
            long l = Long.parseLong(s);
            if (l < 0 || l > 0xFFFFFFFFL)
                return -1;
            return l;
        } catch (NumberFormatException e) {
            return -1;
        }
    }

    private void
    startGenerate() throws IOException {
        String s;
        int n;

        // The first field is of the form start-end[/step]
        // Regexes would be useful here.
        s = st.getIdentifier();
        n = s.indexOf("-");
        if (n < 0)
            throw st.exception("Invalid $GENERATE range specifier: " + s);
        String startstr = s.substring(0, n);
        String endstr = s.substring(n + 1);
        String stepstr = null;
        n = endstr.indexOf("/");
        if (n >= 0) {
            stepstr = endstr.substring(n + 1);
            endstr = endstr.substring(0, n);
        }
        long start = parseUInt32(startstr);
        long end = parseUInt32(endstr);
        long step;
        if (stepstr != null)
            step = parseUInt32(stepstr);
        else
            step = 1;
        if (start < 0 || end < 0 || start > end || step <= 0)
            throw st.exception("Invalid $GENERATE range specifier: " + s);

        // The next field is the name specification.
        String nameSpec = st.getIdentifier();

        // Then the ttl/class/type, in the same form as a normal record.
        // Only some types are supported.
        parseTTLClassAndType();
        if (!Generator.supportedType(currentType))
            throw st.exception("$GENERATE does not support " +
                    Type.string(currentType) + " records");

        // Next comes the rdata specification.
        String rdataSpec = st.getIdentifier();

        // That should be the end.  However, we don't want to move past the
        // line yet, so put back the EOL after reading it.
        st.getEOL();
        st.unget();

        generator = new Generator(start, end, step, nameSpec,
                currentType, currentDClass, currentTTL,
                rdataSpec, origin);
        if (generators == null)
            generators = new ArrayList(1);
        generators.add(generator);
    }

    private void
    endGenerate() throws IOException {
        // Read the EOL that we put back before.
        st.getEOL();

        generator = null;
    }

    private Record
    nextGenerated() throws IOException {
        try {
            return generator.nextRecord();
        } catch (Tokenizer.TokenizerException e) {
            throw st.exception("Parsing $GENERATE: " + e.getBaseMessage());
        } catch (TextParseException e) {
            throw st.exception("Parsing $GENERATE: " + e.getMessage());
        }
    }

    /**
     * Returns the next record in the master file.  This will process any
     * directives before the next record.
     *
     * @return The next record.
     * @throws IOException The master file could not be read, or was syntactically
     *                     invalid.
     */
    public Record
    _nextRecord() throws IOException {
        Tokenizer.Token token;
        String s;

        if (included != null) {
            Record rec = included.nextRecord();
            if (rec != null)
                return rec;
            included = null;
        }
        if (generator != null) {
            Record rec = nextGenerated();
            if (rec != null)
                return rec;
            endGenerate();
        }
        while (true) {
            Name name;

            token = st.get(true, false);
            if (token.type == Tokenizer.WHITESPACE) {
                Tokenizer.Token next = st.get();
                if (next.type == Tokenizer.EOL)
                    continue;
                else if (next.type == Tokenizer.EOF)
                    return null;
                else
                    st.unget();
                if (last == null)
                    throw st.exception("no owner");
                name = last.getName();
            } else if (token.type == Tokenizer.EOL)
                continue;
            else if (token.type == Tokenizer.EOF)
                return null;
            else if (((String) token.value).charAt(0) == '$') {
                s = token.value;

                if (s.equalsIgnoreCase("$ORIGIN")) {
                    origin = st.getName(Name.root);
                    st.getEOL();
                    continue;
                } else if (s.equalsIgnoreCase("$TTL")) {
                    defaultTTL = st.getTTL();
                    st.getEOL();
                    continue;
                } else if (s.equalsIgnoreCase("$INCLUDE")) {
                    String filename = st.getString();
                    File newfile;
                    if (file != null) {
                        String parent = file.getParent();
                        newfile = new File(parent, filename);
                    } else {
                        newfile = new File(filename);
                    }
                    Name incorigin = origin;
                    token = st.get();
                    if (token.isString()) {
                        incorigin = parseName(token.value,
                                Name.root);
                        st.getEOL();
                    }
                    included = new Master(newfile, incorigin,
                            defaultTTL);
                /*
				 * If we continued, we wouldn't be looking in
				 * the new file.  Recursing works better.
				 */
                    return nextRecord();
                } else if (s.equalsIgnoreCase("$GENERATE")) {
                    if (generator != null)
                        throw new IllegalStateException
                                ("cannot nest $GENERATE");
                    startGenerate();
                    if (noExpandGenerate) {
                        endGenerate();
                        continue;
                    }
                    return nextGenerated();
                } else {
                    throw st.exception("Invalid directive: " + s);
                }
            } else {
                s = token.value;
                name = parseName(s, origin);
                if (last != null && name.equals(last.getName())) {
                    name = last.getName();
                }
            }

            parseTTLClassAndType();
            last = Record.fromString(name, currentType, currentDClass,
                    currentTTL, st, origin);
            if (needSOATTL) {
                long ttl = ((SOARecord) last).getMinimum();
                last.setTTL(ttl);
                defaultTTL = ttl;
                needSOATTL = false;
            }
            return last;
        }
    }

    /**
     * Returns the next record in the master file.  This will process any
     * directives before the next record.
     *
     * @return The next record.
     * @throws IOException The master file could not be read, or was syntactically
     *                     invalid.
     */
    public Record
    nextRecord() throws IOException {
        Record rec = null;
        try {
            rec = _nextRecord();
        } finally {
            if (rec == null) {
                st.close();
            }
        }
        return rec;
    }

    /**
     * Specifies whether $GENERATE statements should be expanded.  Whether
     * expanded or not, the specifications for generated records are available
     * by calling {@link #generators}.  This must be called before a $GENERATE
     * statement is seen during iteration to have an effect.
     */
    public void
    expandGenerate(boolean wantExpand) {
        noExpandGenerate = !wantExpand;
    }

    /**
     * Returns an iterator over the generators specified in the master file; that
     * is, the parsed contents of $GENERATE statements.
     *
     * @see Generator
     */
    public Iterator
    generators() {
        if (generators != null)
            return Collections.unmodifiableList(generators).iterator();
        else
            return Collections.EMPTY_LIST.iterator();
    }

    protected void
    finalize() {
        st.close();
    }

}
