package meta_transaction

import (
	"sync"

	"github.com/iotaledger/goshimmer/packages/curl"
	"github.com/iotaledger/goshimmer/packages/ternary"
)

type MetaTransaction struct {
	hash            *ternary.Trytes
	weightMagnitude int

	shardMarker           *ternary.Trytes
	trunkTransactionHash  *ternary.Trytes
	branchTransactionHash *ternary.Trytes
	head                  *bool
	tail                  *bool
	transactionType       *ternary.Trytes
	data                  ternary.Trits
	modified              bool

	hasherMutex                sync.RWMutex
	hashMutex                  sync.RWMutex
	shardMarkerMutex           sync.RWMutex
	trunkTransactionHashMutex  sync.RWMutex
	branchTransactionHashMutex sync.RWMutex
	headMutex                  sync.RWMutex
	tailMutex                  sync.RWMutex
	transactionTypeMutex       sync.RWMutex
	dataMutex                  sync.RWMutex
	bytesMutex                 sync.RWMutex
	modifiedMutex              sync.RWMutex

	trits ternary.Trits
	bytes []byte
}

func New() *MetaTransaction {
	return FromTrits(make(ternary.Trits, MARSHALED_TOTAL_SIZE))
}

func FromTrits(trits ternary.Trits) *MetaTransaction {
	return &MetaTransaction{
		trits: trits,
	}
}

func FromBytes(bytes []byte) (result *MetaTransaction) {
	result = FromTrits(ternary.BytesToTrits(bytes)[:MARSHALED_TOTAL_SIZE])
	result.bytes = bytes

	return
}

func (this *MetaTransaction) BlockHasher() {
	this.hasherMutex.RLock()
}

func (this *MetaTransaction) UnblockHasher() {
	this.hasherMutex.RUnlock()
}

func (this *MetaTransaction) ReHash() {
	this.hashMutex.Lock()
	defer this.hashMutex.Unlock()
	this.hash = nil

	this.bytesMutex.Lock()
	defer this.bytesMutex.Unlock()
	this.bytes = nil
}

// retrieves the hash of the transaction
func (this *MetaTransaction) GetHash() (result ternary.Trytes) {
	this.hashMutex.RLock()
	if this.hash == nil {
		this.hashMutex.RUnlock()
		this.hashMutex.Lock()
		defer this.hashMutex.Unlock()
		if this.hash == nil {
			this.hasherMutex.Lock()
			this.parseHashRelatedDetails()
			this.hasherMutex.Unlock()
		}
	} else {
		defer this.hashMutex.RUnlock()
	}

	result = *this.hash

	return
}

// retrieves weight magnitude of the transaction (amount of pow invested)
func (this *MetaTransaction) GetWeightMagnitude() (result int) {
	this.hashMutex.RLock()
	if this.hash == nil {
		this.hashMutex.RUnlock()
		this.hashMutex.Lock()
		defer this.hashMutex.Unlock()
		if this.hash == nil {
			this.hasherMutex.Lock()
			this.parseHashRelatedDetails()
			this.hasherMutex.Unlock()
		}
	} else {
		defer this.hashMutex.RUnlock()
	}

	result = this.weightMagnitude

	return
}

// hashes the transaction using curl (without locking - internal usage)
func (this *MetaTransaction) parseHashRelatedDetails() {
	hashTrits := <-curl.CURLP81.Hash(this.trits)
	hashTrytes := hashTrits.ToTrytes()

	this.hash = &hashTrytes
	this.weightMagnitude = hashTrits.TrailingZeroes()
}

// getter for the shard marker (supports concurrency)
func (this *MetaTransaction) GetShardMarker() (result ternary.Trytes) {
	this.shardMarkerMutex.RLock()
	if this.shardMarker == nil {
		this.shardMarkerMutex.RUnlock()
		this.shardMarkerMutex.Lock()
		defer this.shardMarkerMutex.Unlock()
		if this.shardMarker == nil {
			shardMarker := this.trits[SHARD_MARKER_OFFSET:SHARD_MARKER_END].ToTrytes()

			this.shardMarker = &shardMarker
		}
	} else {
		defer this.shardMarkerMutex.RUnlock()
	}

	result = *this.shardMarker

	return
}

// setter for the shard marker (supports concurrency)
func (this *MetaTransaction) SetShardMarker(shardMarker ternary.Trytes) bool {
	this.shardMarkerMutex.RLock()
	if this.shardMarker == nil || *this.shardMarker != shardMarker {
		this.shardMarkerMutex.RUnlock()
		this.shardMarkerMutex.Lock()
		defer this.shardMarkerMutex.Unlock()
		if this.shardMarker == nil || *this.shardMarker != shardMarker {
			this.shardMarker = &shardMarker

			this.hasherMutex.RLock()
			copy(this.trits[SHARD_MARKER_OFFSET:SHARD_MARKER_END], shardMarker.ToTrits()[:SHARD_MARKER_SIZE])
			this.hasherMutex.RUnlock()

			this.SetModified(true)
			this.ReHash()

			return true
		}
	} else {
		this.shardMarkerMutex.RUnlock()
	}

	return false
}

