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

package org.xbill.DNS;

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

import org.xbill.DNS.utils.*;

/**
 * APL - Address Prefix List.  See RFC 3123.
 *
 * @author Brian Wellington
 */

/*
 * Note: this currently uses the same constants as the Address class;
 * this could change if more constants are defined for APL records.
 */

public class APLRecord extends Record {

    public static class Element {
        public final int family;
        public final boolean negative;
        public final int prefixLength;
        public final Object address;

        private Element(int family, boolean negative, Object address, int prefixLength) {
            this.family = family;
            this.negative = negative;
            this.address = address;
            this.prefixLength = prefixLength;
            if (!validatePrefixLength(family, prefixLength)) {
                throw new IllegalArgumentException("invalid prefix " +
                        "length");
            }
        }

        /**
         * Creates an APL element corresponding to an IPv4 or IPv6 prefix.
         *
         * @param negative     Indicates if this prefix is a negation.
         * @param address      The IPv4 or IPv6 address.
         * @param prefixLength The length of this prefix, in bits.
         * @throws IllegalArgumentException The prefix length is invalid.
         */
        public Element(boolean negative, InetAddress address, int prefixLength) {
            this(Address.familyOf(address), negative, address,
                    prefixLength);
        }

        public String
        toString() {
            StringBuffer sb = new StringBuffer();
            if (negative)
                sb.append("!");
            sb.append(family);
            sb.append(":");
            if (family == Address.IPv4 || family == Address.IPv6)
                sb.append(((InetAddress) address).getHostAddress());
            else
                sb.append(base16.toString((byte[]) address));
            sb.append("/");
            sb.append(prefixLength);
            return sb.toString();
        }

        public boolean
        equals(Object arg) {
            if (arg == null || !(arg instanceof Element))
                return false;
            Element elt = (Element) arg;
            return (family == elt.family &&
                    negative == elt.negative &&
                    prefixLength == elt.prefixLength &&
                    address.equals(elt.address));
        }

        public int
        hashCode() {
            return address.hashCode() + prefixLength + (negative ? 1 : 0);
        }
    }

    private static final long serialVersionUID = -1348173791712935864L;

    private List elements;

    APLRecord() {
    }

    Record
    getObject() {
        return new APLRecord();
    }

    private static boolean
    validatePrefixLength(int family, int prefixLength) {
        if (prefixLength < 0 || prefixLength >= 256)
            return false;
        if ((family == Address.IPv4 && prefixLength > 32) ||
                (family == Address.IPv6 && prefixLength > 128))
            return false;
        return true;
    }

    /**
     * Creates an APL Record from the given data.
     *
     * @param elements The list of APL elements.
     */
    public APLRecord(Name name, int dclass, long ttl, List elements) {
        super(name, Type.APL, dclass, ttl);
        this.elements = new ArrayList(elements.size());
        for (Iterator it = elements.iterator(); it.hasNext(); ) {
            Object o = it.next();
            if (!(o instanceof Element)) {
                throw new IllegalArgumentException("illegal element");
            }
            Element element = (Element) o;
            if (element.family != Address.IPv4 &&
                    element.family != Address.IPv6) {
                throw new IllegalArgumentException("unknown family");
            }
            this.elements.add(element);

        }
    }

    private static byte[]
    parseAddress(byte[] in, int length) throws WireParseException {
        if (in.length > length)
            throw new WireParseException("invalid address length");
        if (in.length == length)
            return in;
        byte[] out = new byte[length];
        System.arraycopy(in, 0, out, 0, in.length);
        return out;
    }

    void
    rrFromWire(DNSInput in) throws IOException {
        elements = new ArrayList(1);
        while (in.remaining() != 0) {
            int family = in.readU16();
            int prefix = in.readU8();
            int length = in.readU8();
            boolean negative = (length & 0x80) != 0;
            length &= ~0x80;

            byte[] data = in.readByteArray(length);
            Element element;
            if (!validatePrefixLength(family, prefix)) {
                throw new WireParseException("invalid prefix length");
            }

            if (family == Address.IPv4 || family == Address.IPv6) {
                data = parseAddress(data,
                        Address.addressLength(family));
                InetAddress addr = InetAddress.getByAddress(data);
                element = new Element(negative, addr, prefix);
            } else {
                element = new Element(family, negative, data, prefix);
            }
            elements.add(element);

        }
    }

    void
    rdataFromString(Tokenizer st, Name origin) throws IOException {
        elements = new ArrayList(1);
        while (true) {
            Tokenizer.Token t = st.get();
            if (!t.isString())
                break;

            boolean negative = false;
            int family = 0;
            int prefix = 0;

            String s = t.value;
            int start = 0;
            if (s.startsWith("!")) {
                negative = true;
                start = 1;
            }
            int colon = s.indexOf(':', start);
            if (colon < 0)
                throw st.exception("invalid address prefix element");
            int slash = s.indexOf('/', colon);
            if (slash < 0)
                throw st.exception("invalid address prefix element");

            String familyString = s.substring(start, colon);
            String addressString = s.substring(colon + 1, slash);
            String prefixString = s.substring(slash + 1);

            try {
                family = Integer.parseInt(familyString);
            } catch (NumberFormatException e) {
                throw st.exception("invalid family");
            }
            if (family != Address.IPv4 && family != Address.IPv6)
                throw st.exception("unknown family");

            try {
                prefix = Integer.parseInt(prefixString);
            } catch (NumberFormatException e) {
                throw st.exception("invalid prefix length");
            }

            if (!validatePrefixLength(family, prefix)) {
                throw st.exception("invalid prefix length");
            }

            byte[] bytes = Address.toByteArray(addressString, family);
            if (bytes == null)
                throw st.exception("invalid IP address " +
                        addressString);

            InetAddress address = InetAddress.getByAddress(bytes);
            elements.add(new Element(negative, address, prefix));
        }
        st.unget();
    }

    String
    rrToString() {
        StringBuffer sb = new StringBuffer();
        for (Iterator it = elements.iterator(); it.hasNext(); ) {
            Element element = (Element) it.next();
            sb.append(element);
            if (it.hasNext())
                sb.append(" ");
        }
        return sb.toString();
    }

    /**
     * Returns the list of APL elements.
     */
    public List
    getElements() {
        return elements;
    }

    private static int
    addressLength(byte[] addr) {
        for (int i = addr.length - 1; i >= 0; i--) {
            if (addr[i] != 0)
                return i + 1;
        }
        return 0;
    }

    void
    rrToWire(DNSOutput out, Compression c, boolean canonical) {
        for (Iterator it = elements.iterator(); it.hasNext(); ) {
            Element element = (Element) it.next();
            int length = 0;
            byte[] data;
            if (element.family == Address.IPv4 ||
                    element.family == Address.IPv6) {
                InetAddress addr = (InetAddress) element.address;
                data = addr.getAddress();
                length = addressLength(data);
            } else {
                data = (byte[]) element.address;
                length = data.length;
            }
            int wlength = length;
            if (element.negative) {
                wlength |= 0x80;
            }
            out.writeU16(element.family);
            out.writeU8(element.prefixLength);
            out.writeU8(wlength);
            out.writeByteArray(data, 0, length);
        }
    }

}
