package transactionmetadata

import (
	"sync"
	"time"

	"github.com/iotaledger/goshimmer/packages/errors"
	"github.com/iotaledger/hive.go/bitmask"
	"github.com/iotaledger/hive.go/typeutils"
	"github.com/iotaledger/iota.go/trinary"
)

// region type definition and constructor //////////////////////////////////////////////////////////////////////////////

type TransactionMetadata struct {
	hash                trinary.Trytes
	hashMutex           sync.RWMutex
	bundleHeadHash      trinary.Trytes
	bundleHeadHashMutex sync.RWMutex
	receivedTime        time.Time
	receivedTimeMutex   sync.RWMutex
	solid               bool
	solidMutex          sync.RWMutex
	liked               bool
	likedMutex          sync.RWMutex
	finalized           bool
	finalizedMutex      sync.RWMutex
	modified            bool
	modifiedMutex       sync.RWMutex
}

func New(hash trinary.Trytes) *TransactionMetadata {
	return &TransactionMetadata{
		hash:         hash,
		receivedTime: time.Now(),
		solid:        false,
		liked:        false,
		finalized:    false,
		modified:     true,
	}
}

// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////

// region getters and setters //////////////////////////////////////////////////////////////////////////////////////////

func (metadata *TransactionMetadata) GetHash() trinary.Trytes {
	metadata.hashMutex.RLock()
	defer metadata.hashMutex.RUnlock()

	return metadata.hash
}

func (metadata *TransactionMetadata) SetHash(hash trinary.Trytes) {
	metadata.hashMutex.RLock()
	if metadata.hash != hash {
		metadata.hashMutex.RUnlock()
		metadata.hashMutex.Lock()
		defer metadata.hashMutex.Unlock()
		if metadata.hash != hash {
			metadata.hash = hash

			metadata.SetModified(true)
		}
	} else {
		metadata.hashMutex.RUnlock()
	}
}

func (metadata *TransactionMetadata) GetBundleHeadHash() trinary.Trytes {
	metadata.bundleHeadHashMutex.RLock()
	defer metadata.bundleHeadHashMutex.RUnlock()

	return metadata.bundleHeadHash
}

func (metadata *TransactionMetadata) SetBundleHeadHash(bundleTailHash trinary.Trytes) {
	metadata.bundleHeadHashMutex.RLock()
	if metadata.bundleHeadHash != bundleTailHash {
		metadata.bundleHeadHashMutex.RUnlock()
		metadata.bundleHeadHashMutex.Lock()
		defer metadata.bundleHeadHashMutex.Unlock()
		if metadata.bundleHeadHash != bundleTailHash {
			metadata.bundleHeadHash = bundleTailHash

			metadata.SetModified(true)
		}
	} else {
		metadata.bundleHeadHashMutex.RUnlock()
	}
}

func (metadata *TransactionMetadata) GetReceivedTime() time.Time {
	metadata.receivedTimeMutex.RLock()
	defer metadata.receivedTimeMutex.RUnlock()

	return metadata.receivedTime
}

func (metadata *TransactionMetadata) SetReceivedTime(receivedTime time.Time) {
	metadata.receivedTimeMutex.RLock()
	if metadata.receivedTime != receivedTime {
		metadata.receivedTimeMutex.RUnlock()
		metadata.receivedTimeMutex.Lock()
		defer metadata.receivedTimeMutex.Unlock()
		if metadata.receivedTime != receivedTime {
			metadata.receivedTime = receivedTime

			metadata.SetModified(true)
		}
	} else {
		metadata.receivedTimeMutex.RUnlock()
	}
}

func (metadata *TransactionMetadata) GetSolid() bool {
	metadata.solidMutex.RLock()
	defer metadata.solidMutex.RUnlock()

	return metadata.solid
}

func (metadata *TransactionMetadata) SetSolid(solid bool) bool {
	metadata.solidMutex.RLock()
	if metadata.solid != solid {
		metadata.solidMutex.RUnlock()
		metadata.solidMutex.Lock()
		defer metadata.solidMutex.Unlock()
		if metadata.solid != solid {
			metadata.solid = solid

			metadata.SetModified(true)

			return true
		}
	} else {
		metadata.solidMutex.RUnlock()
	}

	return false
}

func (metadata *TransactionMetadata) GetLiked() bool {
	metadata.likedMutex.RLock()
	defer metadata.likedMutex.RUnlock()

	return metadata.liked
}

