package tangle

import (
	"fmt"
	"sync"
	"time"

	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
	"github.com/iotaledger/hive.go/byteutils"
	"github.com/iotaledger/hive.go/marshalutil"
	"github.com/iotaledger/hive.go/objectstorage"
	"github.com/iotaledger/hive.go/stringify"
)

// TransactionMetadata contains the information of a Transaction, that are based on our local perception of things (i.e. if it is
// solid, or when we it became solid).
type TransactionMetadata struct {
	objectstorage.StorableObjectFlags

	id                 transaction.ID
	branchID           branchmanager.BranchID
	solid              bool
	preferred          bool
	finalized          bool
	liked              bool
	confirmed          bool
	rejected           bool
	solidificationTime time.Time
	finalizationTime   time.Time

	branchIDMutex           sync.RWMutex
	solidMutex              sync.RWMutex
	preferredMutex          sync.RWMutex
	finalizedMutex          sync.RWMutex
	likedMutex              sync.RWMutex
	confirmedMutex          sync.RWMutex
	rejectedMutex           sync.RWMutex
	solidificationTimeMutex sync.RWMutex
}

// NewTransactionMetadata is the constructor for the TransactionMetadata type.
func NewTransactionMetadata(id transaction.ID) *TransactionMetadata {
	return &TransactionMetadata{
		id: id,
	}
}

// TransactionMetadataFromBytes unmarshals a TransactionMetadata object from a sequence of bytes.
// It either creates a new object or fills the optionally provided object with the parsed information.
func TransactionMetadataFromBytes(bytes []byte) (result *TransactionMetadata, consumedBytes int, err error) {
	marshalUtil := marshalutil.New(bytes)
	result, err = ParseTransactionMetadata(marshalUtil)
	consumedBytes = marshalUtil.ReadOffset()

	return
}

// TransactionMetadataFromObjectStorage get's called when we restore TransactionMetadata from the storage.
// In contrast to other database models, it unmarshals some information from the key so we simply store the key before
// it gets handed over to UnmarshalObjectStorageValue (by the ObjectStorage).
func TransactionMetadataFromObjectStorage(key []byte, data []byte) (result objectstorage.StorableObject, err error) {
	result, _, err = TransactionMetadataFromBytes(byteutils.ConcatBytes(key, data))
	if err != nil {
		err = fmt.Errorf("failed to parse transaction metadata from object storage: %w", err)
	}

	return
}

// ParseTransactionMetadata is a wrapper for simplified unmarshaling of TransactionMetadata objects from a byte stream using the marshalUtil package.
func ParseTransactionMetadata(marshalUtil *marshalutil.MarshalUtil) (result *TransactionMetadata, err error) {
	result = &TransactionMetadata{}

	if result.id, err = transaction.ParseID(marshalUtil); err != nil {
		err = fmt.Errorf("failed to parse transaction ID of transaction metadata: %w", err)
		return
	}
	if result.branchID, err = branchmanager.ParseBranchID(marshalUtil); err != nil {
		err = fmt.Errorf("failed to parse branch ID of transaction metadata: %w", err)
		return
	}
	if result.solidificationTime, err = marshalUtil.ReadTime(); err != nil {
		err = fmt.Errorf("failed to parse solidification time of transaction metadata: %w", err)
		return
	}
	if result.finalizationTime, err = marshalUtil.ReadTime(); err != nil {
		err = fmt.Errorf("failed to parse finalization time of transaction metadata: %w", err)
		return
	}
	if result.solid, err = marshalUtil.ReadBool(); err != nil {
		err = fmt.Errorf("failed to parse 'solid' of transaction metadata: %w", err)
		return
	}
	if result.preferred, err = marshalUtil.ReadBool(); err != nil {
		err = fmt.Errorf("failed to parse 'preferred' of transaction metadata: %w", err)
		return
	}
	if result.finalized, err = marshalUtil.ReadBool(); err != nil {
		err = fmt.Errorf("failed to parse 'finalized' of transaction metadata: %w", err)
		return
	}
	if result.liked, err = marshalUtil.ReadBool(); err != nil {
		err = fmt.Errorf("failed to parse 'liked' of transaction metadata: %w", err)
		return
	}
	if result.confirmed, err = marshalUtil.ReadBool(); err != nil {
		err = fmt.Errorf("failed to parse 'confirmed' of transaction metadata: %w", err)
		return
	}
	if result.rejected, err = marshalUtil.ReadBool(); err != nil {
		err = fmt.Errorf("failed to parse 'rejected' of transaction metadata: %w", err)
		return
	}

	return
}

