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

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

/**
 * DNS extension options, as described in RFC 2671.  The rdata of an OPT record
 * is defined as a list of options; this represents a single option.
 *
 * @author Brian Wellington
 * @author Ming Zhou &lt;mizhou@bnivideo.com&gt;, Beaumaris Networks
 */
public abstract class EDNSOption {

    public static class Code {
        private Code() {
        }

        /**
         * Name Server Identifier, RFC 5001
         */
        public final static int NSID = 3;

        /**
         * Client Subnet, defined in draft-vandergaast-edns-client-subnet-00
         */
        public final static int CLIENT_SUBNET = 20730;

        private static Mnemonic codes = new Mnemonic("EDNS Option Codes",
                Mnemonic.CASE_UPPER);

        static {
            codes.setMaximum(0xFFFF);
            codes.setPrefix("CODE");
            codes.setNumericAllowed(true);

            codes.add(NSID, "NSID");
            codes.add(CLIENT_SUBNET, "CLIENT_SUBNET");
        }

        /**
         * Converts an EDNS Option Code into its textual representation
         */
        public static String
        string(int code) {
            return codes.getText(code);
        }

        /**
         * Converts a textual representation of an EDNS Option Code into its
         * numeric value.
         *
         * @param s The textual representation of the option code
         * @return The option code, or -1 on error.
         */
        public static int
        value(String s) {
            return codes.getValue(s);
        }
    }

    private final int code;

    /**
     * Creates an option with the given option code and data.
     */
    public EDNSOption(int code) {
        this.code = Record.checkU16("code", code);
    }

    public String
    toString() {
        StringBuffer sb = new StringBuffer();

        sb.append("{");
        sb.append(EDNSOption.Code.string(code));
        sb.append(": ");
        sb.append(optionToString());
        sb.append("}");

        return sb.toString();
    }

    /**
     * Returns the EDNS Option's code.
     *
     * @return the option code
     */
    public int
    getCode() {
        return code;
    }

    /**
     * Returns the EDNS Option's data, as a byte array.
     *
     * @return the option data
     */
    byte[]
    getData() {
        DNSOutput out = new DNSOutput();
        optionToWire(out);
        return out.toByteArray();
    }

    /**
     * Converts the wire format of an EDNS Option (the option data only) into the
     * type-specific format.
     *
     * @param in The input Stream.
     */
    abstract void
    optionFromWire(DNSInput in) throws IOException;

    /**
     * Converts the wire format of an EDNS Option (including code and length) into
     * the type-specific format.
     *
     * @param out The input stream.
     */
    static EDNSOption
    fromWire(DNSInput in) throws IOException {
        int code, length;

        code = in.readU16();
        length = in.readU16();
        if (in.remaining() < length)
            throw new WireParseException("truncated option");
        int save = in.saveActive();
        in.setActive(length);
        EDNSOption option;
        switch (code) {
            case Code.NSID:
                option = new NSIDOption();
                break;
            case Code.CLIENT_SUBNET:
                option = new ClientSubnetOption();
                break;
            default:
                option = new GenericEDNSOption(code);
                break;
        }
        option.optionFromWire(in);
        in.restoreActive(save);

        return option;
    }

    /**
     * Converts the wire format of an EDNS Option (including code and length) into
     * the type-specific format.
     *
     * @return The option, in wire format.
     */
    public static EDNSOption
    fromWire(byte[] b) throws IOException {
        return fromWire(new DNSInput(b));
    }

    /**
     * Converts an EDNS Option (the type-specific option data only) into wire format.
     *
     * @param out The output stream.
     */
    abstract void
    optionToWire(DNSOutput out);

    /**
     * Converts an EDNS Option (including code and length) into wire format.
     *
     * @param out The output stream.
     */
    void
    toWire(DNSOutput out) {
        out.writeU16(code);
        int lengthPosition = out.current();
        out.writeU16(0); /* until we know better */
        optionToWire(out);
        int length = out.current() - lengthPosition - 2;
        out.writeU16At(length, lengthPosition);
    }

    /**
     * Converts an EDNS Option (including code and length) into wire format.
     *
     * @return The option, in wire format.
     */
    public byte[]
    toWire() throws IOException {
        DNSOutput out = new DNSOutput();
        toWire(out);
        return out.toByteArray();
    }

    /**
     * Determines if two EDNS Options are identical.
     *
     * @param arg The option to compare to
     * @return true if the options are equal, false otherwise.
     */
    public boolean
    equals(Object arg) {
        if (arg == null || !(arg instanceof EDNSOption))
            return false;
        EDNSOption opt = (EDNSOption) arg;
        if (code != opt.code)
            return false;
        return Arrays.equals(getData(), opt.getData());
    }

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

    abstract String optionToString();

}
