package com.google.bitcoin.core;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Represents an "addr" message on the P2P network, which contains broadcast IP addresses of other peers. This is
 * one of the ways peers can find each other without using the DNS or IRC discovery mechansisms. However storing and
 * using addr messages is not presently implemented.
 */
public class AddressMessage extends Message {
    private static final long serialVersionUID = 8058283864924679460L;
    private static final long MAX_ADDRESSES = 1024;
    private List<PeerAddress> addresses;
    private transient long numAddresses = -1;

    /**
     * Contruct a new 'addr' message.
     * @param params NetworkParameters object.
     * @param offset The location of the first msg byte within the array.
     * @param parseLazy Whether to perform a full parse immediately or delay until a read is requested.
     * @param parseRetain Whether to retain the backing byte array for quick reserialization.  
     * If true and the backing byte array is invalidated due to modification of a field then 
     * the cached bytes may be repopulated and retained if the message is serialized again in the future.
     * @param length The length of message if known.  Usually this is provided when deserializing of the wire
     * as the length will be provided as part of the header.  If unknown then set to Message.UNKNOWN_LENGTH
     * @throws ProtocolException
     */
    AddressMessage(NetworkParameters params, byte[] payload, int offset, boolean parseLazy, boolean parseRetain, int length) throws ProtocolException {
        super(params, payload, offset, parseLazy, parseRetain, length);
    }

    /**
     * Contruct a new 'addr' message.
     * @param params NetworkParameters object.
     * @param parseLazy Whether to perform a full parse immediately or delay until a read is requested.
     * @param parseRetain Whether to retain the backing byte array for quick reserialization.  
     * If true and the backing byte array is invalidated due to modification of a field then 
     * the cached bytes may be repopulated and retained if the message is serialized again in the future.
     * @param length The length of message if known.  Usually this is provided when deserializing of the wire
     * as the length will be provided as part of the header.  If unknown then set to Message.UNKNOWN_LENGTH
     * @throws ProtocolException
     */
    AddressMessage(NetworkParameters params, byte[] payload, boolean parseLazy, boolean parseRetain, int length) throws ProtocolException {
        super(params, payload, 0, parseLazy, parseRetain, length);
    }

    AddressMessage(NetworkParameters params, byte[] payload, int offset) throws ProtocolException {
        super(params, payload, offset, false, false, UNKNOWN_LENGTH);
    }

    AddressMessage(NetworkParameters params, byte[] payload) throws ProtocolException {
        super(params, payload, 0, false, false, UNKNOWN_LENGTH);
    }

    @Override
    protected void parseLite() throws ProtocolException {
    }

    @Override
    void parse() throws ProtocolException {
        numAddresses = readVarInt();
        // Guard against ultra large messages that will crash us.
        if (numAddresses > MAX_ADDRESSES)
            throw new ProtocolException("Address message too large.");
        addresses = new ArrayList<PeerAddress>((int) numAddresses);
        for (int i = 0; i < numAddresses; i++) {
            PeerAddress addr = new PeerAddress(params, bytes, cursor, protocolVersion, this, parseLazy, parseRetain);
            addresses.add(addr);
            cursor += addr.getMessageSize();
        }
        length = cursor - offset;
    }

    /* (non-Javadoc)
      * @see Message#bitcoinSerializeToStream(java.io.OutputStream)
      */
    @Override
    void bitcoinSerializeToStream(OutputStream stream) throws IOException {
        if (addresses == null)
            return;
        stream.write(new VarInt(addresses.size()).encode());
        for (PeerAddress addr : addresses) {
            addr.bitcoinSerialize(stream);
        }
    }

    public int getMessageSize() {
        if (length != UNKNOWN_LENGTH)
            return length;
        if (addresses != null) {
            length = new VarInt(addresses.size()).getSizeInBytes();
            // The 4 byte difference is the uint32 timestamp that was introduced in version 31402
            length += addresses.size() * (protocolVersion > 31402 ? PeerAddress.MESSAGE_SIZE : PeerAddress.MESSAGE_SIZE - 4);
        }
        return length;
    }

    /**
     * AddressMessage cannot cache checksum in non-retain mode due to dynamic time being used.
     */
    @Override
    void setChecksum(byte[] checksum) {
        if (parseRetain)
            super.setChecksum(checksum);
        else
            this.checksum = null;
    }

    /**
     * @return An unmodifiableList view of the backing List of addresses.  Addresses contained within the list may be safely modified.
     */
    public List<PeerAddress> getAddresses() {
        maybeParse();
        return Collections.unmodifiableList(addresses);
    }

    public void addAddress(PeerAddress address) {
        unCache();
        maybeParse();
        address.setParent(this);
        addresses.add(address);
        if (length == UNKNOWN_LENGTH)
            getMessageSize();
        else
            length += address.getMessageSize();
    }

    public void removeAddress(int index) {
        unCache();
        PeerAddress address = addresses.remove(index);
        address.setParent(null);
        if (length == UNKNOWN_LENGTH)
            getMessageSize();
        else
            length -= address.getMessageSize();
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("addr: ");
        for (PeerAddress a : addresses) {
            builder.append(a.toString());
            builder.append(" ");
        }
        return builder.toString();
    }

}
