package blockchain

import "fmt"
import "bytes"
import "encoding/hex"
import "encoding/binary"

import "github.com/romana/rlog"

import "github.com/deroproject/derosuite/crypto"
import "github.com/deroproject/derosuite/config"
import "github.com/deroproject/derosuite/cryptonight"



// these are defined  in file
//https://github.com/monero-project/monero/src/cryptonote_basic/cryptonote_basic.h
type Block_Header struct {
	Major_Version uint32      `json:"major_version"`
	Minor_Version uint32      `json:"minor_version"`
	Timestamp     uint64      `json:"timestamp"`
	Prev_Hash     crypto.Hash `json:"prev_id"`
	Nonce         uint32      `json:"nonce"`
}

type Block struct {
	Block_Header
	Miner_tx    Transaction   `json:"miner_tx"`
	Merkle_Root crypto.Hash   `json:"-"`
	Tx_hashes   []crypto.Hash `json:"tx_hashes"`

	treehash crypto.Hash
}

// see spec here https://cryptonote.org/cns/cns003.txt
// this function gets the block identifier hash
func (bl *Block) GetHash() (hash crypto.Hash) {
	buf := make([]byte, binary.MaxVarintLen64)
	long_header := bl.GetBlockWork()
	length := uint64(len(long_header))
	n := binary.PutUvarint(buf, length) //
	buf = buf[:n]
	block_id_blob := append(buf, long_header...)

	// keccak hash of this above blob, gives the block id
	hash2 := crypto.Keccak256(block_id_blob)
	return crypto.Hash(hash2)
}

// converts a block, into a getwork style work, ready for either submitting the block
// or doing Pow Calculations
func (bl *Block) GetBlockWork() []byte {
	buf := make([]byte, binary.MaxVarintLen64)
	header := bl.SerializeHeader()
	tx_treehash := bl.GetTreeHash() // treehash of all transactions

	// length of all transactions
	n := binary.PutUvarint(buf, uint64(len(bl.Tx_hashes)+1)) // +1 for miner TX
	buf = buf[:n]

	long_header := append(header, tx_treehash[:]...)
	long_header = append(long_header, buf...)

	return long_header

}

// Get PoW hash , this is very slow function
func (bl *Block) GetPoWHash() (hash crypto.Hash) {
	long_header := bl.GetBlockWork()
	rlog.Tracef(9, "longheader %x\n", long_header)
	tmphash := cryptonight.SlowHash(long_header)
	copy(hash[:], tmphash[:32])

	return
}

// Reward is, total amount in the miner tx - fees
func (bl *Block) GetReward() uint64 {
	total_amount := bl.Miner_tx.Vout[0].Amount
	total_fees := uint64(0)
	// load all the TX and get the fees, since we are in a post rct world
	// extract the fees from the rct sig
	return total_amount - total_fees
}

// serialize block header
func (bl *Block) SerializeHeader() []byte {

	var serialised bytes.Buffer

	buf := make([]byte, binary.MaxVarintLen64)

	n := binary.PutUvarint(buf, uint64(bl.Major_Version))
	serialised.Write(buf[:n])

	n = binary.PutUvarint(buf, uint64(bl.Minor_Version))
	serialised.Write(buf[:n])

	n = binary.PutUvarint(buf, bl.Timestamp)
	serialised.Write(buf[:n])

	serialised.Write(bl.Prev_Hash[:32]) // write previous ID

	binary.LittleEndian.PutUint32(buf[0:8], bl.Nonce) // check whether it needs to be big endian
	serialised.Write(buf[:4])

	return serialised.Bytes()

}

// serialize entire block ( block_header + miner_tx + tx_list )
func (bl *Block) Serialize() []byte {
	var serialized bytes.Buffer
	buf := make([]byte, binary.MaxVarintLen64)

	header := bl.SerializeHeader()
	serialized.Write(header)

	// miner tx should always be coinbase
	minex_tx := bl.Miner_tx.Serialize()
	serialized.Write(minex_tx)

	//fmt.Printf("serializing tx hashes %d\n", len(bl.Tx_hashes))

	n := binary.PutUvarint(buf, uint64(len(bl.Tx_hashes)))
	serialized.Write(buf[:n])

	for _, hash := range bl.Tx_hashes {
		serialized.Write(hash[:])
	}

	return serialized.Bytes()
}

// get block transactions tree hash
func (bl *Block) GetTreeHash() (hash crypto.Hash) {
	var hash_list []crypto.Hash

	hash_list = append(hash_list, bl.Miner_tx.GetHash())
	// add all the remaining hashes
	for i := range bl.Tx_hashes {
		hash_list = append(hash_list, bl.Tx_hashes[i])
	}

	return TreeHash(hash_list)

}

