package main

import (
	"context"
	"math/big"
	"time"
	"os"
	"bytes"

	"github.com/openrelayxyz/plugeth-utils/core"
	"github.com/openrelayxyz/plugeth-utils/restricted/hexutil"
	"github.com/openrelayxyz/plugeth-utils/restricted/crypto"
)

var hookChan chan map[string]struct{} = make(chan map[string]struct{}, 10)
var quit chan string = make(chan string)

func (service *engineService) CaptureShutdown(ctx context.Context) {
	m := map[string]struct{}{
		"OnShutdown":struct{}{},
	}
	hookChan <- m
}

func (service *engineService) CapturePreTrieCommit(ctx context.Context) {
	m := map[string]struct{}{
		"PreTrieCommit":struct{}{},
	}
	hookChan <- m
}

func (service *engineService) CapturePostTrieCommit(ctx context.Context) {
	m := map[string]struct{}{
		"PostTrieCommit":struct{}{},
	}
	hookChan <- m
}

func BlockChain() {

	go func () {
		for {
			select {
				case <- quit:
					if len(plugins) > 0 {
						log.Error("Exit with Error, Plugins map not empty", "Plugins not called", plugins)
						os.Exit(1)
					} else {
						log.Info("Exit without error")
						os.Exit(0)
					}
				case m := <- hookChan:
					var ok bool
					f := func(key string) bool {_, ok = m[key]; return ok}
					switch {
						case f("InitializeNode"):
							delete(plugins, "InitializeNode")
						case f("CreateEngine"):
							delete(plugins, "CreateEngine")
						case f("OnShutdown"):
							delete(plugins, "OnShutdown")
						case f("StateUpdate"):
							delete(plugins, "StateUpdate")
						case f("PreProcessBlock"):
							delete(plugins, "PreProcessBlock")
						case f("PreProcessTransaction"):
							delete(plugins, "PreProcessTransaction")
						case f("PostProcessTransaction"):
							delete(plugins, "PostProcessTransaction")
						case f("PostProcessBlock"):
							delete(plugins, "PostProcessBlock")
						case f("NewHead"):
							delete(plugins, "NewHead")
						case f("LivePreProcessBlock"):
							delete(plugins, "LivePreProcessBlock")
						case f("LivePreProcessTransaction"):
							delete(plugins, "LivePreProcessTransaction")
						case f("LivePostProcessTransaction"):
							delete(plugins, "LivePostProcessTransaction")
						case f("LivePostProcessBlock"):
							delete(plugins, "LivePostProcessBlock")
						case f("GetRPCCalls"):
							delete(plugins, "GetRPCCalls")
						case f("RPCSubscriptionTest"):
							delete(plugins, "RPCSubscriptionTest")
						case f("SetTrieFlushIntervalClone"):
							delete(plugins, "SetTrieFlushIntervalClone")
						case f("StandardCaptureStart"):
							delete(plugins, "StandardCaptureStart")
						case f("StandardCaptureState"):
							delete(plugins, "StandardCaptureState")
						case f("StandardCaptureFault"):
							delete(plugins, "StandardCaptureFault")
						case f("StandardCaptureEnter"):
							delete(plugins, "StandardCaptureEnter")
						case f("StandardCaptureExit"):
							delete(plugins, "StandardCaptureExit")
						case f("StandardCaptureEnd"):
							delete(plugins, "StandardCaptureEnd")
						case f("StandardTracerResult"):
							delete(plugins, "StandardTracerResult")
						case f("LivePreProcessBlock"):
							delete(plugins, "LivePreProcessBlock")
						case f("LiveCaptureStart"):
							delete(plugins, "LiveCaptureStart")
						case f("LiveCaptureState"):
							delete(plugins, "LiveCaptureState")
						case f("LiveCaptureEnd"):
							delete(plugins, "LiveCaptureEnd")
						case f("PreTrieCommit"):
							delete(plugins, "PreTrieCommit")
						case f("PostTrieCommit"):
							delete(plugins, "PostTrieCommit")
						// These methods are not covered by tests at this time
						// case f("LiveCaptureFault"):
						// 	delete(plugins, "LiveCaptureFault")
						// case f("LiveCaptureEnter"):
						// 	delete(plugins, "LiveCaptureEnter")
						// case f("LiveCaptureExit"):
						// 	delete(plugins, "LiveCaptureExit")
						// case f("LiveTracerResult"):
						// 	delete(plugins, "LiveTracerResult")
						case f("SetDefaultDataDir"):
							delete(plugins, "SetDefaultDataDir")
						case f("SetBootstrapNodes"):
							delete(plugins, "SetBootstrapNodes")
						case f("SetNetworkId"):
							delete(plugins, "SetNetworkId")		
						case f("SetETHDiscoveryURLs"):
							delete(plugins, "SetETHDiscoveryURLs")
						case f("SetSnapDiscoveryURLs"):
							delete(plugins, "SetSnapDiscoveryURLs")
						case f("ForkIDs"):
							delete(plugins, "ForkIDs")
						case f("OpCodeSelect"):
							delete(plugins, "OpCodeSelect")
						case f("Is1559"):
							delete(plugins, "Is1559")
						case f("PluginEIPCheck"):
							delete(plugins, "PluginEIPCheck")
				}
			}
		}
	}()
	
	pseudoCreateEngine()
	txFactory()
	txTracer()
}

var t0 core.Hash
var t1 core.Hash
var t2 core.Hash
var t3 core.Hash
var coinBase *core.Address

