/*
 * Copyright (c) [2016] [ <ether.camp> ]
 * This file is part of the ethereumJ library.
 *
 * The ethereumJ library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * The ethereumJ library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
 */
package org.ethereum.net.swarm.bzz;

import org.ethereum.net.client.Capability;
import org.ethereum.net.swarm.Key;
import org.ethereum.net.swarm.NetStore;
import org.ethereum.net.swarm.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.util.encoders.Hex;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;

import static java.lang.Math.min;

/**
 * The class is the lowest level right above the network layer.
 * Responsible for BZZ handshaking, brokering inbound messages
 * and delivering outbound messages.
 *
 * Created by Anton Nashatyrev on 18.06.2015.
 */
public class BzzProtocol implements Consumer<BzzMessage> {
    private final static Logger LOG = LoggerFactory.getLogger("net.bzz");

    private final static AtomicLong idGenerator = new AtomicLong(0);

    public final static int Version            = 0;
    public final static long ProtocolLength     = 8;
    public final static long ProtocolMaxMsgSize = 10 * 1024 * 1024;
    public final static int NetworkId          = 0;
    public final static int Strategy           = 0;

    private NetStore netStore;
    private Consumer<BzzMessage> messageSender;
    private PeerAddress node;

    private boolean handshaken = false;
    private boolean handshakeOut = false;
    private List<BzzMessage> pendingHandshakeOutMessages = new ArrayList<>();
    private List<BzzMessage> pendingHandshakeInMessages = new ArrayList<>();

    public BzzProtocol(NetStore netStore) {
        this.netStore = netStore;
    }

    /**
     * Installs the message sender.
     * Normally this is BzzHandler which just sends the message to the peer over the wire
     * In the testing environment this could be a special handler which delivers the message
     * without network stack
     */
    public void setMessageSender(Consumer<BzzMessage> messageSender) {
        this.messageSender = messageSender;
    }

    public void start() {
        handshakeOut();
    }

    /**
     * Gets the address of the Peer connected to this instance.
     */
    public PeerAddress getNode() {
        return node;
    }

    /**
     * Sends the Status message to the peer
     */
    private void handshakeOut() {
        if (!handshakeOut) {
            handshakeOut = true;
            BzzStatusMessage outStatus = new BzzStatusMessage(Version, "honey",
                    netStore.getSelfAddress(), NetworkId,
                    Collections.singletonList(new Capability(Capability.BZZ, (byte) 0)));
            LOG.info("Outbound handshake: " + outStatus);
            sendMessageImpl(outStatus);
        }
    }

    /**
     * Handles inbound Status Message
     */
    private void handshakeIn(BzzStatusMessage msg) {
        if (!handshaken) {
            LOG.info("Inbound handshake: " + msg);
            netStore.statHandshakes.add(1);
            // TODO check status parameters
            node = msg.getAddr();
            netStore.getHive().addPeer(this);

            handshaken = true;
            handshakeOut();

            start();

            if (!pendingHandshakeOutMessages.isEmpty()) {
                LOG.info("Send pending handshake messages: " + pendingHandshakeOutMessages.size());
                for (BzzMessage pmsg : pendingHandshakeOutMessages) {
                    sendMessageImpl(pmsg);
                }
            }
            pendingHandshakeOutMessages = null;

            // ping the peer for self neighbours
            sendMessageImpl(new BzzRetrieveReqMessage(Key.zeroKey()));

            if (!pendingHandshakeInMessages.isEmpty()) {
                LOG.info("Processing pending handshake inbound messages: " + pendingHandshakeInMessages.size());
                for (BzzMessage pmsg : pendingHandshakeInMessages) {
                    handleMsg(pmsg);
                }
            }
            pendingHandshakeInMessages = null;

        } else {
            LOG.warn("Double inbound status message (ignore): " + msg);
        }
    }

    public synchronized void sendMessage(BzzMessage msg) {
        if (handshaken) {
            sendMessageImpl(msg);
        } else {
            pendingHandshakeOutMessages.add(msg);
        }
    }
    private void sendMessageImpl(BzzMessage msg) {
        netStore.statOutMsg.add(1);
        msg.setId(idGenerator.incrementAndGet());
        LOG.debug("<===   (to " + addressToShortString(getNode()) + ") " + msg);
        messageSender.accept(msg);
    }

    @Override
    public void accept(BzzMessage bzzMessage) {
        handleMsg(bzzMessage);
    }

    private void handleMsg(BzzMessage msg) {
        synchronized (netStore) {
            netStore.statInMsg.add(1);
            msg.setPeer(this);
            if (LOG.isDebugEnabled()) {
                LOG.debug(" ===>(from " + addressToShortString(getNode()) + ") " + msg);
            }
            if (msg.getCommand() == BzzMessageCodes.STATUS) {
                handshakeIn((BzzStatusMessage) msg);
            } else {
                if (!handshaken) {
                    pendingHandshakeInMessages.add(msg);
                } else {
                    switch (msg.getCommand()) {
                        case STORE_REQUEST:
                            netStore.addStoreRequest((BzzStoreReqMessage) msg);
                            break;
                        case RETRIEVE_REQUEST:
                            netStore.addRetrieveRequest((BzzRetrieveReqMessage) msg);
                            break;
                        case PEERS:
                            netStore.getHive().addPeerRecords((BzzPeersMessage) msg);
                            break;
                        default:
                            LOG.error("Invalid BZZ command: " + msg.getCommand() + ": " + msg);
                    }
                }
            }
        }
    }

    public static String addressToShortString(PeerAddress addr) {
        if (addr == null) return "<null>";
        String s = Hex.toHexString(addr.getId());
        s = s.substring(0, min(8, s.length()));
        return s + "@" + Util.ipBytesToString(addr.getIp()) + ":" + addr.getPort();
    }

    @Override
    public String toString() {
        return "BzzProtocol[" + netStore.getSelfAddress() + " => " + node + "]";
    }
}
