package types

import (
	"bytes"
	"crypto/ecdsa"
	"fmt"
	"io"
	"math/big"
	"sync/atomic"

	sdk "github.com/cosmos/cosmos-sdk/types"
	"github.com/cosmos/cosmos-sdk/wire"
	ethcmn "github.com/ethereum/go-ethereum/common"
	ethtypes "github.com/ethereum/go-ethereum/core/types"
	ethcrypto "github.com/ethereum/go-ethereum/crypto"
	ethsha "github.com/ethereum/go-ethereum/crypto/sha3"
	"github.com/ethereum/go-ethereum/rlp"
	"github.com/pkg/errors"
)

const (
	// TypeTxEthereum reflects an Ethereum Transaction type.
	TypeTxEthereum = "Ethereum"
)

// ----------------------------------------------------------------------------
// Ethereum transaction
// ----------------------------------------------------------------------------

var _ sdk.Tx = (*Transaction)(nil)

type (
	// Transaction implements the Ethereum transaction structure as an exact
	// replica. It implements the Cosmos sdk.Tx interface. Due to the private
	// fields, it must be replicated here and cannot be embedded or used
	// directly.
	//
	// Note: The transaction also implements the sdk.Msg interface to perform
	// basic validation that is done in the BaseApp.
	Transaction struct {
		data TxData

		// caches
		hash atomic.Value
		size atomic.Value
		from atomic.Value
	}

	// TxData implements the Ethereum transaction data structure as an exact
	// copy. It is used solely as intended in Ethereum abiding by the protocol
	// except for the payload field which may embed a Cosmos SDK transaction.
	TxData struct {
		AccountNonce uint64          `json:"nonce"`
		Price        *big.Int        `json:"gasPrice"`
		GasLimit     uint64          `json:"gas"`
		Recipient    *ethcmn.Address `json:"to" rlp:"nil"` // nil means contract creation
		Amount       *big.Int        `json:"value"`
		Payload      []byte          `json:"input"`

		// signature values
		V *big.Int `json:"v"`
		R *big.Int `json:"r"`
		S *big.Int `json:"s"`

		// hash is only used when marshaling to JSON
		Hash *ethcmn.Hash `json:"hash" rlp:"-"`
	}

	// sigCache is used to cache the derived sender and contains the signer used
	// to derive it.
	sigCache struct {
		signer ethtypes.Signer
		from   ethcmn.Address
	}
)

// NewTransaction returns a reference to a new Ethereum transaction.
func NewTransaction(
	nonce uint64, to ethcmn.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte,
) *Transaction {

	return newTransaction(nonce, &to, amount, gasLimit, gasPrice, payload)
}

// NewContractCreation returns a reference to a new Ethereum transaction
// designated for contract creation.
func NewContractCreation(
	nonce uint64, amount *big.Int, gasLimit uint64, gasPrice *big.Int, payload []byte,
) *Transaction {

	return newTransaction(nonce, nil, amount, gasLimit, gasPrice, payload)
}

func newTransaction(
	nonce uint64, to *ethcmn.Address, amount *big.Int,
	gasLimit uint64, gasPrice *big.Int, payload []byte,
) *Transaction {

	if len(payload) > 0 {
		payload = ethcmn.CopyBytes(payload)
	}

	txData := TxData{
		AccountNonce: nonce,
		Recipient:    to,
		Payload:      payload,
		GasLimit:     gasLimit,
		Amount:       new(big.Int),
		Price:        new(big.Int),
		V:            new(big.Int),
		R:            new(big.Int),
		S:            new(big.Int),
	}

	if amount != nil {
		txData.Amount.Set(amount)
	}
	if gasPrice != nil {
		txData.Price.Set(gasPrice)
	}

	return &Transaction{data: txData}
}

// Data returns the Transaction's data.
func (tx Transaction) Data() TxData {
	return tx.data
}

// EncodeRLP implements the rlp.Encoder interface.
func (tx *Transaction) EncodeRLP(w io.Writer) error {
	return rlp.Encode(w, &tx.data)
}

// DecodeRLP implements the rlp.Decoder interface.
func (tx *Transaction) DecodeRLP(s *rlp.Stream) error {
	_, size, _ := s.Kind()
	err := s.Decode(&tx.data)
	if err == nil {
		tx.size.Store(ethcmn.StorageSize(rlp.ListSize(size)))
	}

	return err
}

// Hash hashes the RLP encoding of a transaction.
func (tx *Transaction) Hash() ethcmn.Hash {
	if hash := tx.hash.Load(); hash != nil {
		return hash.(ethcmn.Hash)
	}

	v := rlpHash(tx)
	tx.hash.Store(v)
	return v
}

// SigHash returns the RLP hash of a transaction with a given chainID used for
// signing.
func (tx Transaction) SigHash(chainID *big.Int) ethcmn.Hash {
	return rlpHash([]interface{}{
		tx.data.AccountNonce,
		tx.data.Price,
		tx.data.GasLimit,
		tx.data.Recipient,
		tx.data.Amount,
		tx.data.Payload,
		chainID, uint(0), uint(0),
	})
}

// Sign calculates a secp256k1 ECDSA signature and signs the transaction. It
// takes a private key and chainID to sign an Ethereum transaction according to
// EIP155 standard. It mutates the transaction as it populates the V, R, S
// fields of the Transaction's Signature.
func (tx *Transaction) Sign(chainID *big.Int, priv *ecdsa.PrivateKey) {
	txHash := tx.SigHash(chainID)

	sig, err := ethcrypto.Sign(txHash[:], priv)
	if err != nil {
		panic(err)
	}

	if len(sig) != 65 {
		panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
	}

	r := new(big.Int).SetBytes(sig[:32])
	s := new(big.Int).SetBytes(sig[32:64])

	var v *big.Int
	if chainID.Sign() == 0 {
		v = new(big.Int).SetBytes([]byte{sig[64] + 27})
	} else {
		v = big.NewInt(int64(sig[64] + 35))
		chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
		v.Add(v, chainIDMul)
	}

	tx.data.V = v
	tx.data.R = r
	tx.data.S = s
}