// ID return the id of the Transaction that this TransactionMetadata is associated to.
func (transactionMetadata *TransactionMetadata) ID() transaction.ID {
	return transactionMetadata.id
}

// BranchID returns the identifier of the Branch, that this transaction is booked into.
func (transactionMetadata *TransactionMetadata) BranchID() branchmanager.BranchID {
	transactionMetadata.branchIDMutex.RLock()
	defer transactionMetadata.branchIDMutex.RUnlock()

	return transactionMetadata.branchID
}

// setBranchID is the setter for the branch id. It returns true if the value of the flag has been updated.
func (transactionMetadata *TransactionMetadata) setBranchID(branchID branchmanager.BranchID) (modified bool) {
	transactionMetadata.branchIDMutex.RLock()
	if transactionMetadata.branchID == branchID {
		transactionMetadata.branchIDMutex.RUnlock()

		return
	}

	transactionMetadata.branchIDMutex.RUnlock()
	transactionMetadata.branchIDMutex.Lock()
	defer transactionMetadata.branchIDMutex.Unlock()

	if transactionMetadata.branchID == branchID {
		return
	}

	transactionMetadata.branchID = branchID
	transactionMetadata.SetModified()
	modified = true

	return
}

// Conflicting returns true if the Transaction has been forked into its own Branch and there is a vote going on.
func (transactionMetadata *TransactionMetadata) Conflicting() bool {
	return transactionMetadata.BranchID() == branchmanager.NewBranchID(transactionMetadata.ID())
}

// Solid returns true if the Transaction has been marked as solid.
func (transactionMetadata *TransactionMetadata) Solid() (result bool) {
	transactionMetadata.solidMutex.RLock()
	result = transactionMetadata.solid
	transactionMetadata.solidMutex.RUnlock()

	return
}

// setSolid marks a Transaction as either solid or not solid.
// It returns true if the solid flag was changes and automatically updates the solidificationTime as well.
func (transactionMetadata *TransactionMetadata) setSolid(solid bool) (modified bool) {
	transactionMetadata.solidMutex.RLock()
	if transactionMetadata.solid != solid {
		transactionMetadata.solidMutex.RUnlock()

		transactionMetadata.solidMutex.Lock()
		if transactionMetadata.solid != solid {
			transactionMetadata.solid = solid
			if solid {
				transactionMetadata.solidificationTimeMutex.Lock()
				transactionMetadata.solidificationTime = time.Now()
				transactionMetadata.solidificationTimeMutex.Unlock()
			}

			transactionMetadata.SetModified()

			modified = true
		}
		transactionMetadata.solidMutex.Unlock()

	} else {
		transactionMetadata.solidMutex.RUnlock()
	}

	return
}

// Preferred returns true if the transaction is considered to be the first valid spender of all of its Inputs.
func (transactionMetadata *TransactionMetadata) Preferred() (result bool) {
	transactionMetadata.preferredMutex.RLock()
	defer transactionMetadata.preferredMutex.RUnlock()

	return transactionMetadata.preferred
}

// setPreferred updates the preferred flag of the transaction. It is defined as a private setter because updating the
// preferred flag causes changes in other transactions and branches as well. This means that we need additional logic
// in the tangle. To update the preferred flag of a transaction, we need to use Tangle.SetTransactionPreferred(bool).
func (transactionMetadata *TransactionMetadata) setPreferred(preferred bool) (modified bool) {
	transactionMetadata.preferredMutex.RLock()
	if transactionMetadata.preferred == preferred {
		transactionMetadata.preferredMutex.RUnlock()

		return
	}

	transactionMetadata.preferredMutex.RUnlock()
	transactionMetadata.preferredMutex.Lock()
	defer transactionMetadata.preferredMutex.Unlock()

	if transactionMetadata.preferred == preferred {
		return
	}

	transactionMetadata.preferred = preferred
	transactionMetadata.SetModified()
	modified = true

	return
}

