/*
 ** This file is part of Filius, a network construction and simulation software.
 ** 
 ** Originally created at the University of Siegen, Institute "Didactics of
 ** Informatics and E-Learning" by a students' project group:
 **     members (2006-2007): 
 **         André Asschoff, Johannes Bade, Carsten Dittich, Thomas Gerding,
 **         Nadja Haßler, Ernst Johannes Klebert, Michell Weyer
 **     supervisors:
 **         Stefan Freischlad (maintainer until 2009), Peer Stechert
 ** Project is maintained since 2010 by Christian Eibl <filius@c.fameibl.de>
 **         and Stefan Freischlad
 ** Filius is free software: you can redistribute it and/or modify
 ** it under the terms of the GNU General Public License as published by
 ** the Free Software Foundation, either version 2 of the License, or
 ** (at your option) version 3.
 ** 
 ** Filius is distributed in the hope that it will be useful,
 ** but WITHOUT ANY WARRANTY; without even the implied
 ** warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 ** PURPOSE. See the GNU General Public License for more details.
 ** 
 ** You should have received a copy of the GNU General Public License
 ** along with Filius.  If not, see <http://www.gnu.org/licenses/>.
 */
package filius.software.dns;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.StringTokenizer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Das Format einer DNS-Nachricht wird in RFC 1035 beschrieben. <br />
 * The header contains the following fields: <br />
 * <ol>
 * <li>ID</li>
 * <li>QR</li>
 * <li>OPCODE</li>
 * <li>AA</li>
 * <li>TC</li>
 * <li>RD</li>
 * <li>RA</li>
 * <li>Z</li>
 * <li>RCODE</li>
 * <li>QDCOUNT</li>
 * <li>ANCOUNT</li>
 * <li>NSCOUNT</li>
 * <li>ARCOUNT</li>
 * </ol>
 * 
 * <b> Resource record format </b> <br />
 * The answer, authority, and additional sections all share the same format
 */
public class DNSNachricht {
    private static Logger LOG = LoggerFactory.getLogger(DNSNachricht.class);

    /**
     * Konstanten fuer das Bit im Kopfteil (Header), das indiziert, ob es sich um eine Anfrage (QUERY) oder eine Antwort
     * (RESPONSE) handelt.
     */
    public static final int QUERY = 0, RESPONSE = 1;

    /**
     * Konstanten fuer den Response-Code: The values have the following interpretation:
     * <ul>
     * <li>0 - No error condition</li>
     * <li>1 - Format error - The name server was unable to interpret the query.</li>
     * <li>2 - Server failure - The name server was unable to process this query due to a problem with the name
     * server.</li>
     * <li>3 - Name Error - Meaningful only for responses from an authoritative name server, this code signifies that
     * the domain name referenced in the query does not exist.</li>
     * <li>4 - Not Implemented - The name server does not support the requested kind of query.</li>
     * <li>5 - Refused - The name server refuses to perform the specified operation for policy reasons. For example, a
     * name server may not wish to provide the information to the particular requester, or a name server may not wish to
     * perform a particular operation (e.g., zone transfer) for particular data.</li>
     * </ul>
     */
    public static final int NO_ERROR = 0, FORMAT_ERROR = 1, SERVER_FAILURE = 2, NAME_ERROR = 3, NOT_IMPLEMENTED = 4,
            REFUSED = 5;

    /**
     * ID A 16 bit identifier assigned by the program that generates any kind of query. This identifier is copied the
     * corresponding reply and can be used by the requester to match up replies to outstanding queries.
     */
    private int id = (int) (Math.random() * 65536);

    /** Whether this query is from the local resolver (and not from potentially remote server) */
    private boolean isLocal = false;

    /**
     * QR A one bit field that specifies whether this message is a query (0), or a response (1).
     */
    private int queryResponse = QUERY;

    /**
     * OPCODE A four bit field that specifies kind of query in this message. This value is set by the originator of a
     * query and copied into the response. The values are:
     * <ul>
     * <li>0 - a standard query (QUERY)</li>
     * <li>1 - an inverse query (IQUERY)</li>
     * <li>2 - a server status request (STATUS)</li>
     * </ul>
     */
    private int opcode = 0;

