package tangle

import (
    "github.com/iotaledger/goshimmer/packages/bitutils"
    "github.com/iotaledger/goshimmer/packages/errors"
    "github.com/iotaledger/goshimmer/packages/ternary"
    "github.com/iotaledger/goshimmer/packages/typeconversion"
    "sync"
    "time"
)

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