// getter for the bundleHash (supports concurrency)
func (this *MetaTransaction) GetTrunkTransactionHash() (result ternary.Trytes) {
	this.trunkTransactionHashMutex.RLock()
	if this.trunkTransactionHash == nil {
		this.trunkTransactionHashMutex.RUnlock()
		this.trunkTransactionHashMutex.Lock()
		defer this.trunkTransactionHashMutex.Unlock()
		if this.trunkTransactionHash == nil {
			trunkTransactionHash := this.trits[TRUNK_TRANSACTION_HASH_OFFSET:TRUNK_TRANSACTION_HASH_END].ToTrytes()

			this.trunkTransactionHash = &trunkTransactionHash
		}
	} else {
		defer this.trunkTransactionHashMutex.RUnlock()
	}

	result = *this.trunkTransactionHash

	return
}

// setter for the trunkTransactionHash (supports concurrency)
func (this *MetaTransaction) SetTrunkTransactionHash(trunkTransactionHash ternary.Trytes) bool {
	this.trunkTransactionHashMutex.RLock()
	if this.trunkTransactionHash == nil || *this.trunkTransactionHash != trunkTransactionHash {
		this.trunkTransactionHashMutex.RUnlock()
		this.trunkTransactionHashMutex.Lock()
		defer this.trunkTransactionHashMutex.Unlock()
		if this.trunkTransactionHash == nil || *this.trunkTransactionHash != trunkTransactionHash {
			this.trunkTransactionHash = &trunkTransactionHash

			this.hasherMutex.RLock()
			copy(this.trits[TRUNK_TRANSACTION_HASH_OFFSET:TRUNK_TRANSACTION_HASH_END], trunkTransactionHash.ToTrits()[:TRUNK_TRANSACTION_HASH_SIZE])
			this.hasherMutex.RUnlock()

			this.SetModified(true)
			this.ReHash()

			return true
		}
	} else {
		this.trunkTransactionHashMutex.RUnlock()
	}

	return false
}

// getter for the bundleHash (supports concurrency)
func (this *MetaTransaction) GetBranchTransactionHash() (result ternary.Trytes) {
	this.branchTransactionHashMutex.RLock()
	if this.branchTransactionHash == nil {
		this.branchTransactionHashMutex.RUnlock()
		this.branchTransactionHashMutex.Lock()
		defer this.branchTransactionHashMutex.Unlock()
		if this.branchTransactionHash == nil {
			branchTransactionHash := this.trits[BRANCH_TRANSACTION_HASH_OFFSET:BRANCH_TRANSACTION_HASH_END].ToTrytes()

			this.branchTransactionHash = &branchTransactionHash
		}
	} else {
		defer this.branchTransactionHashMutex.RUnlock()
	}

	result = *this.branchTransactionHash

	return
}

// setter for the trunkTransactionHash (supports concurrency)
func (this *MetaTransaction) SetBranchTransactionHash(branchTransactionHash ternary.Trytes) bool {
	this.branchTransactionHashMutex.RLock()
	if this.branchTransactionHash == nil || *this.branchTransactionHash != branchTransactionHash {
		this.branchTransactionHashMutex.RUnlock()
		this.branchTransactionHashMutex.Lock()
		defer this.branchTransactionHashMutex.Unlock()
		if this.branchTransactionHash == nil || *this.branchTransactionHash != branchTransactionHash {
			this.branchTransactionHash = &branchTransactionHash

			this.hasherMutex.RLock()
			copy(this.trits[BRANCH_TRANSACTION_HASH_OFFSET:BRANCH_TRANSACTION_HASH_END], branchTransactionHash.ToTrits()[:BRANCH_TRANSACTION_HASH_SIZE])
			this.hasherMutex.RUnlock()

			this.SetModified(true)
			this.ReHash()

			return true
		}
	} else {
		this.branchTransactionHashMutex.RUnlock()
	}

	return false
}

// getter for the head flag (supports concurrency)
func (this *MetaTransaction) GetHead() (result bool) {
	this.headMutex.RLock()
	if this.head == nil {
		this.headMutex.RUnlock()
		this.headMutex.Lock()
		defer this.headMutex.Unlock()
		if this.head == nil {
			head := this.trits[HEAD_OFFSET] == 1

			this.head = &head
		}
	} else {
		defer this.headMutex.RUnlock()
	}

	result = *this.head

	return
}

// setter for the head flag (supports concurrency)
func (this *MetaTransaction) SetHead(head bool) bool {
	this.headMutex.RLock()
	if this.head == nil || *this.head != head {
		this.headMutex.RUnlock()
		this.headMutex.Lock()
		defer this.headMutex.Unlock()
		if this.head == nil || *this.head != head {
			this.head = &head

			this.hasherMutex.RLock()
			if head {
				this.trits[HEAD_OFFSET] = 1
			} else {
				this.trits[HEAD_OFFSET] = 0
			}
			this.hasherMutex.RUnlock()

			this.SetModified(true)
			this.ReHash()

			return true
		}
	} else {
		this.headMutex.RUnlock()
	}

	return false
}

