package net.i2p.data.i2np;
/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2003 and released into the public domain
 * with no warranty of any kind, either expressed or implied.
 * It probably won't make your computer catch on fire, or eat
 * your children, but it might.  Use at your own risk.
 *
 */

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.router.RouterInfo;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import net.i2p.data.TunnelId;
//import net.i2p.util.Log;
import net.i2p.util.VersionComparator;

/**
 * Defines the message a router sends to another router to search for a
 * key in the network database.
 *
 * @author jrandom
 */
public class DatabaseLookupMessage extends FastI2NPMessageImpl {
    //private final static Log _log = new Log(DatabaseLookupMessage.class);
    public final static int MESSAGE_TYPE = 2;
    private Hash _key;
    private Hash _fromHash;
    private TunnelId _replyTunnel;
    /** this must be kept as a list to preserve the order and not break the checksum */
    private List<Hash> _dontIncludePeers;
    private SessionKey _replyKey;
    private SessionTag _replyTag;
    private Type _type;
    
    //private static volatile long _currentLookupPeriod = 0;
    //private static volatile int _currentLookupCount = 0;
    // if we try to send over 20 netDb lookups in 10 seconds, we're acting up
    //private static final long LOOKUP_THROTTLE_PERIOD = 10*1000;
    //private static final long LOOKUP_THROTTLE_MAX = 50;

    /** Insanely big. Not much more than 1500 will fit in a message.
        Have to prevent a huge alloc on rcv of a malicious msg though */
    private static final int MAX_NUM_PEERS = 512;
    
    private static final byte FLAG_TUNNEL = 0x01;
    // any flags below here will confuse routers 0.9.5 or lower
    private static final byte FLAG_ENCRYPT = 0x02;
    private static final byte FLAG_TYPE_MASK = 0x0c;
    private static final byte FLAG_TYPE_ANY = 0;
    private static final byte FLAG_TYPE_LS = 0x04;
    private static final byte FLAG_TYPE_RI = 0x08;
    private static final byte FLAG_TYPE_EXPL = 0x0c;

    /** @since 0.9.16 */
    public enum Type {
        /** default - LS or RI */
        ANY,
        /** lease set only */
        LS,
        /** router info only */
        RI,
        /** exploratory - return closest non-floodfill router infos */
        EXPL
    }


    /**
     *  It's not supported until 0.9.7, but as of
     *  0.9.6 we can specify the bit in the flags without
     *  the receiver rejecting the whole message as invalid.
     */
    private static final String MIN_ENCRYPTION_VERSION = "0.9.7";

    public DatabaseLookupMessage(I2PAppContext context) {
        this(context, false);
    }

    /** @param locallyCreated ignored */
    public DatabaseLookupMessage(I2PAppContext context, boolean locallyCreated) {
        super(context);
        //setSearchKey(null);
        //setFrom(null);
        //setDontIncludePeers(null);
        
        // This is the wrong place for this, any throttling should be in netdb
        // And it doesnt throttle anyway (that would have to be in netdb), it just increments a stat
        //context.statManager().createRateStat("router.throttleNetDbDoSSend", "How many netDb lookup messages we are sending during a period with a DoS detected", "Throttle", new long[] { 60*1000, 10*60*1000, 60*60*1000, 24*60*60*1000 });
        //
        // only check DoS generation if we are creating the message...
        //if (locallyCreated) {
        //    // we do this in the writeMessage so we know that we have all the data
        //    int dosCount = detectDoS(context);
        //    if (dosCount > 0) {
        //        if (_log.shouldLog(Log.WARN))
        //            _log.warn("Are we flooding the network with NetDb messages?  (" + dosCount 
        //                      + " messages so far)", new Exception("Flood cause"));
        //    }
        //}
        _type = Type.ANY;
    }
    
    /**
     * Return number of netDb messages in this period, if flood, else 0
     *
     */
/*****
    private static int detectDoS(I2PAppContext context) {
        int count = _currentLookupCount++;
        // now lets check for DoS
        long now = context.clock().now();
        if (_currentLookupPeriod + LOOKUP_THROTTLE_PERIOD > now) {
            // same period, check for DoS
            if (count >= LOOKUP_THROTTLE_MAX) {
                context.statManager().addRateData("router.throttleNetDbDoSSend", count, 0);
                return count;
            } else {
                // no DoS, at least, not yet
                return 0;
            }
        } else {
            // on to the next period, reset counter, no DoS
            // (no, I'm not worried about concurrency here)
            _currentLookupPeriod = now;
            _currentLookupCount = 1;
            return 0;
        }
    }
*****/
    
