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

import com.google.common.base.Joiner;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueFactory;
import org.ethereum.config.SystemProperties;
import org.ethereum.crypto.ECKey;
import org.ethereum.facade.EthereumFactory;
import org.ethereum.net.rlpx.discover.NodeManager;
import org.ethereum.net.rlpx.discover.table.NodeEntry;
import org.ethereum.net.server.Channel;
import org.ethereum.net.server.ChannelManager;
import org.spongycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

/**
 * Testing peers discovery.
 *
 * The sample creates a small private net with three peers:
 *  - first is point for discovery;
 *  - two other ones will connect to first.
 *
 * Peers run on same IP 127.0.0.1 and are different by ports.
 *
 * After some time all peers should find each other. We use `peer.discovery.ip.list` config
 * option to point to discovery peer.
 *
 * Created by Stan Reshetnyk on 06.10.2016.
 */
public class PrivateNetworkDiscoverySample {

    /**
     *  Creating 3 instances with different config classes
     */
    public static void main(String[] args) throws Exception {
        BasicSample.sLogger.info("Starting main node to which others will connect to");
        EthereumFactory.createEthereum(Node0Config.class);

        BasicSample.sLogger.info("Starting regular instance 1!");
        EthereumFactory.createEthereum(Node1Config.class);

        BasicSample.sLogger.info("Starting regular instance 2!");
        EthereumFactory.createEthereum(Node2Config.class);
    }


    /**
     * Spring configuration class for the Regular peer
     */
    private static class RegularConfig {

        private final String discoveryNode;

        private final int nodeIndex;

        public RegularConfig(int nodeIndex, String discoveryNode) {
            this.nodeIndex = nodeIndex;
            this.discoveryNode = discoveryNode;
        }

        @Bean
        public BasicSample node() {
            return new BasicSample("sampleNode-" + nodeIndex) {

                @Autowired
                ChannelManager channelManager;

                @Autowired
                NodeManager nodeManager;

                {
                    new Thread(() -> {
                        try {
                            Thread.sleep(5000);
                            while (true) {
                                if (logger != null) {
                                    Thread.sleep(15000);
                                    if (channelManager != null) {
                                        final Collection<Channel> activePeers = channelManager.getActivePeers();
                                        final ArrayList<String> ports = new ArrayList<>();
                                        for (Channel channel: activePeers) {
                                            ports.add(channel.getInetSocketAddress().getHostName() + ":" + channel.getInetSocketAddress().getPort());
                                        }

                                        final Collection<NodeEntry> nodes = nodeManager.getTable().getAllNodes();
                                        final ArrayList<String> nodesString = new ArrayList<>();
                                        for (NodeEntry node: nodes) {
                                            nodesString.add(node.getNode().getHost() + ":" + node.getNode().getPort() + "@" + node.getNode().getHexId().substring(0, 6) );
                                        }

                                        logger.info("channelManager.getActivePeers() " + activePeers.size() + " " + Joiner.on(", ").join(ports));
                                        logger.info("nodeManager.getTable().getAllNodes() " + nodesString.size() + " " + Joiner.on(", ").join(nodesString));
                                    } else {
                                        logger.info("Channel manager is null");
                                    }
                                } else {
                                    System.err.println("Logger is null for " + nodeIndex);
                                }
                            }
                        } catch (Exception e) {
                            logger.error("Error checking peers count: ", e);
                        }
                    }).start();
                }


                @Override
                public void onSyncDone() {
                    logger.info("onSyncDone");


                }
            };
        }

        /**
         * Instead of supplying properties via config file for the peer
         * we are substituting the corresponding bean which returns required
         * config for this instance.
         */
        @Bean
        public SystemProperties systemProperties() {
            return new SystemProperties(getConfig(nodeIndex, discoveryNode));
        }
    }

    public static class Node0Config extends RegularConfig {

        public Node0Config() {
            super(0, null);
        }

        @Bean
        public SystemProperties systemProperties() {
            return super.systemProperties();
        }

        @Bean
        public BasicSample node() {
            return super.node();
        }
    }

    private static class Node1Config extends RegularConfig {

        public Node1Config() {
            super(1, "127.0.0.1:20000");
        }

        @Bean
        public SystemProperties systemProperties() {
            return super.systemProperties();
        }

        @Bean
        public BasicSample node() {
            return super.node();
        }
    }

    private static class Node2Config extends RegularConfig{

        public Node2Config() {
            super(2, "127.0.0.1:20000");
        }

        @Bean
        public SystemProperties systemProperties() {
            return super.systemProperties();
        }

        @Bean
        public BasicSample node() {
            return super.node();
        }
    }

    private static Config getConfig(int index, String discoveryNode) {
        return ConfigFactory.empty()
                .withValue("peer.discovery.enabled", value(true))
                .withValue("peer.discovery.external.ip", value("127.0.0.1"))
                .withValue("peer.discovery.bind.ip", value("127.0.0.1"))
                .withValue("peer.discovery.persist", value("false"))

                .withValue("peer.listen.port", value(20000 + index))
                .withValue("peer.privateKey", value(Hex.toHexString(ECKey.fromPrivate(("" + index).getBytes()).getPrivKeyBytes())))
                .withValue("peer.networkId", value(555))
                .withValue("sync.enabled", value(true))
                .withValue("database.incompatibleDatabaseBehavior", value("RESET"))
                .withValue("genesis", value("sample-genesis.json"))
                .withValue("database.dir", value("sampleDB-" + index))
                .withValue("peer.discovery.ip.list", value(discoveryNode != null ? Arrays.asList(discoveryNode) : Arrays.asList()));
    }

    private static ConfigValue value(Object value) {
        return ConfigValueFactory.fromAnyRef(value);
    }
}
