package tangle

import (
	"sync"
	"time"

	"github.com/iotaledger/goshimmer/packages/bitutils"
	"github.com/iotaledger/goshimmer/packages/errors"
	"github.com/iotaledger/goshimmer/packages/ternary"
	"github.com/iotaledger/goshimmer/packages/typeconversion"
)

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

type TransactionMetadata struct {
	hash              ternary.Trinary
	hashMutex         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 NewTransactionMetadata(hash ternary.Trinary) *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() ternary.Trinary {
	metaData.hashMutex.RLock()
	defer metaData.hashMutex.RUnlock()

	return metaData.hash
}

func (metaData *TransactionMetadata) SetHash(hash ternary.Trinary) {
	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) 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) {
	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)
		}
	} else {
		metaData.solidMutex.RUnlock()
	}
}

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 marshalling functions ////////////////////////////////////////////////////////////////////////////////////////

func (metadata *TransactionMetadata) Marshal() ([]byte, errors.IdentifiableError) {
	marshalledMetadata := make([]byte, MARSHALLED_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(marshalledMetadata[MARSHALLED_HASH_START:MARSHALLED_HASH_END], metadata.hash.CastToBytes())

	marshalledReceivedTime, err := metadata.receivedTime.MarshalBinary()
	if err != nil {
		return nil, ErrMarshallFailed.Derive(err, "failed to marshal received time")
	}
	copy(marshalledMetadata[MARSHALLED_RECEIVED_TIME_START:MARSHALLED_RECEIVED_TIME_END], marshalledReceivedTime)

	var booleanFlags bitutils.BitMask
	if metadata.solid {
		booleanFlags = booleanFlags.SetFlag(0)
	}
	if metadata.liked {
		booleanFlags = booleanFlags.SetFlag(1)
	}
	if metadata.finalized {
		booleanFlags = booleanFlags.SetFlag(2)
	}
	marshalledMetadata[MARSHALLED_FLAGS_START] = byte(booleanFlags)

	return marshalledMetadata, 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 = ternary.Trinary(typeconversion.BytesToString(data[MARSHALLED_HASH_START:MARSHALLED_HASH_END]))

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

	booleanFlags := bitutils.BitMask(data[MARSHALLED_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 ///////////////////////////////////////////////////////////////////////////////////////////////////////////

// region database functions ///////////////////////////////////////////////////////////////////////////////////////////

func (metadata *TransactionMetadata) Store() errors.IdentifiableError {
	if metadata.GetModified() {
		marshalledMetadata, err := metadata.Marshal()
		if err != nil {
			return err
		}

		if err := transactionMetadataDatabase.Set(metadata.GetHash().CastToBytes(), marshalledMetadata); err != nil {
			return ErrDatabaseError.Derive(err, "failed to store the transaction")
		}

		metadata.SetModified(false)
	}

	return nil
}

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

// region constants and variables //////////////////////////////////////////////////////////////////////////////////////

const (
	MARSHALLED_HASH_START          = 0
	MARSHALLED_RECEIVED_TIME_START = MARSHALLED_HASH_END
	MARSHALLED_FLAGS_START         = MARSHALLED_RECEIVED_TIME_END

	MARSHALLED_HASH_END          = MARSHALLED_HASH_START + MARSHALLED_HASH_SIZE
	MARSHALLED_RECEIVED_TIME_END = MARSHALLED_RECEIVED_TIME_START + MARSHALLED_RECEIVED_TIME_SIZE
	MARSHALLED_FLAGS_END         = MARSHALLED_FLAGS_START + MARSHALLED_FLAGS_SIZE

	MARSHALLED_HASH_SIZE          = 81
	MARSHALLED_RECEIVED_TIME_SIZE = 15
	MARSHALLED_FLAGS_SIZE         = 1

	MARSHALLED_TOTAL_SIZE = MARSHALLED_FLAGS_END
)

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