/*
 * 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 com.typesafe.config.ConfigFactory;
import org.ethereum.config.NoAutoscan;
import org.ethereum.config.SystemProperties;
import org.ethereum.core.*;
import org.ethereum.crypto.ECKey;
import org.ethereum.facade.Ethereum;
import org.ethereum.facade.EthereumFactory;
import org.ethereum.listener.EthereumListenerAdapter;
import org.ethereum.mine.Ethash;
import org.ethereum.net.eth.handler.Eth62;
import org.ethereum.net.eth.message.*;
import org.ethereum.net.server.Channel;
import org.ethereum.util.RLP;
import org.junit.Ignore;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static java.lang.Math.max;
import static java.lang.Math.min;
import static org.ethereum.crypto.HashUtil.sha3;

/**
 * Created by Anton Nashatyrev on 13.10.2015.
 */
@Ignore
public class TwoPeerTest {

    @Configuration
    @NoAutoscan
    public static class SysPropConfig1 {
        static Eth62 testHandler = null;
        @Bean
        @Scope("prototype")
        public Eth62 eth62() {
            return testHandler;
//            return new Eth62();
        }

        static SystemProperties props = new SystemProperties();
        @Bean
        public SystemProperties systemProperties() {
            return props;
        }
    }
    @Configuration
    @NoAutoscan
    public static class SysPropConfig2 {
        static SystemProperties props= new SystemProperties();
        @Bean
        public SystemProperties systemProperties() {
            return props;
        }

    }

    public Block createNextBlock(Block parent, String stateRoot, String extraData) {
        Block b = new Block(parent.getHash(), sha3(RLP.encodeList()), parent.getCoinbase(), parent.getLogBloom(),
                parent.getDifficulty(), parent.getNumber() + 1, parent.getGasLimit(), parent.getGasUsed(),
                System.currentTimeMillis() / 1000, new byte[0], new byte[0], new byte[0],
                parent.getReceiptsRoot(), parent.getTxTrieRoot(),
                Hex.decode(stateRoot),
//                    Hex.decode("7c22bebbe3e6cf5af810bef35ad7a7b8172e0a247eaeb44f63fffbce87285a7a"),
                Collections.<Transaction>emptyList(), Collections.<BlockHeader>emptyList());
        //        b.getHeader().setDifficulty(b.getHeader().calcDifficulty(bestBlock.getHeader()).toByteArray());
        if (extraData != null) {
            b.getHeader().setExtraData(extraData.getBytes());
        }
        return b;
    }

    public Block addNextBlock(BlockchainImpl blockchain1, Block parent, String extraData) {
        Block b = createNextBlock(parent, "00", extraData);
        System.out.println("Adding block.");
//        blockchain1.add(b, new Miner() {
//            @Override
//            public long mine(BlockHeader header) {
//                return Ethash.getForBlock(header.getNumber()).mineLight(header);
//            }
//
//            @Override
//            public boolean validate(BlockHeader header) {
//                return true;
//            }
//        });
        return b;
    }