// setFinalized allows us to set the finalized flag on the transactions. Finalized transactions will not be forked when
// a conflict arrives later.
func (transactionMetadata *TransactionMetadata) setFinalized(finalized bool) (modified bool) {
	transactionMetadata.finalizedMutex.RLock()
	if transactionMetadata.finalized == finalized {
		transactionMetadata.finalizedMutex.RUnlock()

		return
	}

	transactionMetadata.finalizedMutex.RUnlock()
	transactionMetadata.finalizedMutex.Lock()
	defer transactionMetadata.finalizedMutex.Unlock()

	if transactionMetadata.finalized == finalized {
		return
	}

	transactionMetadata.finalized = finalized
	transactionMetadata.SetModified()
	if finalized {
		transactionMetadata.finalizationTime = time.Now()
	}
	modified = true

	return
}

// Finalized returns true, if the decision if this transaction is liked or not has been finalized by consensus already.
func (transactionMetadata *TransactionMetadata) Finalized() bool {
	transactionMetadata.finalizedMutex.RLock()
	defer transactionMetadata.finalizedMutex.RUnlock()

	return transactionMetadata.finalized
}

// Liked returns true if the Transaction was marked as liked.
func (transactionMetadata *TransactionMetadata) Liked() bool {
	transactionMetadata.likedMutex.RLock()
	defer transactionMetadata.likedMutex.RUnlock()

	return transactionMetadata.liked
}

// setLiked modifies the liked flag of the given Transaction. It returns true if the value has been updated.
func (transactionMetadata *TransactionMetadata) setLiked(liked bool) (modified bool) {
	transactionMetadata.likedMutex.RLock()
	if transactionMetadata.liked == liked {
		transactionMetadata.likedMutex.RUnlock()

		return
	}

	transactionMetadata.likedMutex.RUnlock()
	transactionMetadata.likedMutex.Lock()
	defer transactionMetadata.likedMutex.Unlock()

	if transactionMetadata.liked == liked {
		return
	}

	transactionMetadata.liked = liked
	transactionMetadata.SetModified()
	modified = true

	return
}

// Confirmed returns true if the Transaction was marked as confirmed.
func (transactionMetadata *TransactionMetadata) Confirmed() bool {
	transactionMetadata.confirmedMutex.RLock()
	defer transactionMetadata.confirmedMutex.RUnlock()

	return transactionMetadata.confirmed
}

// setConfirmed modifies the confirmed flag of the given Transaction. It returns true if the value has been updated.
func (transactionMetadata *TransactionMetadata) setConfirmed(confirmed bool) (modified bool) {
	transactionMetadata.confirmedMutex.RLock()
	if transactionMetadata.confirmed == confirmed {
		transactionMetadata.confirmedMutex.RUnlock()

		return
	}

	transactionMetadata.confirmedMutex.RUnlock()
	transactionMetadata.confirmedMutex.Lock()
	defer transactionMetadata.confirmedMutex.Unlock()

	if transactionMetadata.confirmed == confirmed {
		return
	}

	transactionMetadata.confirmed = confirmed
	transactionMetadata.SetModified()
	modified = true

	return
}

// Rejected returns true if the Transaction was marked as confirmed.
func (transactionMetadata *TransactionMetadata) Rejected() bool {
	transactionMetadata.rejectedMutex.RLock()
	defer transactionMetadata.rejectedMutex.RUnlock()

	return transactionMetadata.rejected
}

// setRejected modifies the rejected flag of the given Transaction. It returns true if the value has been updated.
func (transactionMetadata *TransactionMetadata) setRejected(rejected bool) (modified bool) {
	transactionMetadata.rejectedMutex.RLock()
	if transactionMetadata.rejected == rejected {
		transactionMetadata.rejectedMutex.RUnlock()

		return
	}

	transactionMetadata.rejectedMutex.RUnlock()
	transactionMetadata.rejectedMutex.Lock()
	defer transactionMetadata.rejectedMutex.Unlock()

	if transactionMetadata.rejected == rejected {
		return
	}

	transactionMetadata.rejected = rejected
	transactionMetadata.SetModified()
	modified = true

	return
}

// FinalizationTime returns the time when this transaction was finalized.
func (transactionMetadata *TransactionMetadata) FinalizationTime() time.Time {
	transactionMetadata.finalizedMutex.RLock()
	defer transactionMetadata.finalizedMutex.RUnlock()

	return transactionMetadata.finalizationTime
}

