// Copyright 2017-2021 DERO Project. All rights reserved.
// Use of this source code in any form is governed by RESEARCH license.
// license can be found in the LICENSE file.
// GPG: 0F39 E425 8C65 3947 702A  8234 08B2 0360 A03A 9DE8
//
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package blockchain

// this file implements  core execution of all changes to block chain homomorphically

import "fmt"
import "bufio"
import "strings"
import "strconv"
import "runtime/debug"
import "encoding/hex"
import "math/big"
import "golang.org/x/xerrors"

import "github.com/deroproject/derohe/cryptography/crypto"
import "github.com/deroproject/derohe/cryptography/bn256"
import "github.com/deroproject/derohe/transaction"
import "github.com/deroproject/derohe/config"
import "github.com/deroproject/derohe/premine"
import "github.com/deroproject/derohe/globals"
import "github.com/deroproject/derohe/block"
import "github.com/deroproject/derohe/rpc"
import "github.com/deroproject/derohe/dvm"
import "github.com/deroproject/graviton"

// convert bitcoin model to our, but skip initial 4 years of supply, so our total supply gets to 10.5 million
const RewardReductionInterval = 210000 * 600 / config.BLOCK_TIME // 210000 comes from bitcoin
const BaseReward = (41 * 100000 * config.BLOCK_TIME) / 600       // convert bitcoin reward system to our block

// CalcBlockSubsidy returns the subsidy amount a block at the provided height
// should have. This is mainly used for determining how much the coinbase for
// newly generated blocks awards as well as validating the coinbase for blocks
// has the expected value.
//
// The subsidy is halved every SubsidyReductionInterval blocks.  Mathematically
// this is: baseSubsidy / 2^(height/SubsidyReductionInterval)
//
// At the target block generation rate for the main network, this is
// approximately every 4 years.
//

// basically out of of the bitcoin supply, we have wiped of initial interval ( this wipes of  10.5 million, so total remaining is around 10.5 million
func CalcBlockReward(height uint64) uint64 {
	return BaseReward >> ((height + RewardReductionInterval) / RewardReductionInterval)
}

// process the miner tx, giving fees, miner rewatd etc
func (chain *Blockchain) process_miner_transaction(bl *block.Block, genesis bool, balance_tree *graviton.Tree, fees uint64, height uint64) {

	tx := bl.Miner_TX
	var acckey crypto.Point
	if err := acckey.DecodeCompressed(tx.MinerAddress[:]); err != nil {
		panic(err)
	}

	if genesis == true { // process premine ,register genesis block, dev key
		balance := crypto.ConstructElGamal(acckey.G1(), crypto.ElGamal_BASE_G) // init zero balance
		balance = balance.Plus(new(big.Int).SetUint64(tx.Value))               // add premine to users balance homomorphically
		nb := crypto.NonceBalance{NonceHeight: 0, Balance: balance}
		balance_tree.Put(tx.MinerAddress[:], nb.Serialize()) // reserialize and store

		if globals.IsMainnet() {
			return
		}

		// only testnet/simulator will have dummy accounts to test
		// we must process premine list and register and give them balance,
		premine_count := 0
		scanner := bufio.NewScanner(strings.NewReader(premine.List))
		for scanner.Scan() {
			data := strings.Split(scanner.Text(), ",")
			if len(data) < 2 {
				panic("invalid premine list")
			}

			var raw_tx [4096]byte
			var rtx transaction.Transaction
			if ramount, err := strconv.ParseUint(data[0], 10, 64); err != nil {
				panic(err)
			} else if n, err := hex.Decode(raw_tx[:], []byte(data[1])); err != nil {
				panic(err)
			} else if err := rtx.Deserialize(raw_tx[:n]); err != nil {
				panic(err)
			} else if !rtx.IsRegistration() {
				panic("tx is not registration")
			} else if !rtx.IsRegistrationValid() {
				panic("tx registration signature is invalid")
			} else {

				var racckey crypto.Point
				if err := racckey.DecodeCompressed(rtx.MinerAddress[:]); err != nil {
					panic(err)
				}

				balance := crypto.ConstructElGamal(racckey.G1(), crypto.ElGamal_BASE_G) // init zero balance
				balance = balance.Plus(new(big.Int).SetUint64(ramount))                 // add premine to users balance homomorphically
				nb := crypto.NonceBalance{NonceHeight: 0, Balance: balance}
				balance_tree.Put(rtx.MinerAddress[:], nb.Serialize()) // reserialize and store
				premine_count++
			}
		}

		logger.V(1).Info("successfully added premine accounts", "count", premine_count)

		return
	}

	// general coin base transaction
	base_reward := CalcBlockReward(uint64(height))
	full_reward := base_reward + fees

	//full_reward is divided into equal parts for all miner blocks + miner address
	// since perfect division is not possible, ( see money handling)
	// any left over change is delivered to main miner who integrated the full block

	share := full_reward / uint64(len(bl.MiniBlocks))              //  one block integrator, this is integer division
	leftover := full_reward - (share * uint64(len(bl.MiniBlocks))) // only integrator will get this

	{ // giver integrator his reward
		balance_serialized, err := balance_tree.Get(tx.MinerAddress[:])
		if err != nil {
			panic(err)
		}
		nb := new(crypto.NonceBalance).Deserialize(balance_serialized)
		nb.Balance = nb.Balance.Plus(new(big.Int).SetUint64(share + leftover)) // add miners reward to miners balance homomorphically
		balance_tree.Put(tx.MinerAddress[:], nb.Serialize())                   // reserialize and store
	}

	// all the other miniblocks will get their share
	for _, mbl := range bl.MiniBlocks {
		if mbl.Final {
			continue
		}
		_, key_compressed, balance_serialized, err := balance_tree.GetKeyValueFromHash(mbl.KeyHash[:16])
		if err != nil {
			panic(err)
		}

		nb := new(crypto.NonceBalance).Deserialize(balance_serialized)
		nb.Balance = nb.Balance.Plus(new(big.Int).SetUint64(share)) // add miners reward to miners balance homomorphically
		balance_tree.Put(key_compressed[:], nb.Serialize())         // reserialize and store

	}

	return

}