    @Test
    public void testTest() throws FileNotFoundException, InterruptedException {
        SysPropConfig1.props.overrideParams(
                "peer.listen.port", "30334",
                "peer.privateKey", "3ec771c31cac8c0dba77a69e503765701d3c2bb62435888d4ffa38fed60c445c",
                // nodeId: 3973cb86d7bef9c96e5d589601d788370f9e24670dcba0480c0b3b1b0647d13d0f0fffed115dd2d4b5ca1929287839dcd4e77bdc724302b44ae48622a8766ee6
                "genesis", "genesis-light.json",
                "database.dir", "testDB-1");

        SysPropConfig2.props.overrideParams(ConfigFactory.parseString(
                "peer.listen.port = 30335 \n" +
                        "peer.privateKey = 6ef8da380c27cea8fdf7448340ea99e8e2268fc2950d79ed47cbf6f85dc977ec \n" +
                        "peer.active = [{ url = \"enode://3973cb86d7bef9c96e5d589601d788370f9e24670dcba0480c0b3b1b0647d13d0f0fffed115dd2d4b5ca1929287839dcd4e77bdc724302b44ae48622a8766ee6@localhost:30334\" }] \n" +
                        "sync.enabled = true \n" +
                        "genesis = genesis-light.json \n" +
                        "database.dir = testDB-2 \n"));

        final List<Block> alternativeFork = new ArrayList<>();
        SysPropConfig1.testHandler = new Eth62() {
            @Override
            protected void processGetBlockHeaders(GetBlockHeadersMessage msg) {
                if (msg.getBlockHash() != null) {
                    System.out.println("=== (1)");
                    for (int i = 0; i < alternativeFork.size(); i++) {
                        if (Arrays.equals(msg.getBlockHash(), alternativeFork.get(i).getHash())) {
                            System.out.println("=== (2)");
                            int endIdx = max(0, i - msg.getSkipBlocks());
                            int startIdx = max(0, i - msg.getMaxHeaders());
                            if (!msg.isReverse()) {
                                startIdx = min(alternativeFork.size() - 1, i + msg.getSkipBlocks());
                                endIdx = min(alternativeFork.size() - 1, i + msg.getMaxHeaders());
                            }

                            List<BlockHeader> headers = new ArrayList<>();
                            for (int j = startIdx; j <= endIdx; j++) {
                                headers.add(alternativeFork.get(j).getHeader());
                            }

                            if (msg.isReverse()) {
                                Collections.reverse(headers);
                            }

                            sendMessage(new BlockHeadersMessage(headers));

                            return;
                        }
                    }

                }
                super.processGetBlockHeaders(msg);
            }

            @Override
            protected void processGetBlockBodies(GetBlockBodiesMessage msg) {
                List<byte[]> bodies = new ArrayList<>(msg.getBlockHashes().size());

                for (byte[] hash : msg.getBlockHashes()) {
                    Block block = null;
                    for (Block b : alternativeFork) {
                        if (Arrays.equals(b.getHash(), hash)) {
                            block = b;
                            break;
                        }
                    }
                    if (block == null) {
                        block = blockchain.getBlockByHash(hash);
                    }
                    bodies.add(block.getEncodedBody());
                }

                sendMessage(new BlockBodiesMessage(bodies));
            }
        };

        Ethereum ethereum1 = EthereumFactory.createEthereum(SysPropConfig1.props, SysPropConfig1.class);
        BlockchainImpl blockchain = (BlockchainImpl) ethereum1.getBlockchain();
        Block bGen = blockchain.getBestBlock();
        Block b1 = addNextBlock(blockchain, bGen, "chain A");
        Block b2 = addNextBlock(blockchain, b1, null);
        Block b3 = addNextBlock(blockchain, b2, null);
        Block b4 = addNextBlock(blockchain, b3, null);

        List<BlockHeader> listOfHeadersStartFrom = blockchain.getListOfHeadersStartFrom(new BlockIdentifier(null, 3), 0, 100, true);

//        Block b1b = addNextBlock(blockchain, bGen, "chain B");
        Block b1b = createNextBlock(bGen, "7c22bebbe3e6cf5af810bef35ad7a7b8172e0a247eaeb44f63fffbce87285a7a", "chain B");
        Ethash.getForBlock(SystemProperties.getDefault(), b1b.getNumber()).mineLight(b1b);
        Block b2b = createNextBlock(b1b, Hex.toHexString(b2.getStateRoot()), "chain B");
        Ethash.getForBlock(SystemProperties.getDefault(), b2b.getNumber()).mineLight(b2b);

        alternativeFork.add(bGen);
        alternativeFork.add(b1b);
        alternativeFork.add(b2b);

//        byte[] root = ((RepositoryImpl) ethereum.getRepository()).getRoot();
//        ((RepositoryImpl) ethereum.getRepository()).syncToRoot(root);
//        byte[] root1 = ((RepositoryImpl) ethereum.getRepository()).getRoot();
//        Block b2b = addNextBlock(blockchain, b1, "chain B");

        System.out.println("Blocks added");

        Ethereum ethereum2 = EthereumFactory.createEthereum(SysPropConfig2.props, SysPropConfig2.class);

        final CountDownLatch semaphore = new CountDownLatch(1);

        final Channel[] channel1 = new Channel[1];
        ethereum1.addListener(new EthereumListenerAdapter() {
            @Override
            public void onEthStatusUpdated(Channel channel, StatusMessage statusMessage) {
                channel1[0] = channel;
                System.out.println("==== Got the Channel: " + channel);
            }
        });
        ethereum2.addListener(new EthereumListenerAdapter() {
            @Override
            public void onBlock(Block block, List<TransactionReceipt> receipts) {
                if (block.getNumber() == 4) {
                    semaphore.countDown();
                }
            }

        });

        System.out.println("======= Waiting for block #4");
        semaphore.await(60, TimeUnit.SECONDS);
        if(semaphore.getCount() > 0) {
            throw new RuntimeException("4 blocks were not imported.");
        }

        System.out.println("======= Sending forked block without parent...");
//        ((EthHandler) channel1[0].getEthHandler()).sendNewBlock(b2b);

//        Block b = b4;
//        for (int i = 0; i < 10; i++) {
//            Thread.sleep(3000);
//            System.out.println("=====  Adding next block...");
//            b = addNextBlock(blockchain, b, null);
//        }

        Thread.sleep(10000000);


        ethereum1.close();
        ethereum2.close();

        System.out.println("Passed.");

    }

    public static void main(String[] args) throws Exception {
        ECKey k = ECKey.fromPrivate(Hex.decode("6ef8da380c27cea8fdf7448340ea99e8e2268fc2950d79ed47cbf6f85dc977ec"));
        System.out.println(Hex.toHexString(k.getPrivKeyBytes()));
        System.out.println(Hex.toHexString(k.getAddress()));
        System.out.println(Hex.toHexString(k.getNodeId()));
    }
}