// input is the list of transactions hashes
func TreeHash(hashes []crypto.Hash) (hash crypto.Hash) {

	switch len(hashes) {
	case 0:
		panic("Treehash cannot have 0 transactions, atleast miner tx will be present")
	case 1:
		copy(hash[:], hashes[0][:32])
	case 2:
		var buf []byte
		for i := 0; i < len(hashes); i++ {
			buf = append(buf, hashes[i][:32]...)
		}
		tmp_hash := crypto.Keccak256(buf)
		copy(hash[:], tmp_hash[:32])

	default:

		count := uint64(len(hashes))
		cnt := tree_hash_cnt(count)

		//fmt.Printf("cnt %d count %d\n",cnt, count)
		ints := make([]byte, 32*cnt, 32*cnt)

		hashes_buf := make([]byte, 32*count, 32*count)

		for i := uint64(0); i < count; i++ {
			copy(hashes_buf[i*32:], hashes[i][:32]) // copy hashes 1 by 1
		}

		for i := uint64(0); i < ((2 * cnt) - count); i++ {
			copy(ints[i*32:], hashes[i][:32]) // copy hashes 1 by 1
		}

		i := ((2 * cnt) - count)
		j := ((2 * cnt) - count)
		for ; j < cnt; i, j = i+2, j+1 {
			hash := crypto.Keccak256(hashes_buf[i*32 : (i*32)+64]) // find hash of 64 bytes
			copy(ints[j*32:], hash[:32])
		}
		if i != count {
			panic("please fix tree hash")
		}

		for cnt > 2 {
			cnt = cnt >> 1
			i = 0
			j = 0
			for ; j < cnt; i, j = i+2, j+1 {
				hash := crypto.Keccak256(ints[i*32 : (i*32)+64]) // find hash of 64 bytes
				copy(ints[j*32:], hash[:32])
			}
		}

		hash = crypto.Hash(crypto.Keccak256(ints[0:64])) // find hash of 64 bytes
	}
	return
}

// see crypto/tree-hash.c
// this function has a naughty history
func tree_hash_cnt(count uint64) uint64 {
	pow := uint64(2)
	for pow < count {
		pow = pow << 1
	}
	return pow >> 1
}

func (bl *Block) Deserialize(buf []byte) (err error) {
	done := 0
	var tmp uint64

	defer func() {
		if r := recover(); r != nil {
			logger.Warnf("Panic while deserialising block, block hex_dump below to make a testcase/debug\n")
			logger.Warnf("%s", hex.EncodeToString(buf))
			err = fmt.Errorf("Invalid Block")
			return
		}
	}()

	tmp, done = binary.Uvarint(buf)
	if done <= 0 {
		return fmt.Errorf("Invalid Version in Block\n")
	}
	buf = buf[done:]


	bl.Major_Version = uint32(tmp)

	if uint64(bl.Major_Version) != tmp {
		return fmt.Errorf("Invalid Block major version")
	}

	tmp, done = binary.Uvarint(buf)
	if done <= 0 {
		return fmt.Errorf("Invalid minor Version in Block\n")
	}
	buf = buf[done:]

	bl.Minor_Version = uint32(tmp)

	if uint64(bl.Minor_Version) != tmp {
		return fmt.Errorf("Invalid Block minor version")
	}

	bl.Timestamp, done = binary.Uvarint(buf)
	if done <= 0 {
		return fmt.Errorf("Invalid Timestamp in Block\n")
	}
	buf = buf[done:]

	copy(bl.Prev_Hash[:], buf[:32]) // hash is always 32 byte
	buf = buf[32:]

	bl.Nonce = binary.LittleEndian.Uint32(buf)
	buf = buf[4:] // nonce is always 4 bytes



	// read and parse transaction
	err = bl.Miner_tx.DeserializeHeader(buf)

	if err != nil {
		return fmt.Errorf("Cannot parse miner TX  %x", buf)
	}


	// if tx was parse, make sure it's coin base
	if len(bl.Miner_tx.Vin) != 1 || bl.Miner_tx.Vin[0].(Txin_gen).Height > config.MAX_CHAIN_HEIGHT {
		// serialize transaction again to get the tx size, so as parsing could continue
		return fmt.Errorf("Invalid Miner TX")
	}

	miner_tx_serialized_size := bl.Miner_tx.Serialize()
	buf = buf[len(miner_tx_serialized_size):]

	//fmt.Printf("miner tx %x\n", miner_tx_serialized_size)
	// read number of transactions
	tx_count, done := binary.Uvarint(buf)
	if done <= 0 {
		return fmt.Errorf("Invalid Tx count in Block\n")
	}
	buf = buf[done:]

	// remember first tx is merkle root

	for i := uint64(0); i < tx_count; i++ {
		//fmt.Printf("Parsing transaction hash %d  tx_count %d\n", i, tx_count)
		var h crypto.Hash
		copy(h[:], buf[:32])
		buf = buf[32:]

		bl.Tx_hashes = append(bl.Tx_hashes, h)
		
	}

	//fmt.Printf("%d member in tx hashes \n",len(bl.Tx_hashes))

	return

}