// process the tx, giving fees, miner rewatd etc
// this should be atomic, either all should be done or none at all
func (chain *Blockchain) process_transaction(changed map[crypto.Hash]*graviton.Tree, tx transaction.Transaction, balance_tree *graviton.Tree, height uint64) uint64 {

	logger.V(2).Info("Processing/Executing transaction", "txid", tx.GetHash(), "type", tx.TransactionType.String())
	switch tx.TransactionType {

	case transaction.REGISTRATION: // miner address represents registration
		if _, err := balance_tree.Get(tx.MinerAddress[:]); err != nil {
			if !xerrors.Is(err, graviton.ErrNotFound) { // any other err except not found panic
				panic(err)
			}
		} // address needs registration

		var acckey crypto.Point
		if err := acckey.DecodeCompressed(tx.MinerAddress[:]); err != nil {
			panic(err)
		}

		zerobalance := crypto.ConstructElGamal(acckey.G1(), crypto.ElGamal_BASE_G)

		if !globals.IsMainnet() { // give testnet users a dummy amount to play
			zerobalance = zerobalance.Plus(new(big.Int).SetUint64(800000)) // add fix amount to every wallet to users balance for more testing
		}

		// give new wallets generated in initial month a balance
		// so they can claim previous chain balance safely/securely without revealing themselves
		// 144000= 86400/18 *30
		if globals.IsMainnet() && height < 144000 {
			zerobalance = zerobalance.Plus(new(big.Int).SetUint64(200))
		}

		nb := crypto.NonceBalance{NonceHeight: 0, Balance: zerobalance}

		balance_tree.Put(tx.MinerAddress[:], nb.Serialize())

		return 0 // registration doesn't give any fees . why & how ?

	case transaction.BURN_TX, transaction.NORMAL, transaction.SC_TX: // burned amount is not added anywhere and thus lost forever
		for t := range tx.Payloads {
			var tree *graviton.Tree
			if tx.Payloads[t].SCID.IsZero() {
				tree = balance_tree
			} else {
				tree = changed[tx.Payloads[t].SCID]
			}

			parity := tx.Payloads[t].Proof.Parity()
			for i := 0; i < int(tx.Payloads[t].Statement.RingSize); i++ {
				key_pointer := tx.Payloads[t].Statement.Publickeylist_pointers[i*int(tx.Payloads[t].Statement.Bytes_per_publickey) : (i+1)*int(tx.Payloads[t].Statement.Bytes_per_publickey)]
				_, key_compressed, balance_serialized, err := tree.GetKeyValueFromHash(key_pointer)

				if err != nil && !tx.Payloads[t].SCID.IsZero() {
					if xerrors.Is(err, graviton.ErrNotFound) { // if the address is not found, lookup in main tree
						_, key_compressed, _, err = balance_tree.GetKeyValueFromHash(key_pointer)
						if err == nil {
							var p bn256.G1
							if err = p.DecodeCompressed(key_compressed[:]); err != nil {
								panic(fmt.Errorf("key %d could not be decompressed", i))
							}

							balance := crypto.ConstructElGamal(&p, crypto.ElGamal_BASE_G) // init zero balance
							nb := crypto.NonceBalance{NonceHeight: 0, Balance: balance}
							balance_serialized = nb.Serialize()
						}

					}
				}
				if err != nil {
					panic(fmt.Errorf("balance not obtained err %s\n", err))
				}

				nb := new(crypto.NonceBalance).Deserialize(balance_serialized)
				echanges := crypto.ConstructElGamal(tx.Payloads[t].Statement.C[i], tx.Payloads[t].Statement.D)

				nb.Balance = nb.Balance.Add(echanges) // homomorphic addition of changes

				if (i%2 == 0) == parity { // this condition is well thought out and works good enough
					nb.NonceHeight = height
				}
				tree.Put(key_compressed, nb.Serialize()) // reserialize and store

			}
		}

		return tx.Fees()

	default:
		panic("unknown transaction, do not know how to process it")
		return 0
	}
}

