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

}