// VerifySig attempts to verify a Transaction's signature for a given chainID.
// A derived address is returned upon success or an error if recovery fails.
func (tx Transaction) VerifySig(chainID *big.Int) (ethcmn.Address, error) {
	signer := ethtypes.NewEIP155Signer(chainID)

	if sc := tx.from.Load(); sc != nil {
		sigCache := sc.(sigCache)
		// If the signer used to derive from in a previous
		// call is not the same as used current, invalidate
		// the cache.
		if sigCache.signer.Equal(signer) {
			return sigCache.from, nil
		}
	}

	// do not allow recovery for transactions with an unprotected chainID
	if chainID.Sign() == 0 {
		return ethcmn.Address{}, errors.New("invalid chainID")
	}

	txHash := tx.SigHash(chainID)
	sig := recoverEthSig(tx.data.R, tx.data.S, tx.data.V, chainID)

	pub, err := ethcrypto.Ecrecover(txHash[:], sig)
	if err != nil {
		return ethcmn.Address{}, err
	}

	var addr ethcmn.Address
	copy(addr[:], ethcrypto.Keccak256(pub[1:])[12:])

	tx.from.Store(sigCache{signer: signer, from: addr})
	return addr, nil
}

// Type implements the sdk.Msg interface. It returns the type of the
// Transaction.
func (tx Transaction) Type() string {
	return TypeTxEthereum
}

// ValidateBasic implements the sdk.Msg interface. It performs basic validation
// checks of a Transaction. If returns an sdk.Error if validation fails.
func (tx Transaction) ValidateBasic() sdk.Error {
	if tx.data.Price.Sign() != 1 {
		return ErrInvalidValue(DefaultCodespace, "price must be positive")
	}

	if tx.data.Amount.Sign() != 1 {
		return ErrInvalidValue(DefaultCodespace, "amount must be positive")
	}

	return nil
}

// GetSignBytes performs a no-op and should not be used. It implements the
// sdk.Msg Interface
func (tx Transaction) GetSignBytes() (sigBytes []byte) { return }

// GetSigners performs a no-op and should not be used. It implements the
// sdk.Msg Interface
//
// CONTRACT: The transaction must already be signed.
func (tx Transaction) GetSigners() (signers []sdk.AccAddress) { return }

// GetMsgs returns a single message containing the Transaction itself. It
// implements the Cosmos sdk.Tx interface.
func (tx Transaction) GetMsgs() []sdk.Msg {
	return []sdk.Msg{tx}
}

// hasEmbeddedTx returns a boolean reflecting if the transaction contains an
// SDK transaction or not based on the recipient address.
func (tx Transaction) hasEmbeddedTx(addr ethcmn.Address) bool {
	return bytes.Equal(tx.data.Recipient.Bytes(), addr.Bytes())
}

// GetEmbeddedTx returns the embedded SDK transaction from an Ethereum
// transaction. It returns an error if decoding the inner transaction fails.
//
// CONTRACT: The payload field of an Ethereum transaction must contain a valid
// encoded SDK transaction.
func (tx Transaction) GetEmbeddedTx(codec *wire.Codec) (sdk.Tx, sdk.Error) {
	var etx sdk.Tx

	err := codec.UnmarshalBinary(tx.data.Payload, &etx)
	if err != nil {
		return etx, sdk.ErrTxDecode("failed to decode embedded transaction")
	}

	return etx, nil
}

// ----------------------------------------------------------------------------
// Utilities
// ----------------------------------------------------------------------------

// TxDecoder returns an sdk.TxDecoder that given raw transaction bytes and an
// SDK address, attempts to decode them into a Transaction or an EmbeddedTx or
// returning an error if decoding fails.
func TxDecoder(codec *wire.Codec, sdkAddress ethcmn.Address) sdk.TxDecoder {
	return func(txBytes []byte) (sdk.Tx, sdk.Error) {
		var tx = Transaction{}

		if len(txBytes) == 0 {
			return nil, sdk.ErrTxDecode("transaction bytes are empty")
		}

		err := rlp.DecodeBytes(txBytes, &tx)
		if err != nil {
			return nil, sdk.ErrTxDecode("failed to decode transaction").TraceSDK(err.Error())
		}

		// If the transaction is routed as an SDK transaction, decode and return
		// the embedded SDK transaction.
		if tx.hasEmbeddedTx(sdkAddress) {
			etx, err := tx.GetEmbeddedTx(codec)
			if err != nil {
				return nil, err
			}

			return etx, nil
		}

		return tx, nil
	}
}

// recoverEthSig recovers a signature according to the Ethereum specification.
func recoverEthSig(R, S, Vb, chainID *big.Int) []byte {
	var v byte

	r, s := R.Bytes(), S.Bytes()
	sig := make([]byte, 65)

	copy(sig[32-len(r):32], r)
	copy(sig[64-len(s):64], s)

	if chainID.Sign() == 0 {
		v = byte(Vb.Uint64() - 27)
	} else {
		chainIDMul := new(big.Int).Mul(chainID, big.NewInt(2))
		V := new(big.Int).Sub(Vb, chainIDMul)

		v = byte(V.Uint64() - 35)
	}

	sig[64] = v
	return sig
}

func rlpHash(x interface{}) (hash ethcmn.Hash) {
	hasher := ethsha.NewKeccak256()

	rlp.Encode(hasher, x)
	hasher.Sum(hash[:0])

	return
}