// does additional processing for SC
// all processing occurs in wrapped trees, if any error occurs we dicard all trees
func (chain *Blockchain) process_transaction_sc(cache map[crypto.Hash]*graviton.Tree, ss *graviton.Snapshot, bl_height, bl_topoheight, bl_timestamp uint64, blid crypto.Hash, tx transaction.Transaction, balance_tree *graviton.Tree, sc_tree *graviton.Tree) (gas uint64, err error) {

	if len(tx.SCDATA) == 0 {
		return tx.Fees(), nil
	}

	gas = tx.Fees()

	var gascompute, gasstorage uint64

	_ = gascompute
	_ = gasstorage

	w_sc_tree := &dvm.Tree_Wrapper{Tree: sc_tree, Entries: map[string][]byte{}}
	var w_sc_data_tree *dvm.Tree_Wrapper

	txhash := tx.GetHash()
	scid := txhash

	defer func() {
		if r := recover(); r != nil {
			logger.V(1).Error(nil, "Recover while executing SC ", "txid", txhash, "error", r, "stack", fmt.Sprintf("%s", string(debug.Stack())))

		}
	}()

	if !tx.SCDATA.Has(rpc.SCACTION, rpc.DataUint64) { //  tx doesn't have sc action
		return tx.Fees(), nil
	}

	incoming_value := map[crypto.Hash]uint64{}
	for _, payload := range tx.Payloads {
		incoming_value[payload.SCID] = payload.BurnValue
	}

	chain.Expand_Transaction_NonCoinbase(&tx)

	signer, err := Extract_signer(&tx)
	if err != nil { // allow anonymous SC transactions with condition that SC will not call Signer
		// this allows anonymous voting and numerous other applications
		// otherwise SC receives signer as all zeroes
	}

	action_code := rpc.SC_ACTION(tx.SCDATA.Value(rpc.SCACTION, rpc.DataUint64).(uint64))

	switch action_code {
	case rpc.SC_INSTALL: // request to install an SC
		if !tx.SCDATA.Has(rpc.SCCODE, rpc.DataString) { // but only it is present
			break
		}
		sc_code := tx.SCDATA.Value(rpc.SCCODE, rpc.DataString).(string)
		if sc_code == "" { // no code provided nothing to do
			err = fmt.Errorf("no code provided")
			break
		}

		// check whether sc can be parsed
		//var sc_parsed dvm.SmartContract
		pos := ""
		var sc dvm.SmartContract

		if sc, pos, err = dvm.ParseSmartContract(sc_code); err != nil {
			logger.V(2).Error(err, "error Parsing sc", "txid", txhash, "pos", pos)
			break
		}

		meta := dvm.SC_META_DATA{}
		if _, ok := sc.Functions["InitializePrivate"]; ok {
			meta.Type = 1
		}

		w_sc_data_tree = dvm.Wrapped_tree(cache, ss, scid)

		// install SC, should we check for sanity now, why or why not
		w_sc_data_tree.Put(dvm.SC_Code_Key(scid), dvm.Variable{Type: dvm.String, ValueString: sc_code}.MarshalBinaryPanic())
		w_sc_tree.Put(dvm.SC_Meta_Key(scid), meta.MarshalBinary())

		entrypoint := "Initialize"
		if meta.Type == 1 { // if its a a private SC
			entrypoint = "InitializePrivate"
		}

		balance, sc_parsed, found := dvm.ReadSC(w_sc_tree, w_sc_data_tree, scid)
		if found {
			gascompute, gasstorage, err = dvm.Execute_sc_function(w_sc_tree, w_sc_data_tree, scid, bl_height, bl_topoheight, bl_timestamp, blid, txhash, sc_parsed, entrypoint, 1, balance, signer, incoming_value, tx.SCDATA, tx.Fees(), chain.simulator)
		} else {
			logger.V(1).Error(nil, "SC not found", "scid", scid)
			err = fmt.Errorf("SC not found %s", scid)
		}

		if err != nil {
			return
		}

		//fmt.Printf("Error status after initializing SC %s\n",err)

	case rpc.SC_CALL: // trigger a CALL
		if !tx.SCDATA.Has(rpc.SCID, rpc.DataHash) { // but only if it is present
			err = fmt.Errorf("no scid provided")
			break
		}
		if !tx.SCDATA.Has("entrypoint", rpc.DataString) { // but only if it is present
			err = fmt.Errorf("no entrypoint provided")
			break
		}

		scid = tx.SCDATA.Value(rpc.SCID, rpc.DataHash).(crypto.Hash)

		if _, err = w_sc_tree.Get(dvm.SC_Meta_Key(scid)); err != nil {
			err = fmt.Errorf("scid %s not installed", scid)
			return
		}

		w_sc_data_tree = dvm.Wrapped_tree(cache, ss, scid)

		entrypoint := tx.SCDATA.Value("entrypoint", rpc.DataString).(string)
		//fmt.Printf("We must call the SC %s function\n", entrypoint)

		balance, sc_parsed, found := dvm.ReadSC(w_sc_tree, w_sc_data_tree, scid)
		if found {
			gascompute, gasstorage, err = dvm.Execute_sc_function(w_sc_tree, w_sc_data_tree, scid, bl_height, bl_topoheight, bl_timestamp, blid, txhash, sc_parsed, entrypoint, 1, balance, signer, incoming_value, tx.SCDATA, tx.Fees(), chain.simulator)
		} else {
			logger.V(1).Error(nil, "SC not found", "scid", scid)
			err = fmt.Errorf("SC not found %s", scid)
		}

	default: // unknown  what to do
		err = fmt.Errorf("unknown action what to do scid %x", scid)
		return
	}

	// we must commit all the changes
	// check whether we are not overflowing/underflowing, means SC is not over sending
	if err == nil {
		err = dvm.SanityCheckExternalTransfers(w_sc_data_tree, balance_tree, scid)
	}

	if err != nil { // error occured, give everything to SC, since we may not have information to send them back
		if chain.simulator {
			logger.Error(err, "error executing sc", "txid", txhash)
		}

		if signer, err1 := Extract_signer(&tx); err1 == nil { // if we can identify sender, return funds to him
			dvm.ErrorRevert(ss, cache, balance_tree, signer, scid, incoming_value)
		} else { //  we could not extract signer, give burned funds to SC
			dvm.ErrorRevert(ss, cache, balance_tree, signer, scid, incoming_value)
		}

		return
	}
	dvm.ProcessExternal(ss, cache, balance_tree, signer, scid, w_sc_data_tree, w_sc_tree)

	//c := w_sc_data_tree.tree.Cursor()
	//for k, v, err := c.First(); err == nil; k, v, err = c.Next() {
	//	fmt.Printf("key=%s (%x), value=%s\n", k, k, v)
	//}
	//fmt.Printf("cursor complete\n")

	//h, err := data_tree.Hash()
	//fmt.Printf("%s successfully executed sc_call data_tree hash %x %s\n", scid, h, err)

	return tx.Fees(), nil
}

// func extract signer from a tx, if possible
// extract signer is only possible if ring size is 2
func Extract_signer(tx *transaction.Transaction) (signer [33]byte, err error) {
	for t := range tx.Payloads {
		if uint64(len(tx.Payloads[t].Statement.Publickeylist_compressed)) != tx.Payloads[t].Statement.RingSize {
			panic("tx is not expanded")
			return signer, fmt.Errorf("tx is not expanded")
		}
		if tx.Payloads[t].SCID.IsZero() && tx.Payloads[t].Statement.RingSize == 2 {
			parity := tx.Payloads[t].Proof.Parity()
			for i := 0; i < int(tx.Payloads[t].Statement.RingSize); i++ {
				if (i%2 == 0) == parity { // this condition is well thought out and works good enough
					copy(signer[:], tx.Payloads[t].Statement.Publickeylist_compressed[i][:])
					return
				}
			}

		}
	}

	return signer, fmt.Errorf("unknown signer")
}