    /**
     * Defines the key being searched for
     */
    public Hash getSearchKey() { return _key; }

    /**
     * @throws IllegalStateException if key previously set, to protect saved checksum
     */
    public void setSearchKey(Hash key) {
        if (_key != null)
            throw new IllegalStateException();
        _key = key;
    }
    
    /**
     *  Defines the type of data being searched for.
     *  Default ANY.
     *
     *  @return non-null
     *  @since 0.9.16
     */
    public Type getSearchType() { return _type; }

    /**
     *  Defines the type of data being searched for.
     *  Default ANY.
     *  Must be ANY for queried routers 0.9.5 or lower, but there are few if
     *  any floodfills that old left, so not even worth checking.
     *
     *  @param type non-null
     *  @since 0.9.16
     */
    public void setSearchType(Type type) {
        if (type == null)
            throw new IllegalArgumentException();
        _type = type;
    }
    
    /**
     * Contains the router who requested this lookup
     *
     */
    public Hash getFrom() { return _fromHash; }
    
    /**
     * @throws IllegalStateException if from previously set, to protect saved checksum
     */
    public void setFrom(Hash from) {
        if (_fromHash != null)
            throw new IllegalStateException();
        _fromHash = from;
    }
    
    /**
     * Contains the tunnel ID a reply should be sent to
     *
     */
    public TunnelId getReplyTunnel() { return _replyTunnel; }

    /**
     * @throws IllegalStateException if tunnel previously set, to protect saved checksum
     */
    public void setReplyTunnel(TunnelId replyTunnel) {
        if (_replyTunnel != null)
            throw new IllegalStateException();
        _replyTunnel = replyTunnel;
    }
    
    /**
     *  Does this router support encrypted replies?
     *
     *  @param to null OK
     *  @since 0.9.7
     */
    public static boolean supportsEncryptedReplies(RouterInfo to) {
        if (to == null)
            return false;
        String v = to.getVersion();
        return VersionComparator.comp(v, MIN_ENCRYPTION_VERSION) >= 0;
    }
    
    /**
     *  The included session key or null if unset
     *
     *  @since 0.9.7
     */
    public SessionKey getReplyKey() { return _replyKey; }
    
    /**
     *  The included session tag or null if unset
     *
     *  @since 0.9.7
     */
    public SessionTag getReplyTag() { return _replyTag; }

    /**
     *  Only worthwhile if sending reply via tunnel
     *
     *  @throws IllegalStateException if key or tag previously set, to protect saved checksum
     *  @param encryptKey non-null
     *  @param encryptTag non-null
     *  @since 0.9.7
     */
    public void setReplySession(SessionKey encryptKey, SessionTag encryptTag) {
        if (_replyKey != null || _replyTag != null)
            throw new IllegalStateException();
        _replyKey = encryptKey;
        _replyTag = encryptTag;
    }
    
    /**
     * Set of peers that a lookup reply should NOT include.
     * WARNING - returns a copy.
     *
     * @return Set of Hash objects, each of which is the H(routerIdentity) to skip, or null
     */
    public Set<Hash> getDontIncludePeers() {
        if (_dontIncludePeers == null)
            return null;
        return new HashSet<Hash>(_dontIncludePeers);
    }

    /**
     * Replace the dontInclude set with this set.
     * WARNING - makes a copy.
     * Invalidates the checksum.
     *
     * @param peers may be null
     */
    public void setDontIncludePeers(Collection<Hash> peers) {
        _hasChecksum = false;
        if (peers != null)
            _dontIncludePeers = new ArrayList<Hash>(peers);
        else
            _dontIncludePeers = null;
    }

    /**
     * Add to the set.
     * Invalidates the checksum.
     *
     * @param peer non-null
     * @since 0.8.12
     */
    public void addDontIncludePeer(Hash peer) {
        if (_dontIncludePeers == null)
            _dontIncludePeers = new ArrayList<Hash>();
        else if (_dontIncludePeers.contains(peer))
            return;
        _hasChecksum = false;
        _dontIncludePeers.add(peer);
    }