    /**
     * AA Authoritative Answer - this bit is valid in responses, and specifies that the responding name server is an
     * authority for the domain name in question section. Note that the contents of the answer section may have multiple
     * owner names because of aliases. The AA bit corresponds to the name which matches the query name, or the first
     * owner name in the answer section.
     */
    private boolean authoritativeAnswer = false;

    /**
     * TC TrunCation - specifies that this message was truncated due to length greater than that permitted on the
     * transmission channel.
     */
    private boolean truncated = false;

    /**
     * RD Recursion Desired - this bit may be set in a query and is copied into the response. If RD is set, it directs
     * the name server to pursue the query recursively. Recursive query support is optional.
     */
    private boolean recursionDesired = true;

    /**
     * RA Recursion Available - this be is set or cleared in a response, and denotes whether recursive query support is
     * available in the name server.
     */
    private boolean recursionAvailable = true;

    /**
     * RCODE Response code - this 4 bit field is set as part of responses. <br />
     * Dazu werden in dieser Klasse Konstanten zur Verfuegung gestellt.
     */
    private int responseCode = NO_ERROR;

    /**
     * QDCOUNT an unsigned 16 bit integer specifying the number of entries in the question section.
     */
    private int queryCount = 0;

    /**
     * ANCOUNT an unsigned 16 bit integer specifying the number of resource records in the answer section.
     */
    private int answerCount = 0;

    /**
     * NSCOUNT an unsigned 16 bit integer specifying the number of name server resource records in the authority records
     * section.
     */
    private int authoritativeAnswerCount = 0;

    /** ARCOUNT Anzahl der Resource Records in der 'additional section' */
    private int additionalCount = 0;

    /**
     * Anfragen (query) in der Nachricht
     */
    private LinkedList<Query> queries = new LinkedList<Query>();

    /** Resource Records in der 'answer section' */
    private LinkedList<ResourceRecord> answerRecords = new LinkedList<ResourceRecord>();

    /** Resource Records in der 'authority section' */
    private LinkedList<ResourceRecord> authoratativeRecords = new LinkedList<ResourceRecord>();

    /** Resource Records in der 'additional section' */
    private LinkedList<ResourceRecord> additionalRecords = new LinkedList<ResourceRecord>();

    /**
     * Konstruktor zur Erzeugung einer DNS-Nachricht mit Standardwerten. Lediglich ob es sich um eine Anfrage oder eine
     * Antwort handelt, muss als Parameter uebergeben werden. Dazu werden die Konstanten QUERY und RESPONSE verwendet.
     */
    public DNSNachricht(int queryResponse) {
        LOG.trace("INVOKED (" + this.hashCode() + ") " + getClass() + ", DNSNachricht(" + queryResponse + ")");
        this.queryResponse = queryResponse;
    }