func txFactory() {

	cl := apis[0].Service.(*engineService).stack
	client, err := cl.Attach()
	if err != nil {
		log.Error("Error connecting with client txFactory", "err", err)
	}

	err = client.Call(&coinBase, "eth_coinbase")
	if err != nil {
		log.Error("failed to call eth_coinbase txFactory", "err", err)
	}

	var peerCount hexutil.Uint64
	for peerCount == 0 {
		err = client.Call(&peerCount, "net_peerCount")
		if err != nil {
			log.Error("failed to call net_peerCount", "err", err)
		}
		time.Sleep(100 * time.Millisecond)
	} 

	tx0_params := map[string]interface{}{
		"from": coinBase,
		"to": coinBase,
		"value": (*hexutil.Big)(big.NewInt(1)),
	}
	
	err = client.Call(&t0, "eth_sendTransaction", tx0_params)
	if err != nil {
		log.Error("transaction zero failed", "err", err)
	}

	tx1_params := map[string]interface{}{
		"input": "0x60018080600053f3",
		"from": coinBase,
	}

	time.Sleep(2 * time.Second)
	err = client.Call(&t1, "eth_sendTransaction", tx1_params)
	if err != nil {
		log.Error("transaction one failed", "err", err)
	}

	tx2_params := map[string]interface{}{
		"input": "0x61520873000000000000000000000000000000000000000060006000600060006000f1",
		"from": coinBase,
	}
	
	time.Sleep(2 * time.Second)
	err = client.Call(&t2, "eth_sendTransaction", tx2_params)
	if err != nil {
		log.Error("transaction two failed", "err", err)
	}
	
	genericArg := map[string]interface{}{
		"input": "0x608060405234801561001057600080fd5b5061011a806100206000396000f3fe608060405234801561001057600080fd5b50600436106100375760003560e01c806360fe47b11461003c5780636d4ce63c1461005d57610037565b600080fd5b61004561007e565b60405161005291906100c5565b60405180910390f35b61007c6004803603602081101561007a57600080fd5b50356100c2565b6040516020018083838082843780820191505050505b565b005b6100946100c4565b60405161005291906100bf565b6100d1565b60405180910390f35b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a9059cbb60e11b815260040161010060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16146101e557600080fd5b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663e7ba30df6040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156101ae57600080fd5b505af11580156101c2573d6000803e3d6000fd5b50505050505050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff161461029157600080fd5b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663fdacd5766040518163ffffffff1660e01b8152600401600060405180830381600087803b1580156102f957600080fd5b505af115801561030d573d6000803e3d6000fd5b50505050505050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff168156fea2646970667358221220d4f2763f3a0ae2826cc9ef37a65ff0c14d7a3aafe8d1636ff99f72e2f705413d64736f6c634300060c0033",
		"from": coinBase,
	}

	for i := 0; i < 10; i ++ {
		time.Sleep(2 * time.Second)
		err = client.Call(&t3, "eth_sendTransaction", genericArg)
		if err != nil {
			log.Error("looped transaction failed on index", "i", i, "err", err)
		}
	}

}

type TraceConfig struct {
	Tracer  *string
}

func txTracer() {

	cl := apis[0].Service.(*engineService).stack
	client, err := cl.Attach()
	if err != nil {
		log.Error("Error connecting with client block factory")
	}

	time.Sleep(2 * time.Second)
	tr := "testTracer"
	t := TraceConfig{
		Tracer: &tr,
	}

	var trResult interface{}
	err = client.Call(&trResult, "debug_traceTransaction", t0, t)
	if err != nil {
		log.Error("debug_traceTransaction failed",  "err", err)
	}

	testGetContractCode(t3)

	debugArg0 := map[string]interface{}{
		"input": "0x60006000fd",
		"from": coinBase,
	}

	var trResult0 interface{}
	err = client.Call(&trResult0, "debug_traceCall", debugArg0, "latest", t)
	if err != nil {
		log.Error("debug_traceCall 0 failed",  "err", err)
	}

	debugArg1 := map[string]interface{}{
		"input": "0x61520873000000000000000000000000000000000000000060006000600060006000f1",
		"from": coinBase,
	}

	var trResult1 interface{}
	err = client.Call(&trResult1, "debug_traceCall", debugArg1, "latest", t)

	final := map[string]interface{}{
		"input": "0x61520873000000000000000000000000000000000000000060006000600060006000f1",
		"from": coinBase,
	}

	time.Sleep(2 * time.Second)
	err = client.Call(&t3, "eth_sendTransaction", final)
	if err != nil {
		log.Error("contract call failed", "err", err)
	}

	quit <- "quit"

}

func testGetContractCode(hash core.Hash) {
	
	cl := apis[0].Service.(*engineService).stack
	client, err := cl.Attach()
	if err != nil {
		log.Error("Error connecting with client testGetContractCode")
	}

	receipt := map[string]interface{}{}
	err = client.Call(&receipt, "eth_getTransactionReceipt", hash)
	if err != nil {
		log.Error("Error calling getTransactionReciepts, testGetContractCode", "err", err)
	}

	var controlCode hexutil.Bytes
	err = client.Call(&controlCode, "eth_getCode", receipt["contractAddress"], receipt["blockNumber"])
	if err != nil {
		log.Error("Error calling getCode, testGetContractCode", "err", err)
	}

	codeHash := crypto.Keccak256Hash(controlCode)

	testCode, err := apis[0].Service.(*engineService).backend.GetContractCode(codeHash)
	if err != nil {
		log.Error("Error calling GetContractCode", "err", err)
	}

	if !bytes.Equal(testCode, controlCode) {

		log.Error("Exit with error, return value from GetContractCode is divergent from control value")
		os.Exit(1)
	} 
}