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.Properties;

import net.i2p.I2PAppContext;
import net.i2p.data.Certificate;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.data.SessionKey;
import net.i2p.data.SessionTag;
import net.i2p.data.TunnelId;
import net.i2p.util.Log;

/**
 * Defines the message sent to a router to request that it participate in a
 * tunnel using the included configuration settings.
 *
 */
public class TunnelCreateMessage extends I2NPMessageImpl {
    private Log _log;
    public final static int MESSAGE_TYPE = 6;
    private Hash _nextRouter;
    private TunnelId _nextTunnelId;
    private int _durationSeconds;
    private SessionKey _layerKey;
    private SessionKey _ivKey;
    private Properties _options;
    private Hash _replyGateway;
    private TunnelId _replyTunnel;
    private SessionTag _replyTag;
    private SessionKey _replyKey;
    private boolean _isGateway;
    private long _nonce;
    private Certificate _certificate;
    
    private byte[] _optionsCache;
    private byte[] _certificateCache;
    
    public static final long MAX_NONCE_VALUE = ((1l << 32l) - 1l);
    
    private static final Hash INVALID_HASH = new Hash(new byte[Hash.HASH_LENGTH]); // all 0s

    public TunnelCreateMessage(I2PAppContext context) {
        super(context);
        _log = context.logManager().getLog(TunnelCreateMessage.class);
    }
    
    public void setNextRouter(Hash routerIdentityHash) { _nextRouter = routerIdentityHash; }
    public Hash getNextRouter() { return _nextRouter; }
    public void setNextTunnelId(TunnelId id) { _nextTunnelId = id; }
    public TunnelId getNextTunnelId() { return _nextTunnelId; }
    public void setDurationSeconds(int seconds) { _durationSeconds = seconds; }
    public int getDurationSeconds() { return _durationSeconds; }
    public void setLayerKey(SessionKey key) { _layerKey = key; }
    public SessionKey getLayerKey() { return _layerKey; }
    public void setIVKey(SessionKey key) { _ivKey = key; }
    public SessionKey getIVKey() { return _ivKey; }
    public void setCertificate(Certificate cert) { _certificate = cert; }
    public Certificate getCertificate() { return _certificate; }
    public void setReplyTag(SessionTag tag) { _replyTag = tag; }
    public SessionTag getReplyTag() { return _replyTag; }
    public void setReplyKey(SessionKey key) { _replyKey = key; }
    public SessionKey getReplyKey() { return _replyKey; }
    public void setReplyTunnel(TunnelId id) { _replyTunnel = id; }
    public TunnelId getReplyTunnel() { return _replyTunnel; }
    public void setReplyGateway(Hash peer) { _replyGateway = peer; }
    public Hash getReplyGateway() { return _replyGateway; }
    public void setNonce(long nonce) { _nonce = nonce; }
    public long getNonce() { return _nonce; }
    public void setIsGateway(boolean isGateway) { _isGateway = isGateway; }
    public boolean getIsGateway() { return _isGateway; }
    public Properties getOptions() { return _options; }
    public void setOptions(Properties opts) { _options = opts; }
    
    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");
        
        if (DataHelper.eq(INVALID_HASH.getData(), 0, data, offset, Hash.HASH_LENGTH)) {
            _nextRouter = null;
        } else {
            _nextRouter = new Hash(new byte[Hash.HASH_LENGTH]);
            System.arraycopy(data, offset, _nextRouter.getData(), 0, Hash.HASH_LENGTH);
        }
        offset += Hash.HASH_LENGTH;
        
        long id = DataHelper.fromLong(data, offset, 4);
        if (id > 0)
            _nextTunnelId = new TunnelId(id);
        offset += 4;
        
        _durationSeconds = (int)DataHelper.fromLong(data, offset, 2);
        offset += 2;
        