    /**
     * Ein Konstruktor, der aus einem String, der durch den Aufruf der Methode toString() erzeugt wurde, wieder eine
     * DNSNachricht erstellt.
     * 
     * @param nachricht
     *            ein String, der durch die Methode toString() erstellt wurde
     */
    public DNSNachricht(String nachricht) {
        LOG.trace("INVOKED (" + this.hashCode() + ", T" + this.getId() + ") " + getClass()
                + " (DNSNachricht), constr: DNSNachricht(" + nachricht + ")");
        StringTokenizer lineTokenizer, tokenizer;
        String line, token;

        if (nachricht != null) {
            lineTokenizer = new StringTokenizer(nachricht, "\n");
            line = lineTokenizer.nextToken();

            tokenizer = new StringTokenizer(line, " ");
            while (tokenizer.hasMoreTokens()) {
                token = tokenizer.nextToken().trim();

                if (token.startsWith("ID")) {
                    id = Integer.parseInt(token.substring(3));
                } else if (token.startsWith("QR")) {
                    queryResponse = Integer.parseInt(token.substring(3));
                } else if (token.startsWith("RCODE")) {
                    responseCode = Integer.parseInt(token.substring(6));
                } else if (token.startsWith("QDCOUNT")) {
                    queryCount = Integer.parseInt(token.substring(8));
                } else if (token.startsWith("ANCOUNT")) {
                    answerCount = Integer.parseInt(token.substring(8));
                } else if (token.startsWith("NSCOUNT")) {
                    authoritativeAnswerCount = Integer.parseInt(token.substring(8));
                } else if (token.startsWith("ARCOUNT")) {
                    additionalCount = Integer.parseInt(token.substring(8));
                }
            }

            for (int i = 0; i < queryCount && lineTokenizer.hasMoreTokens(); i++) {
                line = lineTokenizer.nextToken();
                queries.add(new Query(line));
            }
            for (int i = 0; i < answerCount && lineTokenizer.hasMoreTokens(); i++) {
                line = lineTokenizer.nextToken();
                answerRecords.add(new ResourceRecord(line));
            }
            for (int i = 0; i < authoritativeAnswerCount && lineTokenizer.hasMoreTokens(); i++) {
                line = lineTokenizer.nextToken();
                authoratativeRecords.add(new ResourceRecord(line));
            }
            for (int i = 0; i < additionalCount && lineTokenizer.hasMoreTokens(); i++) {
                line = lineTokenizer.nextToken();
                additionalRecords.add(new ResourceRecord(line));
            }
        }

    }

    /**
     * Diese Methode liefert die Nachricht mit ausgewaehlten Attributen des Kopfteils in der Form
     * &lt;Attribut&gt;=&lt;Wert&gt; (z. B. &quot;ID=42&quot;), die durch Leerzeichen getrennt sind, und den Datenteil,
     * der durch einen Zeilenumbruch vom Kopfteil abgetrennt ist.
     */
    public String toString() {
        StringBuffer buffer = new StringBuffer();
        ListIterator<?> it;

        buffer.append("ID=" + id + " ");
        buffer.append("QR=" + queryResponse + " ");
        buffer.append("RCODE=" + responseCode + " ");
        buffer.append("QDCOUNT=" + queryCount + " ");
        buffer.append("ANCOUNT=" + answerCount + " ");
        buffer.append("NSCOUNT=" + authoritativeAnswerCount + " ");
        buffer.append("ARCOUNT=" + additionalCount + " ");
        buffer.append("\n");

        it = queries.listIterator();
        while (it.hasNext()) {
            buffer.append(it.next().toString() + "\n");
        }

        it = answerRecords.listIterator();
        while (it.hasNext()) {
            buffer.append(it.next().toString() + "\n");
        }

        it = authoratativeRecords.listIterator();
        while (it.hasNext()) {
            buffer.append(it.next().toString() + "\n");
        }

        it = additionalRecords.listIterator();
        while (it.hasNext()) {
            buffer.append(it.next().toString() + "\n");
        }

        return buffer.toString();
    }

    /**
     * Methode zum hinzufuegen einer Anfrage (query). Das Format muss folgendermassen sein: NAME TYPE CLASS (Bsp.:
     * web.de. A IN)
     */
    public void hinzuQuery(String anfrage) {
        LOG.trace("INVOKED (" + this.hashCode() + ", T" + this.getId() + ") " + getClass()
                + " (DNSNachricht), hinzuQuery(" + anfrage + ")");
        queries.add(new Query(anfrage));
        queryCount++;
    }

    /** Methode fuer den Zugriff auf die Anfragen (queries) */
    public LinkedList<Query> holeQueries() {
        return queries;
    }

    /**
     * Methode fuer den Zugriff auf die Resource-Records in der 'answer section'.
     */
    public LinkedList<ResourceRecord> holeAntwortResourceRecords() {
        return answerRecords;
    }

    /**
     * Methode fuer den Zugriff auf die Resource-Records in der 'authority section'.
     */
    public LinkedList<ResourceRecord> holeAuthoritativeResourceRecords() {
        return authoratativeRecords;
    }

    /**
     * Methode fuer den Zugriff auf die Resource-Records in der 'additional section'.
     */
    public LinkedList<ResourceRecord> holeZusatzResourceRecords() {
        return additionalRecords;
    }