    /**
     * Add to the set.
     * Invalidates the checksum.
     *
     * @param peers non-null
     * @since 0.8.12
     */
    public void addDontIncludePeers(Collection<Hash> peers) {
        _hasChecksum = false;
        if (_dontIncludePeers == null) {
            _dontIncludePeers = new ArrayList<Hash>(peers);
        } else {
            for (Hash peer : peers) {
                if (!_dontIncludePeers.contains(peer))
                    _dontIncludePeers.add(peer);
            }
        }
    }
    
    public void readMessage(byte data[], int offset, int dataSize, int type) throws I2NPMessageException {
        if (type != MESSAGE_TYPE) throw new I2NPMessageException("Message type is incorrect for this message");
        int curIndex = offset;
        
        //byte keyData[] = new byte[Hash.HASH_LENGTH];
        //System.arraycopy(data, curIndex, keyData, 0, Hash.HASH_LENGTH);
        _key = Hash.create(data, curIndex);
        curIndex += Hash.HASH_LENGTH;
        //_key = new Hash(keyData);
        
        //byte fromData[] = new byte[Hash.HASH_LENGTH];
        //System.arraycopy(data, curIndex, fromData, 0, Hash.HASH_LENGTH);
        _fromHash = Hash.create(data, curIndex);
        curIndex += Hash.HASH_LENGTH;
        //_fromHash = new Hash(fromData);
        
        // as of 0.9.6, ignore other 7 bits of the flag byte
        // TODO store the whole flag byte
        boolean tunnelSpecified = (data[curIndex] & FLAG_TUNNEL) != 0;
        boolean replyKeySpecified = (data[curIndex] & FLAG_ENCRYPT) != 0;
        switch (data[curIndex] & FLAG_TYPE_MASK) {
            case FLAG_TYPE_LS:
                _type = Type.LS;
                break;
            case FLAG_TYPE_RI:
                _type = Type.RI;
                break;
            case FLAG_TYPE_EXPL:
                _type = Type.EXPL;
                break;
            case FLAG_TYPE_ANY:
            default:
                _type = Type.ANY;
                break;
        }
        curIndex++;
        
        if (tunnelSpecified) {
            _replyTunnel = new TunnelId(DataHelper.fromLong(data, curIndex, 4));
            curIndex += 4;
        }
        
        int numPeers = (int)DataHelper.fromLong(data, curIndex, 2);
        curIndex += 2;
        
        if ( (numPeers < 0) || (numPeers > MAX_NUM_PEERS) )
            throw new I2NPMessageException("Invalid number of peers - " + numPeers);
        List<Hash> peers = numPeers > 0 ? new ArrayList<Hash>(numPeers) : null;
        for (int i = 0; i < numPeers; i++) {
            //byte peer[] = new byte[Hash.HASH_LENGTH];
            //System.arraycopy(data, curIndex, peer, 0, Hash.HASH_LENGTH);
            Hash p = Hash.create(data, curIndex);
            curIndex += Hash.HASH_LENGTH;
            peers.add(p);
        }
        _dontIncludePeers = peers;
        if (replyKeySpecified) {
            byte[] rk = new byte[SessionKey.KEYSIZE_BYTES];
            System.arraycopy(data, curIndex, rk, 0, SessionKey.KEYSIZE_BYTES);
            _replyKey = new SessionKey(rk);
            curIndex += SessionKey.KEYSIZE_BYTES;
            // number of tags, assume always 1 for now
            curIndex++;
            byte[] rt = new byte[SessionTag.BYTE_LENGTH];
            System.arraycopy(data, curIndex, rt, 0, SessionTag.BYTE_LENGTH);
            _replyTag = new SessionTag(rt);
        }
    }

    
    protected int calculateWrittenLength() {
        int totalLength = 0;
        totalLength += Hash.HASH_LENGTH*2; // key+fromHash
        totalLength += 1; // hasTunnel?
        if (_replyTunnel != null)
            totalLength += 4;
        totalLength += 2; // numPeers
        if (_dontIncludePeers != null) 
            totalLength += Hash.HASH_LENGTH * _dontIncludePeers.size();
        if (_replyKey != null)
            // number of tags, assume always 1 for now
            totalLength += SessionKey.KEYSIZE_BYTES + 1 + SessionTag.BYTE_LENGTH;
        return totalLength;
    }
    
