// Copyright 2015 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum 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 go-ethereum 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 go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package api

import (
	"fmt"
	"io"
	"math/big"
	"os"
	"time"

	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/compiler"
	"github.com/ethereum/go-ethereum/common/docserver"
	"github.com/ethereum/go-ethereum/common/natspec"
	"github.com/ethereum/go-ethereum/common/registrar"
	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/eth"
	"github.com/ethereum/go-ethereum/logger/glog"
	"github.com/ethereum/go-ethereum/rlp"
	"github.com/ethereum/go-ethereum/rpc/codec"
	"github.com/ethereum/go-ethereum/rpc/comms"
	"github.com/ethereum/go-ethereum/rpc/shared"
	"github.com/ethereum/go-ethereum/xeth"
)

const (
	AdminApiversion = "1.0"
	importBatchSize = 2500
)

var (
	// mapping between methods and handlers
	AdminMapping = map[string]adminhandler{
		"admin_addPeer":            (*adminApi).AddPeer,
		"admin_peers":              (*adminApi).Peers,
		"admin_nodeInfo":           (*adminApi).NodeInfo,
		"admin_exportChain":        (*adminApi).ExportChain,
		"admin_importChain":        (*adminApi).ImportChain,
		"admin_verbosity":          (*adminApi).Verbosity,
		"admin_chainSyncStatus":    (*adminApi).ChainSyncStatus,
		"admin_setSolc":            (*adminApi).SetSolc,
		"admin_datadir":            (*adminApi).DataDir,
		"admin_startRPC":           (*adminApi).StartRPC,
		"admin_stopRPC":            (*adminApi).StopRPC,
		"admin_setGlobalRegistrar": (*adminApi).SetGlobalRegistrar,
		"admin_setHashReg":         (*adminApi).SetHashReg,
		"admin_setUrlHint":         (*adminApi).SetUrlHint,
		"admin_saveInfo":           (*adminApi).SaveInfo,
		"admin_register":           (*adminApi).Register,
		"admin_registerUrl":        (*adminApi).RegisterUrl,
		"admin_startNatSpec":       (*adminApi).StartNatSpec,
		"admin_stopNatSpec":        (*adminApi).StopNatSpec,
		"admin_getContractInfo":    (*adminApi).GetContractInfo,
		"admin_httpGet":            (*adminApi).HttpGet,
		"admin_sleepBlocks":        (*adminApi).SleepBlocks,
		"admin_sleep":              (*adminApi).Sleep,
	}
)

// admin callback handler
type adminhandler func(*adminApi, *shared.Request) (interface{}, error)

// admin api provider
type adminApi struct {
	xeth     *xeth.XEth
	ethereum *eth.Ethereum
	codec    codec.Codec
	coder    codec.ApiCoder
	ds       *docserver.DocServer
}

// create a new admin api instance
func NewAdminApi(xeth *xeth.XEth, ethereum *eth.Ethereum, codec codec.Codec) *adminApi {
	return &adminApi{
		xeth:     xeth,
		ethereum: ethereum,
		codec:    codec,
		coder:    codec.New(nil),
		ds:       docserver.New("/"),
	}
}

// collection with supported methods
func (self *adminApi) Methods() []string {
	methods := make([]string, len(AdminMapping))
	i := 0
	for k := range AdminMapping {
		methods[i] = k
		i++
	}
	return methods
}

// Execute given request
func (self *adminApi) Execute(req *shared.Request) (interface{}, error) {
	if callback, ok := AdminMapping[req.Method]; ok {
		return callback(self, req)
	}

	return nil, &shared.NotImplementedError{req.Method}
}

func (self *adminApi) Name() string {
	return shared.AdminApiName
}

func (self *adminApi) ApiVersion() string {
	return AdminApiversion
}

