/**
 * Copyright 2003-2005 Arthur van Hoff, Rick Blair
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.jmdns;

import java.io.IOException;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * An outgoing DNS message.
 *
 * @version %I%, %G%
 * @author	Arthur van Hoff, Rick Blair, Werner Randelshofer
 */
final class DNSOutgoing
{
    private static Logger logger = Logger.getLogger(DNSOutgoing.class.toString());
    int id;
    int flags;
    private boolean multicast;
    private int numQuestions;
    private int numAnswers;
    private int numAuthorities;
    private int numAdditionals;
    private Hashtable names;

    byte data[];
    int off;
    int len;

    /**
     * Create an outgoing multicast query or response.
     */
    DNSOutgoing(int flags)
    {
        this(flags, true);
    }

    /**
     * Create an outgoing query or response.
     */
    DNSOutgoing(int flags, boolean multicast)
    {
        this.flags = flags;
        this.multicast = multicast;
        names = new Hashtable();
        data = new byte[DNSConstants.MAX_MSG_TYPICAL];
        off = 12;
    }

    /**
     * Add a question to the message.
     */
    void addQuestion(DNSQuestion rec) throws IOException
    {
        if (numAnswers > 0 || numAuthorities > 0 || numAdditionals > 0)
        {
            throw new IllegalStateException("Questions must be added before answers");
        }
        numQuestions++;
        writeQuestion(rec);
    }

    /**
     * Add an answer if it is not suppressed.
     */
    void addAnswer(DNSIncoming in, DNSRecord rec) throws IOException
    {
        if (numAuthorities > 0 || numAdditionals > 0)
        {
            throw new IllegalStateException("Answers must be added before authorities and additionals");
        }
        if (!rec.suppressedBy(in))
        {
            addAnswer(rec, 0);
        }
    }

    /**
     * Add an additional answer to the record. Omit if there is no room.
     */
    void addAdditionalAnswer(DNSIncoming in, DNSRecord rec) throws IOException
    {
        if ((off < DNSConstants.MAX_MSG_TYPICAL - 200) && !rec.suppressedBy(in))
        {
            writeRecord(rec, 0);
            numAdditionals++;
        }
    }

    /**
     * Add an answer to the message.
     */
    void addAnswer(DNSRecord rec, long now) throws IOException
    {
        if (numAuthorities > 0 || numAdditionals > 0)
        {
            throw new IllegalStateException("Questions must be added before answers");
        }
        if (rec != null)
        {
            if ((now == 0) || !rec.isExpired(now))
            {
                writeRecord(rec, now);
                numAnswers++;
            }
        }
    }

    private LinkedList authorativeAnswers = new LinkedList();

    /**
     * Add an authorative answer to the message.
     */
    void addAuthorativeAnswer(DNSRecord rec) throws IOException
    {
        if (numAdditionals > 0)
        {
            throw new IllegalStateException("Authorative answers must be added before additional answers");
        }
        authorativeAnswers.add(rec);
        writeRecord(rec, 0);
        numAuthorities++;

        // VERIFY:

    }

    void writeByte(int value) throws IOException
    {
        if (off >= data.length)
        {
            throw new IOException("buffer full");
        }
        data[off++] = (byte) value;
    }

    void writeBytes(String str, int off, int len) throws IOException
    {
        for (int i = 0; i < len; i++)
        {
            writeByte(str.charAt(off + i));
        }
    }

    void writeBytes(byte data[]) throws IOException
    {
        if (data != null)
        {
            writeBytes(data, 0, data.length);
        }
    }

    void writeBytes(byte data[], int off, int len) throws IOException
    {
        for (int i = 0; i < len; i++)
        {
            writeByte(data[off + i]);
        }
    }

    void writeShort(int value) throws IOException
    {
        writeByte(value >> 8);
        writeByte(value);
    }

    void writeInt(int value) throws IOException
    {
        writeShort(value >> 16);
        writeShort(value);
    }