    protected int writeMessageBody(byte out[], int curIndex) throws I2NPMessageException {
        if (_key == null) throw new I2NPMessageException("Key being searched for not specified");
        if (_fromHash == null) throw new I2NPMessageException("From address not specified");

        System.arraycopy(_key.getData(), 0, out, curIndex, Hash.HASH_LENGTH);
        curIndex += Hash.HASH_LENGTH;
        System.arraycopy(_fromHash.getData(), 0, out, curIndex, Hash.HASH_LENGTH);
        curIndex += Hash.HASH_LENGTH;
        // Generate the flag byte
        byte flag;
        if (_replyKey != null)
            flag = FLAG_ENCRYPT;
        else
            flag = 0;
        switch (_type) {
            case LS:
                flag |= FLAG_TYPE_LS;
                break;
            case RI:
                flag |= FLAG_TYPE_RI;
                break;
            case EXPL:
                flag |= FLAG_TYPE_EXPL;
                break;
            case ANY:
            default:
                // lookup type bits are 0
                break;
        }
        if (_replyTunnel != null) {
            flag |= FLAG_TUNNEL;
            out[curIndex++] = flag;
            DataHelper.toLong(out, curIndex, 4, _replyTunnel.getTunnelId());
            curIndex += 4;
        } else {
            out[curIndex++] = flag;
        }
        if ( (_dontIncludePeers == null) || (_dontIncludePeers.isEmpty()) ) {
            out[curIndex++] = 0x0;
            out[curIndex++] = 0x0;
        } else {
            int size = _dontIncludePeers.size();
            if (size > MAX_NUM_PEERS)
                throw new I2NPMessageException("Too many peers: " + size);
            DataHelper.toLong(out, curIndex, 2, size);
            curIndex += 2;
            for (Hash peer : _dontIncludePeers) {
                System.arraycopy(peer.getData(), 0, out, curIndex, Hash.HASH_LENGTH);
                curIndex += Hash.HASH_LENGTH;
            }
        }
        if (_replyKey != null) {
            System.arraycopy(_replyKey.getData(), 0, out, curIndex, SessionKey.KEYSIZE_BYTES);
            curIndex += SessionKey.KEYSIZE_BYTES;
            // number of tags, always 1 for now
            out[curIndex++] = 1;
            System.arraycopy(_replyTag.getData(), 0, out, curIndex, SessionTag.BYTE_LENGTH);
            curIndex += SessionTag.BYTE_LENGTH;
        }
        return curIndex;
    }
    
    public int getType() { return MESSAGE_TYPE; }
    
    @Override
    public int hashCode() {
        return DataHelper.hashCode(_key) +
               DataHelper.hashCode(_fromHash) +
               DataHelper.hashCode(_replyTunnel) +
               DataHelper.hashCode(_dontIncludePeers);
    }
    
    @Override
    public boolean equals(Object object) {
        if ( (object != null) && (object instanceof DatabaseLookupMessage) ) {
            DatabaseLookupMessage msg = (DatabaseLookupMessage)object;
            return DataHelper.eq(_key, msg._key) &&
                   DataHelper.eq(_fromHash, msg._fromHash) &&
                   DataHelper.eq(_replyTunnel, msg._replyTunnel) &&
                   DataHelper.eq(_dontIncludePeers, msg._dontIncludePeers);
        } else {
            return false;
        }
    }
    
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder(256);
        buf.append("[DatabaseLookupMessage: ");
        buf.append("\n\tSearch Type: ").append(_type);
        buf.append("\n\tSearch Key: ").append(_key);
        if (_replyKey != null)
            buf.append("\n\tReply GW: ");
        else
            buf.append("\n\tFrom: ");
        buf.append(_fromHash);
        buf.append("\n\tReply Tunnel: ").append(_replyTunnel);
        if (_replyKey != null)
            buf.append("\n\tReply Key: ").append(_replyKey);
        if (_replyTag != null)
            buf.append("\n\tReply Tag: ").append(_replyTag);
        if (_dontIncludePeers != null) {
            buf.append("\n\tDon't Include Peers: ");
            buf.append(_dontIncludePeers.size());
        }
        buf.append("]");
        return buf.toString();
    }
}