// getter for the tail flag (supports concurrency)
func (this *MetaTransaction) GetTail() (result bool) {
	this.tailMutex.RLock()
	if this.tail == nil {
		this.tailMutex.RUnlock()
		this.tailMutex.Lock()
		defer this.tailMutex.Unlock()
		if this.tail == nil {
			tail := this.trits[TAIL_OFFSET] == 1

			this.tail = &tail
		}
	} else {
		defer this.tailMutex.RUnlock()
	}

	result = *this.tail

	return
}

// setter for the tail flag (supports concurrency)
func (this *MetaTransaction) SetTail(tail bool) bool {
	this.tailMutex.RLock()
	if this.tail == nil || *this.tail != tail {
		this.tailMutex.RUnlock()
		this.tailMutex.Lock()
		defer this.tailMutex.Unlock()
		if this.tail == nil || *this.tail != tail {
			this.tail = &tail

			this.hasherMutex.RLock()
			if tail {
				this.trits[TAIL_OFFSET] = 1
			} else {
				this.trits[TAIL_OFFSET] = 0
			}
			this.hasherMutex.RUnlock()

			this.SetModified(true)
			this.ReHash()

			return true
		}
	} else {
		this.tailMutex.RUnlock()
	}

	return false
}

// getter for the transaction type (supports concurrency)
func (this *MetaTransaction) GetTransactionType() (result ternary.Trytes) {
	this.transactionTypeMutex.RLock()
	if this.transactionType == nil {
		this.transactionTypeMutex.RUnlock()
		this.transactionTypeMutex.Lock()
		defer this.transactionTypeMutex.Unlock()
		if this.transactionType == nil {
			transactionType := this.trits[TRANSACTION_TYPE_OFFSET:TRANSACTION_TYPE_END].ToTrytes()

			this.transactionType = &transactionType
		}
	} else {
		defer this.transactionTypeMutex.RUnlock()
	}

	result = *this.transactionType

	return
}

// setter for the transaction type (supports concurrency)
func (this *MetaTransaction) SetTransactionType(transactionType ternary.Trytes) bool {
	this.transactionTypeMutex.RLock()
	if this.transactionType == nil || *this.transactionType != transactionType {
		this.transactionTypeMutex.RUnlock()
		this.transactionTypeMutex.Lock()
		defer this.transactionTypeMutex.Unlock()
		if this.transactionType == nil || *this.transactionType != transactionType {
			this.transactionType = &transactionType

			this.hasherMutex.RLock()
			copy(this.trits[TRANSACTION_TYPE_OFFSET:TRANSACTION_TYPE_END], transactionType.ToTrits()[:TRANSACTION_TYPE_SIZE])
			this.hasherMutex.RUnlock()

			this.SetModified(true)
			this.ReHash()

			return true
		}
	} else {
		this.transactionTypeMutex.RUnlock()
	}

	return false
}

// getter for the data slice (supports concurrency)
func (this *MetaTransaction) GetData() (result ternary.Trits) {
	this.dataMutex.RLock()
	if this.data == nil {
		this.dataMutex.RUnlock()
		this.dataMutex.Lock()
		defer this.dataMutex.Unlock()
		if this.data == nil {
			this.data = this.trits[DATA_OFFSET:DATA_END]
		}
	} else {
		defer this.dataMutex.RUnlock()
	}

	result = this.data

	return
}

func (this *MetaTransaction) GetTrits() (result ternary.Trits) {
	result = make(ternary.Trits, len(this.trits))

	this.hasherMutex.Lock()
	copy(result, this.trits)
	this.hasherMutex.Unlock()

	return
}

func (this *MetaTransaction) GetBytes() (result []byte) {
	this.bytesMutex.RLock()
	if this.bytes == nil {
		this.bytesMutex.RUnlock()
		this.bytesMutex.Lock()
		defer this.bytesMutex.Unlock()

		this.hasherMutex.Lock()
		this.bytes = this.trits.ToBytes()
		this.hasherMutex.Unlock()
	} else {
		this.bytesMutex.RUnlock()
	}

	result = make([]byte, len(this.bytes))
	copy(result, this.bytes)

	return
}

// returns true if the transaction contains unsaved changes (supports concurrency)
func (this *MetaTransaction) GetModified() bool {
	this.modifiedMutex.RLock()
	defer this.modifiedMutex.RUnlock()

	return this.modified
}

// sets the modified flag which controls if a transaction is going to be saved (supports concurrency)
func (this *MetaTransaction) SetModified(modified bool) {
	this.modifiedMutex.Lock()
	defer this.modifiedMutex.Unlock()

	this.modified = modified
}