func (self *adminApi) AddPeer(req *shared.Request) (interface{}, error) {
	args := new(AddPeerArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	err := self.ethereum.AddPeer(args.Url)
	if err == nil {
		return true, nil
	}
	return false, err
}

func (self *adminApi) Peers(req *shared.Request) (interface{}, error) {
	return self.ethereum.PeersInfo(), nil
}

func (self *adminApi) NodeInfo(req *shared.Request) (interface{}, error) {
	return self.ethereum.NodeInfo(), nil
}

func (self *adminApi) DataDir(req *shared.Request) (interface{}, error) {
	return self.ethereum.DataDir, nil
}

func hasAllBlocks(chain *core.ChainManager, bs []*types.Block) bool {
	for _, b := range bs {
		if !chain.HasBlock(b.Hash()) {
			return false
		}
	}
	return true
}

func (self *adminApi) ImportChain(req *shared.Request) (interface{}, error) {
	args := new(ImportExportChainArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	fh, err := os.Open(args.Filename)
	if err != nil {
		return false, err
	}
	defer fh.Close()
	stream := rlp.NewStream(fh, 0)

	// Run actual the import.
	blocks := make(types.Blocks, importBatchSize)
	n := 0
	for batch := 0; ; batch++ {

		i := 0
		for ; i < importBatchSize; i++ {
			var b types.Block
			if err := stream.Decode(&b); err == io.EOF {
				break
			} else if err != nil {
				return false, fmt.Errorf("at block %d: %v", n, err)
			}
			blocks[i] = &b
			n++
		}
		if i == 0 {
			break
		}
		// Import the batch.
		if hasAllBlocks(self.ethereum.ChainManager(), blocks[:i]) {
			continue
		}
		if _, err := self.ethereum.ChainManager().InsertChain(blocks[:i]); err != nil {
			return false, fmt.Errorf("invalid block %d: %v", n, err)
		}
	}
	return true, nil
}

func (self *adminApi) ExportChain(req *shared.Request) (interface{}, error) {
	args := new(ImportExportChainArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	fh, err := os.OpenFile(args.Filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
	if err != nil {
		return false, err
	}
	defer fh.Close()
	if err := self.ethereum.ChainManager().Export(fh); err != nil {
		return false, err
	}

	return true, nil
}

func (self *adminApi) Verbosity(req *shared.Request) (interface{}, error) {
	args := new(VerbosityArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	glog.SetV(args.Level)
	return true, nil
}

func (self *adminApi) ChainSyncStatus(req *shared.Request) (interface{}, error) {
	pending, cached, importing, estimate := self.ethereum.Downloader().Stats()

	return map[string]interface{}{
		"blocksAvailable":        pending,
		"blocksWaitingForImport": cached,
		"importing":              importing,
		"estimate":               estimate.String(),
	}, nil
}

func (self *adminApi) SetSolc(req *shared.Request) (interface{}, error) {
	args := new(SetSolcArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	solc, err := self.xeth.SetSolc(args.Path)
	if err != nil {
		return nil, err
	}
	return solc.Info(), nil
}

func (self *adminApi) StartRPC(req *shared.Request) (interface{}, error) {
	args := new(StartRPCArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	cfg := comms.HttpConfig{
		ListenAddress: args.ListenAddress,
		ListenPort:    args.ListenPort,
		CorsDomain:    args.CorsDomain,
	}

	apis, err := ParseApiString(args.Apis, self.codec, self.xeth, self.ethereum)
	if err != nil {
		return false, err
	}

	err = comms.StartHttp(cfg, self.codec, Merge(apis...))
	if err == nil {
		return true, nil
	}
	return false, err
}

func (self *adminApi) StopRPC(req *shared.Request) (interface{}, error) {
	comms.StopHttp()
	return true, nil
}

func (self *adminApi) SleepBlocks(req *shared.Request) (interface{}, error) {
	args := new(SleepBlocksArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}
	var timer <-chan time.Time
	var height *big.Int
	var err error
	if args.Timeout > 0 {
		timer = time.NewTimer(time.Duration(args.Timeout) * time.Second).C
	}

	height = new(big.Int).Add(self.xeth.CurrentBlock().Number(), big.NewInt(args.N))
	height, err = sleepBlocks(self.xeth.UpdateState(), height, timer)
	if err != nil {
		return nil, err
	}
	return height.Uint64(), nil
}

func sleepBlocks(wait chan *big.Int, height *big.Int, timer <-chan time.Time) (newHeight *big.Int, err error) {
	wait <- height
	select {
	case <-timer:
		// if times out make sure the xeth loop does not block
		go func() {
			select {
			case wait <- nil:
			case <-wait:
			}
		}()
		return nil, fmt.Errorf("timeout")
	case newHeight = <-wait:
	}
	return
}

func (self *adminApi) Sleep(req *shared.Request) (interface{}, error) {
	args := new(SleepArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}
	time.Sleep(time.Duration(args.S) * time.Second)
	return nil, nil
}

func (self *adminApi) SetGlobalRegistrar(req *shared.Request) (interface{}, error) {
	args := new(SetGlobalRegistrarArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	sender := common.HexToAddress(args.ContractAddress)

	reg := registrar.New(self.xeth)
	txhash, err := reg.SetGlobalRegistrar(args.NameReg, sender)
	if err != nil {
		return false, err
	}

	return txhash, nil
}

func (self *adminApi) SetHashReg(req *shared.Request) (interface{}, error) {
	args := new(SetHashRegArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	reg := registrar.New(self.xeth)
	sender := common.HexToAddress(args.Sender)
	txhash, err := reg.SetHashReg(args.HashReg, sender)
	if err != nil {
		return false, err
	}

	return txhash, nil
}

func (self *adminApi) SetUrlHint(req *shared.Request) (interface{}, error) {
	args := new(SetUrlHintArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	urlHint := args.UrlHint
	sender := common.HexToAddress(args.Sender)

	reg := registrar.New(self.xeth)
	txhash, err := reg.SetUrlHint(urlHint, sender)
	if err != nil {
		return nil, err
	}

	return txhash, nil
}

func (self *adminApi) SaveInfo(req *shared.Request) (interface{}, error) {
	args := new(SaveInfoArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	contenthash, err := compiler.SaveInfo(&args.ContractInfo, args.Filename)
	if err != nil {
		return nil, err
	}

	return contenthash.Hex(), nil
}

func (self *adminApi) Register(req *shared.Request) (interface{}, error) {
	args := new(RegisterArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	sender := common.HexToAddress(args.Sender)
	// sender and contract address are passed as hex strings
	codeb := self.xeth.CodeAtBytes(args.Address)
	codeHash := common.BytesToHash(crypto.Sha3(codeb))
	contentHash := common.HexToHash(args.ContentHashHex)
	registry := registrar.New(self.xeth)

	_, err := registry.SetHashToHash(sender, codeHash, contentHash)
	if err != nil {
		return false, err
	}

	return true, nil
}

func (self *adminApi) RegisterUrl(req *shared.Request) (interface{}, error) {
	args := new(RegisterUrlArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	sender := common.HexToAddress(args.Sender)
	registry := registrar.New(self.xeth)
	_, err := registry.SetUrlToHash(sender, common.HexToHash(args.ContentHash), args.Url)
	if err != nil {
		return false, err
	}

	return true, nil
}

func (self *adminApi) StartNatSpec(req *shared.Request) (interface{}, error) {
	self.ethereum.NatSpec = true
	return true, nil
}

func (self *adminApi) StopNatSpec(req *shared.Request) (interface{}, error) {
	self.ethereum.NatSpec = false
	return true, nil
}

func (self *adminApi) GetContractInfo(req *shared.Request) (interface{}, error) {
	args := new(GetContractInfoArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	infoDoc, err := natspec.FetchDocsForContract(args.Contract, self.xeth, self.ds)
	if err != nil {
		return nil, err
	}

	var info interface{}
	err = self.coder.Decode(infoDoc, &info)
	if err != nil {
		return nil, err
	}

	return info, nil
}

func (self *adminApi) HttpGet(req *shared.Request) (interface{}, error) {
	args := new(HttpGetArgs)
	if err := self.coder.Decode(req.Params, &args); err != nil {
		return nil, shared.NewDecodeParamError(err.Error())
	}

	resp, err := self.ds.Get(args.Uri, args.Path)
	if err != nil {
		return nil, err
	}

	return string(resp), nil
}