    void writeUTF(String str, int off, int len) throws IOException
    {
        // compute utf length
        int utflen = 0;
        for (int i = 0; i < len; i++)
        {
            int ch = str.charAt(off + i);
            if ((ch >= 0x0001) && (ch <= 0x007F))
            {
                utflen += 1;
            }
            else
            {
                if (ch > 0x07FF)
                {
                    utflen += 3;
                }
                else
                {
                    utflen += 2;
                }
            }
        }
        // write utf length
        writeByte(utflen);
        // write utf data
        for (int i = 0; i < len; i++)
        {
            int ch = str.charAt(off + i);
            if ((ch >= 0x0001) && (ch <= 0x007F))
            {
                writeByte(ch);
            }
            else
            {
                if (ch > 0x07FF)
                {
                    writeByte(0xE0 | ((ch >> 12) & 0x0F));
                    writeByte(0x80 | ((ch >> 6) & 0x3F));
                    writeByte(0x80 | ((ch >> 0) & 0x3F));
                }
                else
                {
                    writeByte(0xC0 | ((ch >> 6) & 0x1F));
                    writeByte(0x80 | ((ch >> 0) & 0x3F));
                }
            }
        }
    }

    void writeName(String name) throws IOException
    {
        while (true)
        {
            int n = name.indexOf('.');
            if (n < 0)
            {
                n = name.length();
            }
            if (n <= 0)
            {
                writeByte(0);
                return;
            }
            Integer offset = (Integer) names.get(name);
            if (offset != null)
            {
                int val = offset.intValue();

                if (val > off)
                {
                    logger.log(Level.WARNING, "DNSOutgoing writeName failed val=" + val + " name=" + name);
                }

                writeByte((val >> 8) | 0xC0);
                writeByte(val);
                return;
            }
            names.put(name, new Integer(off));
            writeUTF(name, 0, n);
            name = name.substring(n);
            if (name.startsWith("."))
            {
                name = name.substring(1);
            }
        }
    }

    void writeQuestion(DNSQuestion question) throws IOException
    {
        writeName(question.name);
        writeShort(question.type);
        writeShort(question.clazz);
    }

    void writeRecord(DNSRecord rec, long now) throws IOException
    {
        int save = off;
        try
        {
            writeName(rec.name);
            writeShort(rec.type);
            writeShort(rec.clazz | ((rec.unique && multicast) ? DNSConstants.CLASS_UNIQUE : 0));
            writeInt((now == 0) ? rec.ttl : rec.getRemainingTTL(now));
            writeShort(0);
            int start = off;
            rec.write(this);
            int len = off - start;
            data[start - 2] = (byte) (len >> 8);
            data[start - 1] = (byte) (len & 0xFF);
        }
        catch (IOException e)
        {
            off = save;
            throw e;
        }
    }

    /**
     * Finish the message before sending it off.
     */
    void finish() throws IOException
    {
        int save = off;
        off = 0;

        writeShort(multicast ? 0 : id);
        writeShort(flags);
        writeShort(numQuestions);
        writeShort(numAnswers);
        writeShort(numAuthorities);
        writeShort(numAdditionals);
        off = save;
    }

    boolean isQuery()
    {
        return (flags & DNSConstants.FLAGS_QR_MASK) == DNSConstants.FLAGS_QR_QUERY;
    }

    public boolean isEmpty()
    {
        return numQuestions == 0 && numAuthorities == 0
            && numAdditionals == 0 && numAnswers == 0;
    }


    public String toString()
    {
        StringBuffer buf = new StringBuffer();
        buf.append(isQuery() ? "dns[query," : "dns[response,");
        //buf.append(packet.getAddress().getHostAddress());
        buf.append(':');
        //buf.append(packet.getPort());
        //buf.append(",len=");
        //buf.append(packet.getLength());
        buf.append(",id=0x");
        buf.append(Integer.toHexString(id));
        if (flags != 0)
        {
            buf.append(",flags=0x");
            buf.append(Integer.toHexString(flags));
            if ((flags & DNSConstants.FLAGS_QR_RESPONSE) != 0)
            {
                buf.append(":r");
            }
            if ((flags & DNSConstants.FLAGS_AA) != 0)
            {
                buf.append(":aa");
            }
            if ((flags & DNSConstants.FLAGS_TC) != 0)
            {
                buf.append(":tc");
            }
        }
        if (numQuestions > 0)
        {
            buf.append(",questions=");
            buf.append(numQuestions);
        }
        if (numAnswers > 0)
        {
            buf.append(",answers=");
            buf.append(numAnswers);
        }
        if (numAuthorities > 0)
        {
            buf.append(",authorities=");
            buf.append(numAuthorities);
        }
        if (numAdditionals > 0)
        {
            buf.append(",additionals=");
            buf.append(numAdditionals);
        }
        buf.append(",\nnames=" + names);
        buf.append(",\nauthorativeAnswers=" + authorativeAnswers);

        buf.append("]");
        return buf.toString();
    }

}
