// 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();
}

}