    /** retrieve all resource records no matter what section they are in */
    public List<ResourceRecord> holeResourceRecords() {
        List<ResourceRecord> list = new ArrayList<>();
        list.addAll(answerRecords);
        list.addAll(authoratativeRecords);
        list.addAll(additionalRecords);
        return list;
    }

    /**
     * Diese Methode fuegt der DNS-Nachricht einen Resource Record hinzu. Das Format muss folgendermassen aussehen: NAME
     * TYPE CLASS TTL RDATA (Bsp. web.de. A IN 3600 217.72.195.42)
     */
    public void hinzuAntwortResourceRecord(String record) {
        hinzuAntwortResourceRecord(new ResourceRecord(record));
    }

    public void hinzuAntwortResourceRecords(List<ResourceRecord> rrList) {
        answerRecords.addAll(rrList);
        answerCount += rrList.size();
    }

    public void hinzuAntwortResourceRecord(ResourceRecord rr) {
        answerRecords.add(rr);
        answerCount++;
    }

    /**
     * Diese Methode fuegt der DNS-Nachricht einen Resource Record hinzu. Das Format muss folgendermassen aussehen: NAME
     * TYPE CLASS TTL RDATA (Bsp. web.de. A IN 3600 217.72.195.42)
     */
    public void hinzuAuthoritativeResourceRecord(String record) {
        authoratativeRecords.add(new ResourceRecord(record));
        authoritativeAnswerCount++;
    }

    public void hinzuAuthoritativeResourceRecords(List<ResourceRecord> rrList) {
        authoratativeRecords.addAll(rrList);
        authoritativeAnswerCount += rrList.size();
    }

    /**
     * @return the authoritativeAnswer
     */
    public boolean isAuthoritativeAnswer() {
        return authoritativeAnswer;
    }

    /**
     * @param authoritativeAnswer
     *            the authoritativeAnswer to set
     */
    public void setAuthoritativeAnswer(boolean authoritativeAnswer) {
        this.authoritativeAnswer = authoritativeAnswer;
    }

    /**
     * @return the id
     */
    public int getId() {
        return id;
    }

    /**
     * @param id
     *            the id to set
     */
    public void setId(int id) {
        this.id = id;
    }

    /**
     * @return the opcode
     */
    public int getOpcode() {
        return opcode;
    }

    /**
     * @param opcode
     *            the opcode to set
     */
    public void setOpcode(int opcode) {
        this.opcode = opcode;
    }

    /**
     * @return the queryResponse
     */
    public int getQueryResponse() {
        return queryResponse;
    }

    /**
     * @param queryResponse
     *            the queryResponse to set
     */
    public void setQueryResponse(int queryResponse) {
        this.queryResponse = queryResponse;
    }

    /**
     * @return the recursionAvailable
     */
    public boolean isRecursionAvailable() {
        return recursionAvailable;
    }

    /**
     * @param recursionAvailable
     *            the recursionAvailable to set
     */
    public void setRecursionAvailable(boolean recursionAvailable) {
        this.recursionAvailable = recursionAvailable;
    }

    /**
     * @return the recursionDesired
     */
    public boolean isRecursionDesired() {
        return recursionDesired;
    }

    /**
     * @param recursionDesired
     *            the recursionDesired to set
     */
    public void setRecursionDesired(boolean recursionDesired) {
        this.recursionDesired = recursionDesired;
    }

    /**
     * @return the responseCode
     */
    public int getResponseCode() {
        return responseCode;
    }

    /**
     * @param responseCode
     *            the responseCode to set
     */
    public void setResponseCode(int responseCode) {
        this.responseCode = responseCode;
    }

    /**
     * @return the truncated
     */
    public boolean isTruncated() {
        return truncated;
    }

    /**
     * @param truncated
     *            the truncated to set
     */
    public void setTruncated(boolean truncated) {
        this.truncated = truncated;
    }

    public boolean isLocal() {
        return isLocal;
    }

    public void setLocal() {
        this.isLocal = true;
    }
}