// SolidificationTime returns the time when the Transaction was marked to be solid.
func (transactionMetadata *TransactionMetadata) SolidificationTime() time.Time {
	transactionMetadata.solidificationTimeMutex.RLock()
	defer transactionMetadata.solidificationTimeMutex.RUnlock()

	return transactionMetadata.solidificationTime
}

// Bytes marshals the TransactionMetadata object into a sequence of bytes.
func (transactionMetadata *TransactionMetadata) Bytes() []byte {
	return byteutils.ConcatBytes(transactionMetadata.ObjectStorageKey(), transactionMetadata.ObjectStorageValue())
}

// String creates a human readable version of the metadata (for debug purposes).
func (transactionMetadata *TransactionMetadata) String() string {
	return stringify.Struct("transaction.TransactionMetadata",
		stringify.StructField("id", transactionMetadata.ID()),
		stringify.StructField("branchId", transactionMetadata.BranchID()),
		stringify.StructField("solid", transactionMetadata.Solid()),
		stringify.StructField("solidificationTime", transactionMetadata.SolidificationTime()),
	)
}

// ObjectStorageKey returns the key that is used to identify the TransactionMetadata in the objectstorage.
func (transactionMetadata *TransactionMetadata) ObjectStorageKey() []byte {
	return transactionMetadata.id.Bytes()
}

// Update is disabled and panics if it ever gets called - updates are supposed to happen through the setters.
func (transactionMetadata *TransactionMetadata) Update(other objectstorage.StorableObject) {
	panic("update forbidden")
}

// ObjectStorageValue marshals the TransactionMetadata object into a sequence of bytes and matches the encoding.BinaryMarshaler
// interface.
func (transactionMetadata *TransactionMetadata) ObjectStorageValue() []byte {
	return marshalutil.New(branchmanager.BranchIDLength + 2*marshalutil.TIME_SIZE + 6*marshalutil.BOOL_SIZE).
		WriteBytes(transactionMetadata.BranchID().Bytes()).
		WriteTime(transactionMetadata.SolidificationTime()).
		WriteTime(transactionMetadata.FinalizationTime()).
		WriteBool(transactionMetadata.Solid()).
		WriteBool(transactionMetadata.Preferred()).
		WriteBool(transactionMetadata.Finalized()).
		WriteBool(transactionMetadata.Liked()).
		WriteBool(transactionMetadata.Confirmed()).
		WriteBool(transactionMetadata.Rejected()).
		Bytes()
}

// CachedTransactionMetadata is a wrapper for the object storage, that takes care of type casting the TransactionMetadata objects.
// Since go does not have generics (yet), the object storage works based on the generic "interface{}" type, which means
// that we have to regularly type cast the returned objects, to match the expected type. To reduce the burden of
// manually managing these type, we create a wrapper that does this for us. This way, we can consistently handle the
// specialized types of TransactionMetadata, without having to manually type cast over and over again.
type CachedTransactionMetadata struct {
	objectstorage.CachedObject
}

// Retain overrides the underlying method to return a new CachedTransactionMetadata instead of a generic CachedObject.
func (cachedTransactionMetadata *CachedTransactionMetadata) Retain() *CachedTransactionMetadata {
	return &CachedTransactionMetadata{cachedTransactionMetadata.CachedObject.Retain()}
}

// Consume  overrides the underlying method to use a CachedTransactionMetadata object instead of a generic CachedObject in the
// consumer).
func (cachedTransactionMetadata *CachedTransactionMetadata) Consume(consumer func(metadata *TransactionMetadata)) bool {
	return cachedTransactionMetadata.CachedObject.Consume(func(object objectstorage.StorableObject) {
		consumer(object.(*TransactionMetadata))
	})
}

// Unwrap provides a way to retrieve a type casted version of the underlying object.
func (cachedTransactionMetadata *CachedTransactionMetadata) Unwrap() *TransactionMetadata {
	untypedTransaction := cachedTransactionMetadata.Get()
	if untypedTransaction == nil {
		return nil
	}

	typeCastedTransaction := untypedTransaction.(*TransactionMetadata)
	if typeCastedTransaction == nil || typeCastedTransaction.IsDeleted() {
		return nil
	}

	return typeCastedTransaction
}

// Interface contract: make compiler warn if the interface is not implemented correctly.
var _ objectstorage.StorableObject = &TransactionMetadata{}