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

package org.xbill.DNS;

import java.io.*;
import java.text.*;
import java.util.*;
import org.xbill.DNS.utils.*;

/**
 * A generic DNS resource record.  The specific record types extend this class.
 * A record contains a name, type, class, ttl, and rdata.
 *
 * @author Brian Wellington
 */

public abstract class Record implements Cloneable, Comparable, Serializable {

private static final long serialVersionUID = 2694906050116005466L;

protected Name name;
protected int type, dclass;
protected long ttl;

private static final DecimalFormat byteFormat = new DecimalFormat();

static {
	byteFormat.setMinimumIntegerDigits(3);
}

protected
Record() {}

Record(Name name, int type, int dclass, long ttl) {
	if (!name.isAbsolute())
		throw new RelativeNameException(name);
	Type.check(type);
	DClass.check(dclass);
	TTL.check(ttl);
	this.name = name;
	this.type = type;
	this.dclass = dclass;
	this.ttl = ttl;
}

/**
 * Creates an empty record of the correct type; must be overriden
 */
abstract Record
getObject();

private static final Record
getEmptyRecord(Name name, int type, int dclass, long ttl, boolean hasData) {
	Record proto, rec;

	if (hasData) {
		proto = Type.getProto(type);
		if (proto != null)
			rec = proto.getObject();
		else
			rec = new UNKRecord();
	} else
		rec = new EmptyRecord();
	rec.name = name;
	rec.type = type;
	rec.dclass = dclass;
	rec.ttl = ttl;
	return rec;
}

/**
 * Converts the type-specific RR to wire format - must be overriden
 */
abstract void
rrFromWire(DNSInput in) throws IOException;

private static Record
newRecord(Name name, int type, int dclass, long ttl, int length, DNSInput in)
throws IOException
{
	Record rec;
	rec = getEmptyRecord(name, type, dclass, ttl, in != null);
	if (in != null) {
		if (in.remaining() < length)
			throw new WireParseException("truncated record");
		in.setActive(length);

		rec.rrFromWire(in);

		if (in.remaining() > 0)
			throw new WireParseException("invalid record length");
		in.clearActive();
	}
	return rec;
}

/**
 * Creates a new record, with the given parameters.
 * @param name The owner name of the record.
 * @param type The record's type.
 * @param dclass The record's class.
 * @param ttl The record's time to live.
 * @param length The length of the record's data.
 * @param data The rdata of the record, in uncompressed DNS wire format.  Only
 * the first length bytes are used.
 */
public static Record
newRecord(Name name, int type, int dclass, long ttl, int length, byte [] data) {
	if (!name.isAbsolute())
		throw new RelativeNameException(name);
	Type.check(type);
	DClass.check(dclass);
	TTL.check(ttl);

	DNSInput in;
	if (data != null)
		in = new DNSInput(data);
	else
		in = null;
	try {
		return newRecord(name, type, dclass, ttl, length, in);
	}
	catch (IOException e) {
		return null;
	}
}

/**
 * Creates a new record, with the given parameters.
 * @param name The owner name of the record.
 * @param type The record's type.
 * @param dclass The record's class.
 * @param ttl The record's time to live.
 * @param data The complete rdata of the record, in uncompressed DNS wire
 * format.
 */
public static Record
newRecord(Name name, int type, int dclass, long ttl, byte [] data) {
	return newRecord(name, type, dclass, ttl, data.length, data);
}

/**
 * Creates a new empty record, with the given parameters.
 * @param name The owner name of the record.
 * @param type The record's type.
 * @param dclass The record's class.
 * @param ttl The record's time to live.
 * @return An object of a subclass of Record
 */
public static Record
newRecord(Name name, int type, int dclass, long ttl) {
	if (!name.isAbsolute())
		throw new RelativeNameException(name);
	Type.check(type);
	DClass.check(dclass);
	TTL.check(ttl);

	return getEmptyRecord(name, type, dclass, ttl, false);
}

/**
 * Creates a new empty record, with the given parameters.  This method is
 * designed to create records that will be added to the QUERY section
 * of a message.
 * @param name The owner name of the record.
 * @param type The record's type.
 * @param dclass The record's class.
 * @return An object of a subclass of Record
 */
public static Record
newRecord(Name name, int type, int dclass) {
	return newRecord(name, type, dclass, 0);
}

static Record
fromWire(DNSInput in, int section, boolean isUpdate) throws IOException {
	int type, dclass;
	long ttl;
	int length;
	Name name;
	Record rec;

	name = new Name(in);
	type = in.readU16();
	dclass = in.readU16();

	if (section == Section.QUESTION)
		return newRecord(name, type, dclass);

	ttl = in.readU32();
	length = in.readU16();
	if (length == 0 && isUpdate &&
	    (section == Section.PREREQ || section == Section.UPDATE))
		return newRecord(name, type, dclass, ttl);
	rec = newRecord(name, type, dclass, ttl, length, in);
	return rec;
}

static Record
fromWire(DNSInput in, int section) throws IOException {
	return fromWire(in, section, false);
}

/**
 * Builds a Record from DNS uncompressed wire format.
 */
public static Record
fromWire(byte [] b, int section) throws IOException {
	return fromWire(new DNSInput(b), section, false);
}

void
toWire(DNSOutput out, int section, Compression c) {
	name.toWire(out, c);
	out.writeU16(type);
	out.writeU16(dclass);
	if (section == Section.QUESTION)
		return;
	out.writeU32(ttl);
	int lengthPosition = out.current();
	out.writeU16(0); /* until we know better */
	rrToWire(out, c, false);
	int rrlength = out.current() - lengthPosition - 2;
	out.writeU16At(rrlength, lengthPosition);
}

/**
 * Converts a Record into DNS uncompressed wire format.
 */
public byte []
toWire(int section) {
	DNSOutput out = new DNSOutput();
	toWire(out, section, null);
	return out.toByteArray();
}

private void
toWireCanonical(DNSOutput out, boolean noTTL) {
	name.toWireCanonical(out);
	out.writeU16(type);
	out.writeU16(dclass);
	if (noTTL) {
		out.writeU32(0);
	} else {
		out.writeU32(ttl);
	}
	int lengthPosition = out.current();
	out.writeU16(0); /* until we know better */
	rrToWire(out, null, true);
	int rrlength = out.current() - lengthPosition - 2;
	out.writeU16At(rrlength, lengthPosition);
}

/*
 * Converts a Record into canonical DNS uncompressed wire format (all names are
 * converted to lowercase), optionally ignoring the TTL.
 */
private byte []
toWireCanonical(boolean noTTL) {
	DNSOutput out = new DNSOutput();
	toWireCanonical(out, noTTL);
	return out.toByteArray();
}

/**
 * Converts a Record into canonical DNS uncompressed wire format (all names are
 * converted to lowercase).
 */
public byte []
toWireCanonical() {
	return toWireCanonical(false);
}

/**
 * Converts the rdata in a Record into canonical DNS uncompressed wire format
 * (all names are converted to lowercase).
 */
public byte []
rdataToWireCanonical() {
	DNSOutput out = new DNSOutput();
	rrToWire(out, null, true);
	return out.toByteArray();
}

/**
 * Converts the type-specific RR to text format - must be overriden
 */
abstract String rrToString();

/**
 * Converts the rdata portion of a Record into a String representation
 */
public String
rdataToString() {
	return rrToString();
}

/**
 * Converts a Record into a String representation
 */
public String
toString() {
	StringBuffer sb = new StringBuffer();
	sb.append(name);
	if (sb.length() < 8)
		sb.append("\t");
	if (sb.length() < 16)
		sb.append("\t");
	sb.append("\t");
	if (Options.check("BINDTTL"))
		sb.append(TTL.format(ttl));
	else
		sb.append(ttl);
	sb.append("\t");
	if (dclass != DClass.IN || !Options.check("noPrintIN")) {
		sb.append(DClass.string(dclass));
		sb.append("\t");
	}
	sb.append(Type.string(type));
	String rdata = rrToString();
	if (!rdata.equals("")) {
		sb.append("\t");
		sb.append(rdata);
	}
	return sb.toString();
}

/**
 * Converts the text format of an RR to the internal format - must be overriden
 */
abstract void
rdataFromString(Tokenizer st, Name origin) throws IOException;

/**
 * Converts a String into a byte array.
 */
protected static byte []
byteArrayFromString(String s) throws TextParseException {
	byte [] array = s.getBytes();
	boolean escaped = false;
	boolean hasEscapes = false;

	for (int i = 0; i < array.length; i++) {
		if (array[i] == '\\') {
			hasEscapes = true;
			break;
		}
	}
	if (!hasEscapes) {
		if (array.length > 255) {
			throw new TextParseException("text string too long");
		}
		return array;
	}

	ByteArrayOutputStream os = new ByteArrayOutputStream();

	int digits = 0;
	int intval = 0;
	for (int i = 0; i < array.length; i++) {
		byte b = array[i];
		if (escaped) {
			if (b >= '0' && b <= '9' && digits < 3) {
				digits++; 
				intval *= 10;
				intval += (b - '0');
				if (intval > 255)
					throw new TextParseException
								("bad escape");
				if (digits < 3)
					continue;
				b = (byte) intval;
			}
			else if (digits > 0 && digits < 3)
				throw new TextParseException("bad escape");
			os.write(b);
			escaped = false;
		}
		else if (array[i] == '\\') {
			escaped = true;
			digits = 0;
			intval = 0;
		}
		else
			os.write(array[i]);
	}
	if (digits > 0 && digits < 3)
		throw new TextParseException("bad escape");
	array = os.toByteArray();
	if (array.length > 255) {
		throw new TextParseException("text string too long");
	}

	return os.toByteArray();
}

/**
 * Converts a byte array into a String.
 */
protected static String
byteArrayToString(byte [] array, boolean quote) {
	StringBuffer sb = new StringBuffer();
	if (quote)
		sb.append('"');
	for (int i = 0; i < array.length; i++) {
		int b = array[i] & 0xFF;
		if (b < 0x20 || b >= 0x7f) {
			sb.append('\\');
			sb.append(byteFormat.format(b));
		} else if (b == '"' || b == '\\') {
			sb.append('\\');
			sb.append((char)b);
		} else
			sb.append((char)b);
	}
	if (quote)
		sb.append('"');
	return sb.toString();
}

/**
 * Converts a byte array into the unknown RR format.
 */
protected static String
unknownToString(byte [] data) {
	StringBuffer sb = new StringBuffer();
	sb.append("\\# ");
	sb.append(data.length);
	sb.append(" ");
	sb.append(base16.toString(data));
	return sb.toString();
}

/**
 * Builds a new Record from its textual representation
 * @param name The owner name of the record.
 * @param type The record's type.
 * @param dclass The record's class.
 * @param ttl The record's time to live.
 * @param st A tokenizer containing the textual representation of the rdata.
 * @param origin The default origin to be appended to relative domain names.
 * @return The new record
 * @throws IOException The text format was invalid.
 */
public static Record
fromString(Name name, int type, int dclass, long ttl, Tokenizer st, Name origin)
throws IOException
{
	Record rec;

	if (!name.isAbsolute())
		throw new RelativeNameException(name);
	Type.check(type);
	DClass.check(dclass);
	TTL.check(ttl);

	Tokenizer.Token t = st.get();
	if (t.type == Tokenizer.IDENTIFIER && t.value.equals("\\#")) {
		int length = st.getUInt16();
		byte [] data = st.getHex();
		if (data == null) {
			data = new byte[0];
		}
		if (length != data.length)
			throw st.exception("invalid unknown RR encoding: " +
					   "length mismatch");
		DNSInput in = new DNSInput(data);
		return newRecord(name, type, dclass, ttl, length, in);
	}
	st.unget();
	rec = getEmptyRecord(name, type, dclass, ttl, true);
	rec.rdataFromString(st, origin);
	t = st.get();
	if (t.type != Tokenizer.EOL && t.type != Tokenizer.EOF) {
		throw st.exception("unexpected tokens at end of record");
	}
	return rec;
}

/**
 * Builds a new Record from its textual representation
 * @param name The owner name of the record.
 * @param type The record's type.
 * @param dclass The record's class.
 * @param ttl The record's time to live.
 * @param s The textual representation of the rdata.
 * @param origin The default origin to be appended to relative domain names.
 * @return The new record
 * @throws IOException The text format was invalid.
 */
public static Record
fromString(Name name, int type, int dclass, long ttl, String s, Name origin)
throws IOException
{
	return fromString(name, type, dclass, ttl, new Tokenizer(s), origin);
}

/**
 * Returns the record's name
 * @see Name
 */
public Name
getName() {
	return name;
}

/**
 * Returns the record's type
 * @see Type
 */
public int
getType() {
	return type;
}

/**
 * Returns the type of RRset that this record would belong to.  For all types
 * except RRSIG, this is equivalent to getType().
 * @return The type of record, if not RRSIG.  If the type is RRSIG,
 * the type covered is returned.
 * @see Type
 * @see RRset
 * @see SIGRecord
 */
public int
getRRsetType() {
	if (type == Type.RRSIG) {
		RRSIGRecord sig = (RRSIGRecord) this;
		return sig.getTypeCovered();
	}
	return type;
}

/**
 * Returns the record's class
 */
public int
getDClass() {
	return dclass;
}

/**
 * Returns the record's TTL
 */
public long
getTTL() {
	return ttl;
}

/**
 * Converts the type-specific RR to wire format - must be overriden
 */
abstract void
rrToWire(DNSOutput out, Compression c, boolean canonical);

/**
 * Determines if two Records could be part of the same RRset.
 * This compares the name, type, and class of the Records; the ttl and
 * rdata are not compared.
 */
public boolean
sameRRset(Record rec) {
	return (getRRsetType() == rec.getRRsetType() &&
		dclass == rec.dclass &&
		name.equals(rec.name));
}

/**
 * Determines if two Records are identical.  This compares the name, type,
 * class, and rdata (with names canonicalized).  The TTLs are not compared.
 * @param arg The record to compare to
 * @return true if the records are equal, false otherwise.
 */
public boolean
equals(Object arg) {
	if (arg == null || !(arg instanceof Record))
		return false;
	Record r = (Record) arg;
	if (type != r.type || dclass != r.dclass || !name.equals(r.name))
		return false;
	byte [] array1 = rdataToWireCanonical();
	byte [] array2 = r.rdataToWireCanonical();
	return Arrays.equals(array1, array2);
}

/**
 * Generates a hash code based on the Record's data.
 */
public int
hashCode() {
	byte [] array = toWireCanonical(true);
	int code = 0;
	for (int i = 0; i < array.length; i++)
		code += ((code << 3) + (array[i] & 0xFF));
	return code;
}

Record
cloneRecord() {
	try {
		return (Record) clone();
	}
	catch (CloneNotSupportedException e) {
		throw new IllegalStateException();
	}
}

/**
 * Creates a new record identical to the current record, but with a different
 * name.  This is most useful for replacing the name of a wildcard record.
 */
public Record
withName(Name name) {
	if (!name.isAbsolute())
		throw new RelativeNameException(name);
	Record rec = cloneRecord();
	rec.name = name;
	return rec;
}

/**
 * Creates a new record identical to the current record, but with a different
 * class and ttl.  This is most useful for dynamic update.
 */
Record
withDClass(int dclass, long ttl) {
	Record rec = cloneRecord();
	rec.dclass = dclass;
	rec.ttl = ttl;
	return rec;
}

/* Sets the TTL to the specified value.  This is intentionally not public. */
void
setTTL(long ttl) {
	this.ttl = ttl;
}

/**
 * Compares this Record to another Object.
 * @param o The Object to be compared.
 * @return The value 0 if the argument is a record equivalent to this record;
 * a value less than 0 if the argument is less than this record in the
 * canonical ordering, and a value greater than 0 if the argument is greater
 * than this record in the canonical ordering.  The canonical ordering
 * is defined to compare by name, class, type, and rdata.
 * @throws ClassCastException if the argument is not a Record.
 */
public int
compareTo(Object o) {
	Record arg = (Record) o;

	if (this == arg)
		return (0);

	int n = name.compareTo(arg.name);
	if (n != 0)
		return (n);
	n = dclass - arg.dclass;
	if (n != 0)
		return (n);
	n = type - arg.type;
	if (n != 0)
		return (n);
	byte [] rdata1 = rdataToWireCanonical();
	byte [] rdata2 = arg.rdataToWireCanonical();
	for (int i = 0; i < rdata1.length && i < rdata2.length; i++) {
		n = (rdata1[i] & 0xFF) - (rdata2[i] & 0xFF);
		if (n != 0)
			return (n);
	}
	return (rdata1.length - rdata2.length);
}

/**
 * Returns the name for which additional data processing should be done
 * for this record.  This can be used both for building responses and
 * parsing responses.
 * @return The name to used for additional data processing, or null if this
 * record type does not require additional data processing.
 */
public Name
getAdditionalName() {
	return null;
}

/* Checks that an int contains an unsigned 8 bit value */
static int
checkU8(String field, int val) {
	if (val < 0 || val > 0xFF)
		throw new IllegalArgumentException("\"" + field + "\" " + val + 
						   " must be an unsigned 8 " +
						   "bit value");
	return val;
}

/* Checks that an int contains an unsigned 16 bit value */
static int
checkU16(String field, int val) {
	if (val < 0 || val > 0xFFFF)
		throw new IllegalArgumentException("\"" + field + "\" " + val + 
						   " must be an unsigned 16 " +
						   "bit value");
	return val;
}

/* Checks that a long contains an unsigned 32 bit value */
static long
checkU32(String field, long val) {
	if (val < 0 || val > 0xFFFFFFFFL)
		throw new IllegalArgumentException("\"" + field + "\" " + val + 
						   " must be an unsigned 32 " +
						   "bit value");
	return val;
}

/* Checks that a name is absolute */
static Name
checkName(String field, Name name) {
	if (!name.isAbsolute())
		throw new RelativeNameException(name);
	return name;
}

static byte []
checkByteArrayLength(String field, byte [] array, int maxLength) {
	if (array.length > 0xFFFF)
		throw new IllegalArgumentException("\"" + field + "\" array " +
						   "must have no more than " +
						   maxLength + " elements");
	byte [] out = new byte[array.length];
	System.arraycopy(array, 0, out, 0, array.length);
	return out;
}

}
