package tangle

import (
	"sync"
	"time"

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

// OutputKeyPartitions defines the "layout" of the key. This enables prefix iterations in the objectstorage.
var OutputKeyPartitions = objectstorage.PartitionKey([]int{address.Length, transaction.IDLength}...)

// Output represents the output of a Transaction and contains the balances and the identifiers for this output.
type Output struct {
	address            address.Address
	transactionID      transaction.ID
	branchID           branchmanager.BranchID
	solid              bool
	solidificationTime time.Time
	firstConsumer      transaction.ID
	consumerCount      int
	preferred          bool
	finalized          bool
	liked              bool
	confirmed          bool
	rejected           bool
	balances           []*balance.Balance

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

	objectstorage.StorableObjectFlags
	storageKey []byte
}

// NewOutput creates an Output that contains the balances and identifiers of a Transaction.
func NewOutput(address address.Address, transactionID transaction.ID, branchID branchmanager.BranchID, balances []*balance.Balance) *Output {
	return &Output{
		address:            address,
		transactionID:      transactionID,
		branchID:           branchID,
		solid:              false,
		solidificationTime: time.Time{},
		balances:           balances,

		storageKey: marshalutil.New().WriteBytes(address.Bytes()).WriteBytes(transactionID.Bytes()).Bytes(),
	}
}

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

	return
}

// ParseOutput unmarshals an Output using the given marshalUtil (for easier marshaling/unmarshaling).
func ParseOutput(marshalUtil *marshalutil.MarshalUtil, optionalTargetObject ...*Output) (result *Output, err error) {
	parsedObject, parseErr := marshalUtil.Parse(func(data []byte) (interface{}, int, error) {
		return OutputFromStorageKey(data, optionalTargetObject...)
	})
	if parseErr != nil {
		err = parseErr

		return
	}

	result = parsedObject.(*Output)
	_, err = marshalUtil.Parse(func(data []byte) (parseResult interface{}, parsedBytes int, parseErr error) {
		parsedBytes, parseErr = result.UnmarshalObjectStorageValue(data)

		return
	})

	return
}

// OutputFromStorageKey get's called when we restore a Output 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 OutputFromStorageKey(keyBytes []byte, optionalTargetObject ...*Output) (result *Output, consumedBytes int, err error) {
	// determine the target object that will hold the unmarshaled information
	switch len(optionalTargetObject) {
	case 0:
		result = &Output{}
	case 1:
		result = optionalTargetObject[0]
	default:
		panic("too many arguments in call to OutputFromStorageKey")
	}

	// parse information
	marshalUtil := marshalutil.New(keyBytes)
	result.address, err = address.Parse(marshalUtil)
	if err != nil {
		return
	}
	result.transactionID, err = transaction.ParseID(marshalUtil)
	if err != nil {
		return
	}
	result.storageKey = marshalutil.New(keyBytes[:transaction.OutputIDLength]).Bytes(true)
	consumedBytes = marshalUtil.ReadOffset()

	return
}

// ID returns the identifier of this Output.
func (output *Output) ID() transaction.OutputID {
	return transaction.NewOutputID(output.Address(), output.TransactionID())
}

// Address returns the address that this output belongs to.
func (output *Output) Address() address.Address {
	return output.address
}

// TransactionID returns the id of the Transaction, that created this output.
func (output *Output) TransactionID() transaction.ID {
	return output.transactionID
}

// BranchID returns the id of the ledger state branch, that this output was booked in.
func (output *Output) BranchID() branchmanager.BranchID {
	output.branchIDMutex.RLock()
	defer output.branchIDMutex.RUnlock()

	return output.branchID
}

// setBranchID is the setter for the property that indicates in which ledger state branch the output is booked.
func (output *Output) setBranchID(branchID branchmanager.BranchID) (modified bool) {
	output.branchIDMutex.RLock()
	if output.branchID == branchID {
		output.branchIDMutex.RUnlock()

		return
	}

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

	if output.branchID == branchID {
		return
	}

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

	return
}

// Solid returns true if the output has been marked as solid.
func (output *Output) Solid() bool {
	output.solidMutex.RLock()
	defer output.solidMutex.RUnlock()

	return output.solid
}

