/*
 * 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;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.Channel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;
import org.apache.commons.lang3.RandomStringUtils;
import org.ethereum.crypto.ECKey;
import org.ethereum.net.rlpx.FindNodeMessage;
import org.ethereum.net.rlpx.Message;
import org.ethereum.net.rlpx.discover.DiscoveryEvent;
import org.ethereum.net.rlpx.discover.PacketDecoder;
import org.junit.Ignore;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import static org.ethereum.util.ByteUtil.longToBytesNoLeadZeroes;

/**
 * Not for regular run, but just for testing UDP client/server communication
 *
 * For running server with gradle:
 * - adjust constants
 * - remove @Ignore from server() method
 * - > ./gradlew -Dtest.single=UdpTest test
 *
 * Created by Anton Nashatyrev on 28.12.2016.
 */
public class UdpTest {

    private static final String clientAddr = bindIp();
    private static final int clientPort = 8888;
    private static final String serverAddr = bindIp();
    private static final int serverPort = 30321;

    private static final String privKeyStr = "abb51256c1324a1350598653f46aa3ad693ac3cf5d05f36eba3f495a1f51590f";
    private static final ECKey privKey = ECKey.fromPrivate(Hex.decode(privKeyStr));
    private static final int MAX_LENGTH = 4096;

    private final SimpleNodeManager nodeManager = new SimpleNodeManager();

    private class SimpleMessageHandler extends SimpleChannelInboundHandler<DiscoveryEvent>
            implements Consumer<DiscoveryEvent> {

        Channel channel;

        SimpleNodeManager nodeManager;

        public SimpleMessageHandler(Channel channel, SimpleNodeManager nodeManager) {
            this.channel = channel;
            this.nodeManager = nodeManager;
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            InetSocketAddress localAddress = (InetSocketAddress) ctx.channel().localAddress();
            System.out.printf("Channel initialized on %s:%s%n", localAddress.getHostString(), localAddress.getPort());
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, DiscoveryEvent msg) throws Exception {
            InetSocketAddress localAddress = (InetSocketAddress) ctx.channel().localAddress();
            System.out.printf("Message received on %s:%s%n", localAddress.getHostString(), localAddress.getPort());
            nodeManager.handleInbound(msg);
        }

        @Override
        public void accept(DiscoveryEvent discoveryEvent) {
            InetSocketAddress address = discoveryEvent.getAddress();
            sendPacket(discoveryEvent.getMessage().getPacket(), address);
        }

        private void sendPacket(byte[] payload, InetSocketAddress address) {
            DatagramPacket packet = new DatagramPacket(Unpooled.copiedBuffer(payload), address);
            System.out.println("Sending message from " + clientAddr + ":" + clientPort +
                    " to " + address.getHostString() + ":" + address.getPort());
            channel.writeAndFlush(packet);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
        }
    }

    private class SimpleNodeManager {

        SimpleMessageHandler messageSender;

        public void setMessageSender(SimpleMessageHandler messageSender) {
            this.messageSender = messageSender;
        }

        public SimpleMessageHandler getMessageSender() {
            return messageSender;
        }

        public void handleInbound(DiscoveryEvent discoveryEvent) {
            Message m = discoveryEvent.getMessage();
            if (!(m instanceof FindNodeMessage)) {
                return;
            }
            String msg = new String(((FindNodeMessage) m).getTarget());
            System.out.printf("Inbound message \"%s\" from %s:%s%n", msg,
                    discoveryEvent.getAddress().getHostString(), discoveryEvent.getAddress().getPort());
            if (msg.endsWith("+1")) {
                messageSender.channel.close();
            } else {
                FindNodeMessage newMsg = FindNodeMessage.create((msg + "+1").getBytes(), privKey);
                messageSender.sendPacket(newMsg.getPacket(), discoveryEvent.getAddress());
            }
        }
    }

    public Channel create(String bindAddr, int port) throws InterruptedException {
        NioEventLoopGroup group = new NioEventLoopGroup(1);

        Bootstrap b = new Bootstrap();
        b.group(group)
            .channel(NioDatagramChannel.class)
            .handler(new ChannelInitializer<NioDatagramChannel>() {
                @Override
                public void initChannel(NioDatagramChannel ch)
                        throws Exception {
                    ch.pipeline().addLast(new PacketDecoder());
                    SimpleMessageHandler messageHandler = new SimpleMessageHandler(ch, nodeManager);
                    nodeManager.setMessageSender(messageHandler);
                    ch.pipeline().addLast(messageHandler);
                }
            });

        return b.bind(bindAddr, port).sync().channel();
    }

    public void startServer() throws InterruptedException {
        create(serverAddr, serverPort).closeFuture().sync();
    }

    public void startClient()
            throws InterruptedException {
        String defaultMessage = RandomStringUtils.randomAlphanumeric(MAX_LENGTH);
        for (int i = defaultMessage.length() - 1; i >= 0 ; i--) {
            int sendAttempts = 0;
            boolean ok = false;
            while (sendAttempts < 3) {
                Channel channel = create(clientAddr, clientPort);
                String sendMessage = defaultMessage.substring(i, defaultMessage.length());
                FindNodeMessage msg = FindNodeMessage.create(sendMessage.getBytes(), privKey);
                System.out.printf("Sending message with string payload of size %s, packet size %s, attempt %s%n", sendMessage.length(), msg.getPacket().length, sendAttempts + 1);
                nodeManager.getMessageSender().sendPacket(msg.getPacket(), new InetSocketAddress(serverAddr, serverPort));
                ok = channel.closeFuture().await(1, TimeUnit.SECONDS);
                if (ok) break;
                sendAttempts++;
                channel.close().sync();
            }
            if (!ok) {
                System.out.println("ERROR: Timeout waiting for response after all attempts");
                assert false;
            } else {
                System.out.println("OK");
            }
        }
    }

    @Ignore
    @Test
    public void server() throws Exception {
        startServer();
    }

    @Ignore
    @Test
    public void client() throws Exception {
        startClient();
    }

    public static String bindIp() {
        String bindIp;
            try (Socket s = new Socket("www.google.com", 80)) {
                bindIp = s.getLocalAddress().getHostAddress();
                System.out.printf("UDP local bound to: %s%n", bindIp);
            } catch (IOException e) {
                System.out.printf("Can't get bind IP. Fall back to 0.0.0.0: " + e);
                bindIp = "0.0.0.0";
            }
        return bindIp;
    }
}
