package client

import (
	"github.com/iotaledger/goshimmer/packages/model/value_transaction"
	"github.com/iotaledger/iota.go/consts"
	"github.com/iotaledger/iota.go/converter"
	"github.com/iotaledger/iota.go/signing"
	"github.com/iotaledger/iota.go/trinary"
)

type BundleFactory struct {
	inputs  []bundleFactoryInputEntry
	outputs []bundleFactoryOutputEntry
}

func NewBundleFactory() *BundleFactory {
	return &BundleFactory{
		inputs:  make([]bundleFactoryInputEntry, 0),
		outputs: make([]bundleFactoryOutputEntry, 0),
	}
}

func (bundleFactory *BundleFactory) AddInput(address *Address, value int64) {
	bundleFactory.inputs = append(bundleFactory.inputs, bundleFactoryInputEntry{
		address: address,
		value:   value,
	})
}

func (bundleFactory *BundleFactory) AddOutput(address *Address, value int64, message ...string) {
	if len(message) >= 1 {
		messageTrytes, err := converter.ASCIIToTrytes(message[0])
		if err != nil {
			panic(err)
		}

		bundleFactory.outputs = append(bundleFactory.outputs, bundleFactoryOutputEntry{
			address: address,
			value:   value,
			message: trinary.MustPad(messageTrytes, value_transaction.SIGNATURE_MESSAGE_FRAGMENT_SIZE),
		})
	} else {
		bundleFactory.outputs = append(bundleFactory.outputs, bundleFactoryOutputEntry{
			address: address,
			value:   value,
		})
	}
}

func (bundleFactory *BundleFactory) GenerateBundle(branchTransactionHash trinary.Trytes, trunkTransactionHash trinary.Trytes) *Bundle {
	transactions := bundleFactory.generateTransactions()

	bundleHash := bundleFactory.signTransactions(transactions)

	bundleFactory.connectTransactions(transactions, branchTransactionHash, trunkTransactionHash)

	return &Bundle{
		essenceHash:  bundleHash,
		transactions: transactions,
	}
}

func (bundleFactory *BundleFactory) generateTransactions() []*value_transaction.ValueTransaction {
	transactions := make([]*value_transaction.ValueTransaction, 0)

	for _, input := range bundleFactory.inputs {
		transaction := value_transaction.New()
		transaction.SetValue(input.value)
		transaction.SetAddress(input.address.trytes)

		transactions = append(transactions, transaction)

		for i := 1; i < int(input.address.securityLevel); i++ {
			transaction := value_transaction.New()
			transaction.SetValue(0)
			transaction.SetAddress(input.address.trytes)

			transactions = append(transactions, transaction)
		}
	}

	for _, output := range bundleFactory.outputs {
		transaction := value_transaction.New()
		transaction.SetValue(output.value)
		transaction.SetAddress(output.address.trytes)

		if len(output.message) != 0 {
			transaction.SetSignatureMessageFragment(output.message)
		}

		transactions = append(transactions, transaction)
	}

	transactions[0].SetHead(true)
	transactions[len(transactions)-1].SetTail(true)

	return transactions
}

func (bundleFactory *BundleFactory) signTransactions(transactions []*value_transaction.ValueTransaction) trinary.Trytes {
	bundleHash := CalculateBundleHash(transactions)
	normalizedBundleHash := signing.NormalizedBundleHash(bundleHash)

	signedTransactions := 0
	for _, input := range bundleFactory.inputs {
		securityLevel := input.address.securityLevel
		privateKey := input.address.privateKey

		for i := 0; i < int(securityLevel); i++ {
			signedFragTrits, _ := signing.SignatureFragment(
				normalizedBundleHash[i*consts.HashTrytesSize/3:(i+1)*consts.HashTrytesSize/3],
				privateKey[i*consts.KeyFragmentLength:(i+1)*consts.KeyFragmentLength],
			)

			transactions[signedTransactions].SetSignatureMessageFragment(trinary.MustTritsToTrytes(signedFragTrits))

			signedTransactions++
		}
	}

	return bundleHash
}

func (bundleFactory *BundleFactory) connectTransactions(transactions []*value_transaction.ValueTransaction, branchTransactionHash trinary.Trytes, trunkTransactionHash trinary.Trytes) {
	transactionCount := len(transactions)

	transactions[transactionCount-1].SetBranchTransactionHash(branchTransactionHash)
	transactions[transactionCount-1].SetTrunkTransactionHash(trunkTransactionHash)

	for i := transactionCount - 2; i >= 0; i-- {
		transactions[i].SetBranchTransactionHash(branchTransactionHash)
		transactions[i].SetTrunkTransactionHash(transactions[i+1].GetHash())
	}
}

type bundleFactoryInputEntry struct {
	address *Address
	value   int64
}

type bundleFactoryOutputEntry struct {
	address *Address
	value   int64
	message trinary.Trytes
}