func (metadata *TransactionMetadata) SetLiked(liked bool) {
	metadata.likedMutex.RLock()
	if metadata.liked != liked {
		metadata.likedMutex.RUnlock()
		metadata.likedMutex.Lock()
		defer metadata.likedMutex.Unlock()
		if metadata.liked != liked {
			metadata.liked = liked

			metadata.SetModified(true)
		}
	} else {
		metadata.likedMutex.RUnlock()
	}
}

func (metadata *TransactionMetadata) GetFinalized() bool {
	metadata.finalizedMutex.RLock()
	defer metadata.finalizedMutex.RUnlock()

	return metadata.finalized
}

func (metadata *TransactionMetadata) SetFinalized(finalized bool) {
	metadata.finalizedMutex.RLock()
	if metadata.finalized != finalized {
		metadata.finalizedMutex.RUnlock()
		metadata.finalizedMutex.Lock()
		defer metadata.finalizedMutex.Unlock()
		if metadata.finalized != finalized {
			metadata.finalized = finalized

			metadata.SetModified(true)
		}
	} else {
		metadata.finalizedMutex.RUnlock()
	}
}

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

	return metadata.modified
}

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

	metadata.modified = modified
}

// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////

// region marshaling functions /////////////////////////////////////////////////////////////////////////////////////////

func (metadata *TransactionMetadata) Marshal() ([]byte, errors.IdentifiableError) {
	marshaledMetadata := make([]byte, MARSHALED_TOTAL_SIZE)

	metadata.receivedTimeMutex.RLock()
	defer metadata.receivedTimeMutex.RUnlock()
	metadata.solidMutex.RLock()
	defer metadata.solidMutex.RUnlock()
	metadata.likedMutex.RLock()
	defer metadata.likedMutex.RUnlock()
	metadata.finalizedMutex.RLock()
	defer metadata.finalizedMutex.RUnlock()

	copy(marshaledMetadata[MARSHALED_HASH_START:MARSHALED_HASH_END], typeutils.StringToBytes(metadata.hash))

	marshaledReceivedTime, err := metadata.receivedTime.MarshalBinary()
	if err != nil {
		return nil, ErrMarshallFailed.Derive(err, "failed to marshal received time")
	}
	copy(marshaledMetadata[MARSHALED_RECEIVED_TIME_START:MARSHALED_RECEIVED_TIME_END], marshaledReceivedTime)

	var booleanFlags bitmask.BitMask
	if metadata.solid {
		booleanFlags = booleanFlags.SetFlag(0)
	}
	if metadata.liked {
		booleanFlags = booleanFlags.SetFlag(1)
	}
	if metadata.finalized {
		booleanFlags = booleanFlags.SetFlag(2)
	}
	marshaledMetadata[MARSHALED_FLAGS_START] = byte(booleanFlags)

	return marshaledMetadata, nil
}

func (metadata *TransactionMetadata) Unmarshal(data []byte) errors.IdentifiableError {
	metadata.hashMutex.Lock()
	defer metadata.hashMutex.Unlock()
	metadata.receivedTimeMutex.Lock()
	defer metadata.receivedTimeMutex.Unlock()
	metadata.solidMutex.Lock()
	defer metadata.solidMutex.Unlock()
	metadata.likedMutex.Lock()
	defer metadata.likedMutex.Unlock()
	metadata.finalizedMutex.Lock()
	defer metadata.finalizedMutex.Unlock()

	metadata.hash = trinary.Trytes(typeutils.BytesToString(data[MARSHALED_HASH_START:MARSHALED_HASH_END]))

	if err := metadata.receivedTime.UnmarshalBinary(data[MARSHALED_RECEIVED_TIME_START:MARSHALED_RECEIVED_TIME_END]); err != nil {
		return ErrUnmarshalFailed.Derive(err, "could not unmarshal the received time")
	}

	booleanFlags := bitmask.BitMask(data[MARSHALED_FLAGS_START])
	if booleanFlags.HasFlag(0) {
		metadata.solid = true
	}
	if booleanFlags.HasFlag(1) {
		metadata.liked = true
	}
	if booleanFlags.HasFlag(2) {
		metadata.finalized = true
	}

	return nil
}

// endregion ///////////////////////////////////////////////////////////////////////////////////////////////////////////