// setSolid is the setter of the solid flag. It returns true if the solid flag was modified.
func (output *Output) setSolid(solid bool) (modified bool) {
	output.solidMutex.RLock()
	if output.solid != solid {
		output.solidMutex.RUnlock()

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

			output.SetModified()

			modified = true
		}
		output.solidMutex.Unlock()

	} else {
		output.solidMutex.RUnlock()
	}

	return
}

// SolidificationTime returns the time when this Output was marked to be solid.
func (output *Output) SolidificationTime() time.Time {
	output.solidificationTimeMutex.RLock()
	defer output.solidificationTimeMutex.RUnlock()

	return output.solidificationTime
}

// RegisterConsumer keeps track of the first transaction, that consumed an Output and consequently keeps track of the
// amount of other transactions spending the same Output.
func (output *Output) RegisterConsumer(consumer transaction.ID) (consumerCount int, firstConsumerID transaction.ID) {
	output.consumerMutex.Lock()
	defer output.consumerMutex.Unlock()

	if consumerCount = output.consumerCount; consumerCount == 0 {
		output.firstConsumer = consumer
	}
	output.consumerCount++
	output.SetModified()

	firstConsumerID = output.firstConsumer

	return
}

// ConsumerCount returns the number of transactions that have spent this Output.
func (output *Output) ConsumerCount() int {
	output.consumerMutex.RLock()
	defer output.consumerMutex.RUnlock()

	return output.consumerCount
}

// Preferred returns true if the output belongs to a preferred transaction.
func (output *Output) Preferred() (result bool) {
	output.preferredMutex.RLock()
	defer output.preferredMutex.RUnlock()

	return output.preferred
}

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

		return
	}

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

	if output.preferred == preferred {
		return
	}

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

	return
}

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

		return
	}

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

	if output.finalized == finalized {
		return
	}

	output.finalized = finalized
	output.SetModified()
	modified = true

	return
}

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

	return output.finalized
}

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

	return output.liked
}

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

		return
	}

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

	if output.liked == liked {
		return
	}

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

	return
}

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

	return output.confirmed
}

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

		return
	}

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

	if output.confirmed == confirmed {
		return
	}

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

	return
}

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

	return output.rejected
}

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

		return
	}

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

	if output.rejected == rejected {
		return
	}

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

	return
}

// Balances returns the colored balances (color + balance) that this output contains.
func (output *Output) Balances() []*balance.Balance {
	return output.balances
}

// Bytes marshals the object into a sequence of bytes.
func (output *Output) Bytes() []byte {
	return marshalutil.New().
		WriteBytes(output.ObjectStorageKey()).
		WriteBytes(output.ObjectStorageValue()).
		Bytes()
}

// ObjectStorageKey returns the key that is used to store the object in the database.
// It is required to match StorableObject interface.
func (output *Output) ObjectStorageKey() []byte {
	return marshalutil.New(transaction.OutputIDLength).
		WriteBytes(output.address.Bytes()).
		WriteBytes(output.transactionID.Bytes()).
		Bytes()
}

// ObjectStorageValue marshals the balances into a sequence of bytes - the address and transaction id are stored inside the key
// and are ignored here.
func (output *Output) ObjectStorageValue() []byte {
	// determine amount of balances in the output
	balanceCount := len(output.balances)

	// initialize helper
	marshalUtil := marshalutil.New(branchmanager.BranchIDLength + 6*marshalutil.BOOL_SIZE + marshalutil.TIME_SIZE + transaction.IDLength + marshalutil.UINT32_SIZE + marshalutil.UINT32_SIZE + balanceCount*balance.Length)
	marshalUtil.WriteBytes(output.branchID.Bytes())
	marshalUtil.WriteBool(output.solid)
	marshalUtil.WriteTime(output.solidificationTime)
	marshalUtil.WriteBytes(output.firstConsumer.Bytes())
	marshalUtil.WriteUint32(uint32(output.consumerCount))
	marshalUtil.WriteBool(output.Preferred())
	marshalUtil.WriteBool(output.Finalized())
	marshalUtil.WriteBool(output.Liked())
	marshalUtil.WriteBool(output.Confirmed())
	marshalUtil.WriteBool(output.Rejected())
	marshalUtil.WriteUint32(uint32(balanceCount))
	for _, balanceToMarshal := range output.balances {
		marshalUtil.WriteBytes(balanceToMarshal.Bytes())
	}

	return marshalUtil.Bytes()
}

