// Copyright 2003-2005 Arthur van Hoff, Rick Blair
// Licensed under Apache License version 2.0
// Original license LGPL

package javax.jmdns.impl;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceInfo.Fields;
import javax.jmdns.impl.DNSOutgoing.MessageOutputStream;
import javax.jmdns.impl.constants.DNSConstants;
import javax.jmdns.impl.constants.DNSRecordClass;
import javax.jmdns.impl.constants.DNSRecordType;

import javax.jmdns.impl.util.ByteWrangler;


/**
 * DNS record
 *
 * @author Arthur van Hoff, Rick Blair, Werner Randelshofer, Pierre Frisch
 */
public abstract class DNSRecord extends DNSEntry {
    private static Logger logger = LoggerFactory.getLogger(DNSRecord.class.getName());

    private int           _ttl;
    private long          _created;
    private int           _isStaleAndShouldBeRefreshedPercentage;
    private final int     _randomStaleRefreshOffset;

    /**
     * This source is mainly for debugging purposes, should be the address that sent this record.
     */
    private InetAddress   _source;

    /**
     * Create a DNSRecord with a name, type, class, and ttl.
     */
    DNSRecord(String name, DNSRecordType type, DNSRecordClass recordClass, boolean unique, int ttl) {
        super(name, type, recordClass, unique);
        this._ttl = ttl;
        this._created = System.currentTimeMillis();
        _randomStaleRefreshOffset = new Random().nextInt(3);
        _isStaleAndShouldBeRefreshedPercentage = DNSConstants.STALE_REFRESH_STARTING_PERCENTAGE + _randomStaleRefreshOffset;
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSEntry#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object other) {
        return (other instanceof DNSRecord) && super.equals(other) && sameValue((DNSRecord) other);
    }

    /**
     * True if this record has the same value as some other record.
     */
    abstract boolean sameValue(DNSRecord other);

    /**
     * True if this record has the same type as some other record.
     */
    boolean sameType(DNSRecord other) {
        return this.getRecordType() == other.getRecordType();
    }

    /**
     * Handles a query represented by this record.
     *
     * @return Returns true if a conflict with one of the services registered with JmDNS or with the hostname occured.
     */
    abstract boolean handleQuery(JmDNSImpl dns, long expirationTime);

    /**
     * Handles a response represented by this record.
     *
     * @return Returns true if a conflict with one of the services registered with JmDNS or with the hostname occured.
     */
    abstract boolean handleResponse(JmDNSImpl dns);

    /**
     * Adds this as an answer to the provided outgoing datagram.
     */
    abstract DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException;

    /**
     * True if this record is suppressed by the answers in a message.
     */
    boolean suppressedBy(DNSIncoming msg) {
        try {
            for (DNSRecord answer : msg.getAllAnswers()) {
                if (suppressedBy(answer)) {
                    return true;
                }
            }
            return false;
        } catch (ArrayIndexOutOfBoundsException e) {
            logger.warn("suppressedBy() message " + msg + " exception ", e);
            // msg.print(true);
            return false;
        }
    }

    /**
     * True if this record would be suppressed by an answer. This is the case if this record would not have a significantly longer TTL.
     */
    boolean suppressedBy(DNSRecord other) {
        if (this.equals(other) && (other._ttl > _ttl / 2)) {
            return true;
        }
        return false;
    }

    /**
     * Get the expiration time of this record.
     */
    long getExpirationTime(int percent) {
        // ttl is in seconds the constant 10 is 1000 ms / 100 %
        return _created + (percent * ((long)_ttl) * 10L);
    }