        _layerKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
        System.arraycopy(data, offset, _layerKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
        offset += SessionKey.KEYSIZE_BYTES;
        
        _ivKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
        System.arraycopy(data, offset, _ivKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
        offset += SessionKey.KEYSIZE_BYTES;

        try {
            Properties opts = new Properties();
            _options = opts;
            offset = DataHelper.fromProperties(data, offset, opts);
        } catch (DataFormatException dfe) {
            throw new I2NPMessageException("Error reading the options", dfe);
        }
        
        _replyGateway = new Hash(new byte[Hash.HASH_LENGTH]);
        System.arraycopy(data, offset, _replyGateway.getData(), 0, Hash.HASH_LENGTH);
        offset += Hash.HASH_LENGTH;
        
        _replyTunnel = new TunnelId(DataHelper.fromLong(data, offset, 4));
        offset += 4;
        
        _replyTag = new SessionTag(new byte[SessionTag.BYTE_LENGTH]);
        System.arraycopy(data, offset, _replyTag.getData(), 0, SessionTag.BYTE_LENGTH);
        offset += SessionTag.BYTE_LENGTH;
        
        _replyKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
        System.arraycopy(data, offset, _replyKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
        offset += SessionKey.KEYSIZE_BYTES;
        
        _nonce = DataHelper.fromLong(data, offset, 4);
        offset += 4;
        
        try {
            Certificate cert = new Certificate();
            _certificate = cert;
            offset += cert.readBytes(data, offset);
        } catch (DataFormatException dfe) {
            throw new I2NPMessageException("Error reading the certificate", dfe);
        }
        
        Boolean b = DataHelper.fromBoolean(data, offset);
        if (b == null)
            throw new I2NPMessageException("isGateway == unknown?!");
        _isGateway = b.booleanValue();
        offset += DataHelper.BOOLEAN_LENGTH;
    }
    
    
    /** calculate the message body's length (not including the header and footer */
    protected int calculateWrittenLength() { 
        int length = 0;
        length += Hash.HASH_LENGTH; // nextRouter
        length += 4; // nextTunnel
        length += 2; // duration
        length += SessionKey.KEYSIZE_BYTES; // layerKey
        length += SessionKey.KEYSIZE_BYTES; // ivKey
        
        if (_optionsCache == null) {
            try {
                _optionsCache = DataHelper.toProperties(_options);
                length += _optionsCache.length;
            } catch (DataFormatException dfe) {
            }
        }
        
        length += Hash.HASH_LENGTH; // replyGateway
        length += 4; // replyTunnel
        length += SessionTag.BYTE_LENGTH; // replyTag
        length += SessionKey.KEYSIZE_BYTES; // replyKey
        length += 4; // nonce
        if (_certificateCache == null)
            _certificateCache = _certificate.toByteArray();
        length += _certificateCache.length;
        length += DataHelper.BOOLEAN_LENGTH;
        return length;
    }
    
    /** write the message body to the output array, starting at the given index */
    protected int writeMessageBody(byte data[], int offset) throws I2NPMessageException {
        if (_nextRouter == null)
            System.arraycopy(INVALID_HASH.getData(), 0, data, offset, Hash.HASH_LENGTH);
        else
            System.arraycopy(_nextRouter.getData(), 0, data, offset, Hash.HASH_LENGTH);
        offset += Hash.HASH_LENGTH;
        
        if (_nextTunnelId == null)
            DataHelper.toLong(data, offset, 4, 0);
        else
            DataHelper.toLong(data, offset, 4, _nextTunnelId.getTunnelId());
        offset += 4;
        
        DataHelper.toLong(data, offset, 2, _durationSeconds);
        offset += 2;
        
        System.arraycopy(_layerKey.getData(), 0, data, offset, SessionKey.KEYSIZE_BYTES);
        offset += SessionKey.KEYSIZE_BYTES;
        
        System.arraycopy(_ivKey.getData(), 0, data, offset, SessionKey.KEYSIZE_BYTES);
        offset += SessionKey.KEYSIZE_BYTES;
        
        if (_optionsCache == null) {
            try {
                _optionsCache = DataHelper.toProperties(_options);
            } catch (DataFormatException dfe) {
                throw new I2NPMessageException("Error reading the options", dfe);
            }
        }
        System.arraycopy(_optionsCache, 0, data, offset, _optionsCache.length);
        offset += _optionsCache.length;
        
        System.arraycopy(_replyGateway.getData(), 0, data, offset, Hash.HASH_LENGTH);
        offset += Hash.HASH_LENGTH;
        
        DataHelper.toLong(data, offset, 4, _replyTunnel.getTunnelId());
        offset += 4;
        
        System.arraycopy(_replyTag.getData(), 0, data, offset, SessionTag.BYTE_LENGTH);
        offset += SessionTag.BYTE_LENGTH;
        
        System.arraycopy(_replyKey.getData(), 0, data, offset, SessionKey.KEYSIZE_BYTES);
        offset += SessionKey.KEYSIZE_BYTES;
        
        DataHelper.toLong(data, offset, 4, _nonce);
        offset += 4;
        
        if (_certificateCache == null)
            _certificateCache = _certificate.toByteArray();
        System.arraycopy(_certificateCache, 0, data, offset, _certificateCache.length);
        offset += _certificateCache.length;
        
        DataHelper.toBoolean(data, offset, _isGateway);
        offset += DataHelper.BOOLEAN_LENGTH;
        
        return offset;
    }
    
    
    @Override
    public byte[] toByteArray() {
        byte rv[] = super.toByteArray();
        if (rv == null)
            throw new RuntimeException("unable to toByteArray(): " + toString());
        return rv;
    }

    @Override
    public int hashCode() {
        return DataHelper.hashCode(getNextRouter()) +
               DataHelper.hashCode(getNextTunnelId()) +
               DataHelper.hashCode(getReplyGateway()) +
               DataHelper.hashCode(getReplyTunnel());
    }
    
    @Override
    public boolean equals(Object object) {
        if ( (object != null) && (object instanceof TunnelCreateMessage) ) {
            TunnelCreateMessage msg = (TunnelCreateMessage)object;
            return DataHelper.eq(getNextRouter(), msg.getNextRouter()) &&
                   DataHelper.eq(getNextTunnelId(), msg.getNextTunnelId()) &&
                   DataHelper.eq(getReplyGateway(), msg.getReplyGateway()) &&
                   DataHelper.eq(getReplyTunnel(), msg.getReplyTunnel());
        } else {
            return false;
        }
    }
    
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append("[TunnelCreateMessage: ");
        buf.append("\n\tNext Router: ").append(getNextRouter());
        buf.append("\n\tNext Tunnel: ").append(getNextTunnelId());
        buf.append("\n\tReply Tunnel: ").append(getReplyTunnel());
        buf.append("\n\tReply Peer: ").append(getReplyGateway());
        buf.append("]");
        return buf.toString();
    }

    public int getType() { return MESSAGE_TYPE; }    
}