// UnmarshalObjectStorageValue restores a Output from a serialized version in the ObjectStorage with parts of the object
// being stored in its key rather than the content of the database to reduce storage requirements.
func (output *Output) UnmarshalObjectStorageValue(data []byte) (consumedBytes int, err error) {
	marshalUtil := marshalutil.New(data)
	if output.branchID, err = branchmanager.ParseBranchID(marshalUtil); err != nil {
		return
	}
	if output.solid, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	if output.solidificationTime, err = marshalUtil.ReadTime(); err != nil {
		return
	}
	if output.firstConsumer, err = transaction.ParseID(marshalUtil); err != nil {
		return
	}
	consumerCount, err := marshalUtil.ReadUint32()
	if err != nil {
		return
	}
	if output.preferred, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	if output.finalized, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	if output.liked, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	if output.confirmed, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	if output.rejected, err = marshalUtil.ReadBool(); err != nil {
		return
	}
	output.consumerCount = int(consumerCount)
	balanceCount, err := marshalUtil.ReadUint32()
	if err != nil {
		return
	}
	output.balances = make([]*balance.Balance, balanceCount)
	for i := uint32(0); i < balanceCount; i++ {
		output.balances[i], err = balance.Parse(marshalUtil)
		if err != nil {
			return
		}
	}
	consumedBytes = marshalUtil.ReadOffset()

	return
}

// Update is disabled and panics if it ever gets called - it is required to match StorableObject interface.
func (output *Output) Update(other objectstorage.StorableObject) {
	panic("this object should never be updated")
}

func (output *Output) String() string {
	return stringify.Struct("Output",
		stringify.StructField("address", output.Address()),
		stringify.StructField("transactionId", output.TransactionID()),
		stringify.StructField("branchId", output.BranchID()),
		stringify.StructField("solid", output.Solid()),
		stringify.StructField("solidificationTime", output.SolidificationTime()),
		stringify.StructField("balances", output.Balances()),
	)
}

// define contract (ensure that the struct fulfills the given interface)
var _ objectstorage.StorableObject = &Output{}

// region CachedOutput /////////////////////////////////////////////////////////////////////////////////////////////////

// CachedOutput is a wrapper for the generic CachedObject returned by the objectstorage, that overrides the accessor
// methods, with a type-casted one.
type CachedOutput struct {
	objectstorage.CachedObject
}

// Unwrap is the type-casted equivalent of Get. It returns nil if the object does not exist.
func (cachedOutput *CachedOutput) Unwrap() *Output {
	untypedObject := cachedOutput.Get()
	if untypedObject == nil {
		return nil
	}

	typedObject := untypedObject.(*Output)
	if typedObject == nil || typedObject.IsDeleted() {
		return nil
	}

	return typedObject
}

// Consume unwraps the CachedObject and passes a type-casted version to the consumer (if the object is not empty - it
// exists). It automatically releases the object when the consumer finishes.
func (cachedOutput *CachedOutput) Consume(consumer func(output *Output)) (consumed bool) {
	return cachedOutput.CachedObject.Consume(func(object objectstorage.StorableObject) {
		consumer(object.(*Output))
	})
}

// CachedOutputs represents a collection of CachedOutputs.
type CachedOutputs map[transaction.OutputID]*CachedOutput

// Consume iterates over the CachedObjects, unwraps them and passes a type-casted version to the consumer (if the object
// is not empty - it exists). It automatically releases the object when the consumer finishes. It returns true, if at
// least one object was consumed.
func (cachedOutputs CachedOutputs) Consume(consumer func(output *Output)) (consumed bool) {
	for _, cachedOutput := range cachedOutputs {
		consumed = cachedOutput.Consume(func(output *Output) {
			consumer(output)
		}) || consumed
	}

	return
}

// Release is a utility function, that allows us to release all CachedObjects in the collection.
func (cachedOutputs CachedOutputs) Release(force ...bool) {
	for _, cachedOutput := range cachedOutputs {
		cachedOutput.Release(force...)
	}
}

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