    /**
     * Get the remaining TTL for this record.
     */
    int getRemainingTTL(long now) {
        return (int) Math.max(0, (getExpirationTime(100) - now) / 1000);
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSEntry#isExpired(long)
     */
    @Override
    public boolean isExpired(long now) {
        return getExpirationTime(100) <= now;
    }

    /*
     * (non-Javadoc)
     * @see javax.jmdns.impl.DNSEntry#isStale(long)
     */
    @Override
    public boolean isStale(long now) {
        return getExpirationTime(50) <= now;
    }

    /**
     * Check if the record is stale and whether the record should be refreshed over the network.
     *
     * @param now
     *            update date
     * @return <code>true</code> is the record is stale and should be refreshed, <code>false</code> otherwise.
     */
    public boolean isStaleAndShouldBeRefreshed(long now) {
        return getExpirationTime(_isStaleAndShouldBeRefreshedPercentage) <= now;
    }

    /*
    * Increment the percentage that determines whether a record needs to be refreshed.
     */
    public void incrementRefreshPercentage() {
        _isStaleAndShouldBeRefreshedPercentage += DNSConstants.STALE_REFRESH_INCREMENT;
        if (_isStaleAndShouldBeRefreshedPercentage > 100) {
            _isStaleAndShouldBeRefreshedPercentage = 100;
        }
    }

    /**
     * Reset the TTL of a record. This avoids having to update the entire record in the cache.
     */
    void resetTTL(DNSRecord other) {
        _created = other._created;
        _ttl = other._ttl;
        _isStaleAndShouldBeRefreshedPercentage = DNSConstants.STALE_REFRESH_STARTING_PERCENTAGE + _randomStaleRefreshOffset;
    }

    /**
     * When a record flushed we don't remove it immediately, but mark it for rapid decay.
     */
    void setWillExpireSoon(long now) {
        _created = now;
        _ttl = DNSConstants.RECORD_EXPIRY_DELAY;
    }

    /**
     * Write this record into an outgoing message.
     */
    abstract void write(MessageOutputStream out);

    public static class IPv4Address extends Address {

        IPv4Address(String name, DNSRecordClass recordClass, boolean unique, int ttl, InetAddress addr) {
            super(name, DNSRecordType.TYPE_A, recordClass, unique, ttl, addr);
        }

        IPv4Address(String name, DNSRecordClass recordClass, boolean unique, int ttl, byte[] rawAddress) {
            super(name, DNSRecordType.TYPE_A, recordClass, unique, ttl, rawAddress);
        }

        @Override
        void write(MessageOutputStream out) {
            if (_addr != null) {
                byte[] buffer = _addr.getAddress();
                // If we have a type A records we should answer with a IPv4 address
                if (_addr instanceof Inet4Address) {
                    // All is good
                } else {
                    // Get the last four bytes
                    byte[] tempbuffer = buffer;
                    buffer = new byte[4];
                    System.arraycopy(tempbuffer, 12, buffer, 0, 4);
                }
                int length = buffer.length;
                out.writeBytes(buffer, 0, length);
            }
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceInfo(boolean)
         */
        @Override
        public ServiceInfo getServiceInfo(boolean persistent) {

            ServiceInfoImpl info = (ServiceInfoImpl) super.getServiceInfo(persistent);
            info.addAddress((Inet4Address) _addr);
            return info;
        }

    }

    public static class IPv6Address extends Address {

        IPv6Address(String name, DNSRecordClass recordClass, boolean unique, int ttl, InetAddress addr) {
            super(name, DNSRecordType.TYPE_AAAA, recordClass, unique, ttl, addr);
        }

        IPv6Address(String name, DNSRecordClass recordClass, boolean unique, int ttl, byte[] rawAddress) {
            super(name, DNSRecordType.TYPE_AAAA, recordClass, unique, ttl, rawAddress);
        }

        @Override
        void write(MessageOutputStream out) {
            if (_addr != null) {
                byte[] buffer = _addr.getAddress();
                // If we have a type AAAA records we should answer with a IPv6 address
                if (_addr instanceof Inet4Address) {
                    byte[] tempbuffer = buffer;
                    buffer = new byte[16];
                    for (int i = 0; i < 16; i++) {
                        if (i < 11) {
                            buffer[i] = tempbuffer[i - 12];
                        } else {
                            buffer[i] = 0;
                        }
                    }
                }
                int length = buffer.length;
                out.writeBytes(buffer, 0, length);
            }
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceInfo(boolean)
         */
        @Override
        public ServiceInfo getServiceInfo(boolean persistent) {

            ServiceInfoImpl info = (ServiceInfoImpl) super.getServiceInfo(persistent);
            info.addAddress((Inet6Address) _addr);
            return info;
        }

    }

    /**
     * Address record.
     */
    public static abstract class Address extends DNSRecord {
        private static Logger logger1 = LoggerFactory.getLogger(Address.class.getName());

        InetAddress           _addr;

        protected Address(String name, DNSRecordType type, DNSRecordClass recordClass, boolean unique, int ttl, InetAddress addr) {
            super(name, type, recordClass, unique, ttl);
            this._addr = addr;
        }

        protected Address(String name, DNSRecordType type, DNSRecordClass recordClass, boolean unique, int ttl, byte[] rawAddress) {
            super(name, type, recordClass, unique, ttl);
            try {
                this._addr = InetAddress.getByAddress(rawAddress);
            } catch (UnknownHostException exception) {
                logger1.warn("Address() exception ", exception);
            }
        }

        boolean same(DNSRecord other) {
            if (!(other instanceof Address)) {
                return false;
            }
            return ((sameName(other)) && ((sameValue(other))));
        }

        boolean sameName(DNSRecord other) {
            return this.getName().equalsIgnoreCase(other.getName());
        }

        @Override
        boolean sameValue(DNSRecord other) {
            try {
                if (!(other instanceof Address)) {
                    return false;
                }
                Address address = (Address) other;
                if ((this.getAddress() == null) && (address.getAddress() != null)) {
                    return false;
                }
                return this.getAddress().equals(address.getAddress());
            } catch (Exception e) {
                logger1.info("Failed to compare addresses of DNSRecords", e);
                return false;
            }
        }

        @Override
        public boolean isSingleValued() {
            return false;
        }

        InetAddress getAddress() {
            return _addr;
        }

        /**
         * Creates a byte array representation of this record. This is needed for tie-break tests according to draft-cheshire-dnsext-multicastdns-04.txt chapter 9.2.
         */
        @Override
        protected void toByteArray(DataOutputStream dout) throws IOException {
            super.toByteArray(dout);
            byte[] buffer = this.getAddress().getAddress();
            for (int i = 0; i < buffer.length; i++) {
                dout.writeByte(buffer[i]);
            }
        }

        /**
         * Does the necessary actions, when this as a query.
         */
        @Override
        boolean handleQuery(JmDNSImpl dns, long expirationTime) {
            if (dns.getLocalHost().conflictWithRecord(this)) {
                DNSRecord.Address localAddress = dns.getLocalHost().getDNSAddressRecord(this.getRecordType(), this.isUnique(), DNSConstants.DNS_TTL);
                if (localAddress != null) {
                    int comparison = this.compareTo(localAddress);

                    if (comparison == 0) {
                        // the 2 records are identical this probably means we are seeing our own record.
                        // With multiple interfaces on a single computer it is possible to see our
                        // own records come in on different interfaces than the ones they were sent on.
                        // see section "10. Conflict Resolution" of mdns draft spec.
                        logger1.debug("handleQuery() Ignoring an identical address query");
                        return false;
                    }

                    logger1.debug("handleQuery() Conflicting query detected.");
                    // Tie breaker test
                    if (dns.isProbing() && comparison > 0) {
                        // We lost the tie-break. We have to choose a different name.
                        dns.getLocalHost().incrementHostName();
                        dns.getCache().clear();
                        for (ServiceInfo serviceInfo : dns.getServices().values()) {
                            ServiceInfoImpl info = (ServiceInfoImpl) serviceInfo;
                            info.revertState();
                        }
                    }
                    dns.revertState();
                    return true;
                }
            }
            return false;
        }

        /**
         * Does the necessary actions, when this as a response.
         */
        @Override
        boolean handleResponse(JmDNSImpl dns) {
            if (dns.getLocalHost().conflictWithRecord(this)) {
                logger1.debug("handleResponse() Denial detected");

                if (dns.isProbing()) {
                    dns.getLocalHost().incrementHostName();
                    dns.getCache().clear();
                    for (ServiceInfo serviceInfo : dns.getServices().values()) {
                        ServiceInfoImpl info = (ServiceInfoImpl) serviceInfo;
                        info.revertState();
                    }
                }
                dns.revertState();
                return true;
            }
            return false;
        }

        @Override
        DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException {
            return out;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceInfo(boolean)
         */
        @Override
        public ServiceInfo getServiceInfo(boolean persistent) {
            ServiceInfoImpl info = new ServiceInfoImpl(this.getQualifiedNameMap(), 0, 0, 0, persistent, (byte[]) null);
            // info.setAddress(_addr); This is done in the sub class so we don't have to test for class type
            return info;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceEvent(javax.jmdns.impl.JmDNSImpl)
         */
        @Override
        public ServiceEvent getServiceEvent(JmDNSImpl dns) {
            ServiceInfo info = this.getServiceInfo(false);
            ((ServiceInfoImpl) info).setDns(dns);
            return new ServiceEventImpl(dns, info.getType(), info.getName(), info);
        }

        /*
         * (non-Javadoc)
         * @see com.webobjects.discoveryservices.DNSRecord#toString(java.lang.StringBuilder)
         */
        @Override
        protected void toString(final StringBuilder sb) {
            super.toString(sb);
            sb.append(" address: '")
                .append(this.getAddress() != null ? this.getAddress().getHostAddress() : "null")
                .append('\'');
        }

    }

    /**
     * Pointer record.
     */
    public static class Pointer extends DNSRecord {
        // private static Logger logger = LoggerFactory.getLogger(Pointer.class.getName());
        private final String _alias;

        public Pointer(String name, DNSRecordClass recordClass, boolean unique, int ttl, String alias) {
            super(name, DNSRecordType.TYPE_PTR, recordClass, unique, ttl);
            this._alias = alias;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSEntry#isSameEntry(javax.jmdns.impl.DNSEntry)
         */
        @Override
        public boolean isSameEntry(DNSEntry entry) {
            return super.isSameEntry(entry) && (entry instanceof Pointer) && this.sameValue((Pointer) entry);
        }

        @Override
        void write(MessageOutputStream out) {
            out.writeName(_alias);
        }

        @Override
        boolean sameValue(DNSRecord other) {
            if (!(other instanceof Pointer)) {
                return false;
            }
            Pointer pointer = (Pointer) other;
            if ((_alias == null) && (pointer._alias != null)) {
                return false;
            }
            return _alias.equals(pointer._alias);
        }

        @Override
        public boolean isSingleValued() {
            return false;
        }

        @Override
        boolean handleQuery(JmDNSImpl dns, long expirationTime) {
            // Nothing to do (?)
            // I think there is no possibility for conflicts for this record type?
            return false;
        }

        @Override
        boolean handleResponse(JmDNSImpl dns) {
            // Nothing to do (?)
            // I think there is no possibility for conflicts for this record type?
            return false;
        }

        String getAlias() {
            return _alias;
        }

        @Override
        DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException {
            return out;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceInfo(boolean)
         */
        @Override
        public ServiceInfo getServiceInfo(boolean persistent) {
            if (this.isServicesDiscoveryMetaQuery()) {
                // The service name is in the alias
                Map<Fields, String> map = ServiceInfoImpl.decodeQualifiedNameMapForType(this.getAlias());
                return new ServiceInfoImpl(map, 0, 0, 0, persistent, (byte[]) null);
            } else if (this.isReverseLookup()) {
                return new ServiceInfoImpl(this.getQualifiedNameMap(), 0, 0, 0, persistent, (byte[]) null);
            } else if (this.isDomainDiscoveryQuery()) {
                // FIXME [PJYF Nov 16 2010] We do not currently support domain discovery
                return new ServiceInfoImpl(this.getQualifiedNameMap(), 0, 0, 0, persistent, (byte[]) null);
            }
            Map<Fields, String> map = ServiceInfoImpl.decodeQualifiedNameMapForType(this.getAlias());
            map.put(Fields.Subtype, this.getQualifiedNameMap().get(Fields.Subtype));
            return new ServiceInfoImpl(map, 0, 0, 0, persistent, this.getAlias());
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceEvent(javax.jmdns.impl.JmDNSImpl)
         */
        @Override
        public ServiceEvent getServiceEvent(JmDNSImpl dns) {
            ServiceInfo info = this.getServiceInfo(false);
            ((ServiceInfoImpl) info).setDns(dns);
            String domainName = info.getType();
            String serviceName = JmDNSImpl.toUnqualifiedName(domainName, this.getAlias());
            return new ServiceEventImpl(dns, domainName, serviceName, info);
        }

        /*
         * (non-Javadoc)
         * @see com.webobjects.discoveryservices.DNSRecord#toString(java.lang.StringBuilder)
         */
        @Override
        protected void toString(final StringBuilder sb) {
            super.toString(sb);
            sb.append(" alias: '")
                .append(_alias != null ? _alias.toString() : "null")
                .append('\'');
        }

    }

    public static class Text extends DNSRecord {
        // private static Logger logger = LoggerFactory.getLogger(Text.class.getName());
        private final byte[] _text;

        public Text(String name, DNSRecordClass recordClass, boolean unique, int ttl, byte text[]) {
            super(name, DNSRecordType.TYPE_TXT, recordClass, unique, ttl);
            this._text = (text != null && text.length > 0 ? text : ByteWrangler.EMPTY_TXT);
        }

        /**
         * @return the text
         */
        byte[] getText() {
            return this._text;
        }

        @Override
        void write(MessageOutputStream out) {
            out.writeBytes(_text, 0, _text.length);
        }

        @Override
        boolean sameValue(DNSRecord other) {
            if (!(other instanceof Text)) {
                return false;
            }
            Text txt = (Text) other;
            if ((_text == null) && (txt._text != null)) {
                return false;
            }
            if (txt._text.length != _text.length) {
                return false;
            }
            for (int i = _text.length; i-- > 0;) {
                if (txt._text[i] != _text[i]) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public boolean isSingleValued() {
            return true;
        }

        @Override
        boolean handleQuery(JmDNSImpl dns, long expirationTime) {
            // Nothing to do (?)
            // I think there is no possibility for conflicts for this record type?
            return false;
        }

        @Override
        boolean handleResponse(JmDNSImpl dns) {
            // Nothing to do (?)
            // Shouldn't we care if we get a conflict at this level?
            /*
             * ServiceInfo info = (ServiceInfo) dns.services.get(name.toLowerCase()); if (info != null) { if (! Arrays.equals(text,info.text)) { info.revertState(); return true; } }
             */
            return false;
        }

        @Override
        DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException {
            return out;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceInfo(boolean)
         */
        @Override
        public ServiceInfo getServiceInfo(boolean persistent) {
            return new ServiceInfoImpl(this.getQualifiedNameMap(), 0, 0, 0, persistent, _text);
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceEvent(javax.jmdns.impl.JmDNSImpl)
         */
        @Override
        public ServiceEvent getServiceEvent(JmDNSImpl dns) {
            ServiceInfo info = this.getServiceInfo(false);
            ((ServiceInfoImpl) info).setDns(dns);
            return new ServiceEventImpl(dns, info.getType(), info.getName(), info);
        }

        /*
         * (non-Javadoc)
         * @see com.webobjects.discoveryservices.DNSRecord#toString(java.lang.StringBuilder)
         */
        @Override
        protected void toString(final StringBuilder sb) {
            super.toString(sb);
            sb.append(" text: '");

            final String text = ByteWrangler.readUTF(_text);

            if (text != null) {
                // if the text is longer than 20 characters cut it to 17 chars
                // and add "..." at the end
                if (20 < text.length()) {
                    sb.append(text, 0, 17).append("...");
                } else {
                    sb.append(text);
                }
            }
            sb.append('\'');
        }

    }

    /**
     * Service record.
     */
    public static class Service extends DNSRecord {
        private static Logger logger1 = LoggerFactory.getLogger(Service.class.getName());
        private final int     _priority;
        private final int     _weight;
        private final int     _port;
        private final String  _server;

        public Service(String name, DNSRecordClass recordClass, boolean unique, int ttl, int priority, int weight, int port, String server) {
            super(name, DNSRecordType.TYPE_SRV, recordClass, unique, ttl);
            this._priority = priority;
            this._weight = weight;
            this._port = port;
            this._server = server;
        }

        @Override
        void write(MessageOutputStream out) {
            out.writeShort(_priority);
            out.writeShort(_weight);
            out.writeShort(_port);
            if (DNSIncoming.USE_DOMAIN_NAME_FORMAT_FOR_SRV_TARGET) {
                out.writeName(_server);
            } else {
                // [PJYF Nov 13 2010] Do we still need this? This looks really bad. All label are supposed to start by a length.
                out.writeUTF(_server, 0, _server.length());

                // add a zero byte to the end just to be safe, this is the strange form
                // used by the BonjourConformanceTest
                out.writeByte(0);
            }
        }

        @Override
        protected void toByteArray(DataOutputStream dout) throws IOException {
            super.toByteArray(dout);
            dout.writeShort(_priority);
            dout.writeShort(_weight);
            dout.writeShort(_port);
            try {
                dout.write(_server.getBytes("UTF-8"));
            } catch (UnsupportedEncodingException exception) {
                /* UTF-8 is always present */
            }
        }

        String getServer() {
            return _server;
        }

        /**
         * @return the priority
         */
        public int getPriority() {
            return this._priority;
        }

        /**
         * @return the weight
         */
        public int getWeight() {
            return this._weight;
        }

        /**
         * @return the port
         */
        public int getPort() {
            return this._port;
        }

        @Override
        boolean sameValue(DNSRecord other) {
            if (!(other instanceof Service)) {
                return false;
            }
            Service s = (Service) other;
            return (_priority == s._priority) && (_weight == s._weight) && (_port == s._port) && _server.equals(s._server);
        }

        @Override
        public boolean isSingleValued() {
            return true;
        }

        @Override
        boolean handleQuery(JmDNSImpl dns, long expirationTime) {
            ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(this.getKey());
            if (info != null && (info.isAnnouncing() || info.isAnnounced()) && (_port != info.getPort() || !_server.equalsIgnoreCase(dns.getLocalHost().getName()))) {
                logger1.debug("handleQuery() Conflicting probe detected from: {}", getRecordSource());
                DNSRecord.Service localService = new DNSRecord.Service(info.getQualifiedName(), DNSRecordClass.CLASS_IN, DNSRecordClass.UNIQUE, DNSConstants.DNS_TTL, info.getPriority(), info.getWeight(), info.getPort(), dns.getLocalHost().getName());

                // This block is useful for debugging race conditions when jmDNS is responding to itself.
                try {
                    if (dns.getInetAddress().equals(getRecordSource())) {
                        logger1.warn("Got conflicting probe from ourselves\nincoming: {}\nlocal   : {}", this.toString(), localService.toString());
                    }
                } catch (IOException e) {
                    logger1.warn("IOException", e);
                }

                int comparison = this.compareTo(localService);

                if (comparison == 0) {
                    // the 2 records are identical this probably means we are seeing our own record.
                    // With multiple interfaces on a single computer it is possible to see our
                    // own records come in on different interfaces than the ones they were sent on.
                    // see section "10. Conflict Resolution" of mdns draft spec.
                    logger1.debug("handleQuery() Ignoring a identical service query");
                    return false;
                }

                // Tie breaker test
                if (info.isProbing() && comparison > 0) {
                    // We lost the tie break
                    String oldName = info.getQualifiedName().toLowerCase();
                    info.setName(NameRegister.Factory.getRegistry().incrementName(dns.getLocalHost().getInetAddress(), info.getName(), NameRegister.NameType.SERVICE));
                    dns.getServices().remove(oldName);
                    dns.getServices().put(info.getQualifiedName().toLowerCase(), info);
                    logger1.debug("handleQuery() Lost tie break: new unique name chosen:{}", info.getName());

                    // We revert the state to start probing again with the new name
                    info.revertState();
                } else {
                    // We won the tie break, so this conflicting probe should be ignored
                    // See paragraph 3 of section 9.2 in mdns draft spec
                    return false;
                }

                return true;

            }
            return false;
        }

        @Override
        boolean handleResponse(JmDNSImpl dns) {
            ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(this.getKey());
            if (info != null && (_port != info.getPort() || !_server.equalsIgnoreCase(dns.getLocalHost().getName()))) {
                logger1.debug("handleResponse() Denial detected");

                if (info.isProbing()) {
                    String oldName = info.getQualifiedName().toLowerCase();
                    info.setName(NameRegister.Factory.getRegistry().incrementName(dns.getLocalHost().getInetAddress(), info.getName(), NameRegister.NameType.SERVICE));
                    dns.getServices().remove(oldName);
                    dns.getServices().put(info.getQualifiedName().toLowerCase(), info);
                    logger1.debug("handleResponse() New unique name chose:{}", info.getName());

                }
                info.revertState();
                return true;
            }
            return false;
        }

        @Override
        DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException {
            ServiceInfoImpl info = (ServiceInfoImpl) dns.getServices().get(this.getKey());
            if (info != null) {
                if (this._port == info.getPort() != _server.equals(dns.getLocalHost().getName())) {
                    return dns.addAnswer(in, addr, port, out, new DNSRecord.Service(info.getQualifiedName(), DNSRecordClass.CLASS_IN, DNSRecordClass.UNIQUE, DNSConstants.DNS_TTL, info.getPriority(), info.getWeight(), info.getPort(), dns
                            .getLocalHost().getName()));
                }
            }
            return out;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceInfo(boolean)
         */
        @Override
        public ServiceInfo getServiceInfo(boolean persistent) {
            return new ServiceInfoImpl(this.getQualifiedNameMap(), _port, _weight, _priority, persistent, (byte[]) null);
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceEvent(javax.jmdns.impl.JmDNSImpl)
         */
        @Override
        public ServiceEvent getServiceEvent(JmDNSImpl dns) {
            ServiceInfo info = this.getServiceInfo(false);
            ((ServiceInfoImpl) info).setDns(dns);
            // String domainName = "";
            // String serviceName = this.getServer();
            // int index = serviceName.indexOf('.');
            // if (index > 0)
            // {
            // serviceName = this.getServer().substring(0, index);
            // if (index + 1 < this.getServer().length())
            // domainName = this.getServer().substring(index + 1);
            // }
            // return new ServiceEventImpl(dns, domainName, serviceName, info);
            return new ServiceEventImpl(dns, info.getType(), info.getName(), info);

        }

        /*
         * (non-Javadoc)
         * @see com.webobjects.discoveryservices.DNSRecord#toString(java.lang.StringBuilder)
         */
        @Override
        protected void toString(final StringBuilder sb) {
            super.toString(sb);
            sb.append(" server: '")
                .append(_server).append(':').append(_port)
                .append('\'');
        }

    }

    public static class HostInformation extends DNSRecord {
        String _os;
        String _cpu;

        /**
         * @param name
         * @param recordClass
         * @param unique
         * @param ttl
         * @param cpu
         * @param os
         */
        public HostInformation(String name, DNSRecordClass recordClass, boolean unique, int ttl, String cpu, String os) {
            super(name, DNSRecordType.TYPE_HINFO, recordClass, unique, ttl);
            _cpu = cpu;
            _os = os;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#addAnswer(javax.jmdns.impl.JmDNSImpl, javax.jmdns.impl.DNSIncoming, java.net.InetAddress, int, javax.jmdns.impl.DNSOutgoing)
         */
        @Override
        DNSOutgoing addAnswer(JmDNSImpl dns, DNSIncoming in, InetAddress addr, int port, DNSOutgoing out) throws IOException {
            return out;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#handleQuery(javax.jmdns.impl.JmDNSImpl, long)
         */
        @Override
        boolean handleQuery(JmDNSImpl dns, long expirationTime) {
            return false;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#handleResponse(javax.jmdns.impl.JmDNSImpl)
         */
        @Override
        boolean handleResponse(JmDNSImpl dns) {
            return false;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#sameValue(javax.jmdns.impl.DNSRecord)
         */
        @Override
        boolean sameValue(DNSRecord other) {
            if (!(other instanceof HostInformation)) {
                return false;
            }
            HostInformation hinfo = (HostInformation) other;
            if ((_cpu == null) && (hinfo._cpu != null)) {
                return false;
            }
            if ((_os == null) && (hinfo._os != null)) {
                return false;
            }
            return _cpu.equals(hinfo._cpu) && _os.equals(hinfo._os);
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#isSingleValued()
         */
        @Override
        public boolean isSingleValued() {
            return true;
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#write(javax.jmdns.impl.DNSOutgoing)
         */
        @Override
        void write(MessageOutputStream out) {
            String hostInfo = _cpu + " " + _os;
            out.writeUTF(hostInfo, 0, hostInfo.length());
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceInfo(boolean)
         */
        @Override
        public ServiceInfo getServiceInfo(boolean persistent) {
            Map<String, String> hinfo = new HashMap<String, String>(2);
            hinfo.put("cpu", _cpu);
            hinfo.put("os", _os);
            return new ServiceInfoImpl(this.getQualifiedNameMap(), 0, 0, 0, persistent, hinfo);
        }

        /*
         * (non-Javadoc)
         * @see javax.jmdns.impl.DNSRecord#getServiceEvent(javax.jmdns.impl.JmDNSImpl)
         */
        @Override
        public ServiceEvent getServiceEvent(JmDNSImpl dns) {
            ServiceInfo info = this.getServiceInfo(false);
            ((ServiceInfoImpl) info).setDns(dns);
            return new ServiceEventImpl(dns, info.getType(), info.getName(), info);
        }

        /*
         * (non-Javadoc)
         * @see com.webobjects.discoveryservices.DNSRecord#toString(java.lang.StringBuilder)
         */
        @Override
        protected void toString(final StringBuilder sb) {
            super.toString(sb);
            sb.append(" cpu: '").append(_cpu)
                .append("' os: '").append( _os)
                .append('\'');
        }

    }

    /**
     * Determine if a record can have multiple values in the cache.
     *
     * @return <code>false</code> if this record can have multiple values in the cache, <code>true</code> otherwise.
     */
    public abstract boolean isSingleValued();

    /**
     * Return a service information associated with that record if appropriate.
     *
     * @return service information
     */
    public ServiceInfo getServiceInfo() {
        return this.getServiceInfo(false);
    }

    /**
     * Return a service information associated with that record if appropriate.
     *
     * @param persistent
     *            if <code>true</code> ServiceListener.resolveService will be called whenever new new information is received.
     * @return service information
     */
    public abstract ServiceInfo getServiceInfo(boolean persistent);

    /**
     * Creates and return a service event for this record.
     *
     * @param dns
     *            DNS serviced by this event
     * @return service event
     */
    public abstract ServiceEvent getServiceEvent(JmDNSImpl dns);

    public void setRecordSource(InetAddress source) {
        this._source = source;
    }

    public InetAddress getRecordSource() {
        return _source;
    }

    /*
     * (non-Javadoc)
     * @see com.webobjects.discoveryservices.DNSRecord#toString(java.lang.StringBuilder)
     */
    @Override
    protected void toString(final StringBuilder sb) {
        super.toString(sb);
        final int remaininggTTL = getRemainingTTL(System.currentTimeMillis());
        sb.append(" ttl: '").append(remaininggTTL).append('/').append(_ttl).append('\'');
    }

    public void setTTL(int ttl) {
        this._ttl = ttl;
    }

    public int getTTL() {
        return _ttl;
    }

    public long getCreated() {
        return this._created;
    }

}
