////////////////////////////////////////////////////////////////////////////////
// Copyright © 2020 xx network SEZC                                           //
//                                                                            //
// Use of this source code is governed by a license that can be found in the  //
// LICENSE file                                                               //
////////////////////////////////////////////////////////////////////////////////

package fileTransfer

import (
	"bytes"
	"github.com/pkg/errors"
	"gitlab.com/elixxir/client/interfaces"
	"gitlab.com/elixxir/client/storage/versioned"
	ftCrypto "gitlab.com/elixxir/crypto/fileTransfer"
	"gitlab.com/xx_network/crypto/csprng"
	"gitlab.com/xx_network/primitives/id"
	"gitlab.com/xx_network/primitives/netTime"
	"sync"
	"time"
)

// Storage keys and versions.
const (
	sentFileTransfersPrefix  = "SentFileTransfersStore"
	sentFileTransfersKey     = "SentFileTransfers"
	sentFileTransfersVersion = 0
)

// Error messages.
const (
	saveSentTransfersListErr = "failed to save list of sent items in transfer map to storage: %+v"
	loadSentTransfersListErr = "failed to load list of sent items in transfer map from storage: %+v"
	loadSentTransfersErr     = "failed to load sent transfers from storage: %+v"

	newSentTransferErr    = "failed to create new sent transfer: %+v"
	getSentTransferErr    = "sent file transfer not found"
	deleteSentTransferErr = "failed to delete sent transfer with ID %s from store: %+v"
)

// SentFileTransfers contains information for tracking sent file transfers.
type SentFileTransfers struct {
	transfers map[ftCrypto.TransferID]*SentTransfer
	mux       sync.Mutex
	kv        *versioned.KV
}

// NewSentFileTransfers creates a new SentFileTransfers with an empty map.
func NewSentFileTransfers(kv *versioned.KV) (*SentFileTransfers, error) {
	sft := &SentFileTransfers{
		transfers: make(map[ftCrypto.TransferID]*SentTransfer),
		kv:        kv.Prefix(sentFileTransfersPrefix),
	}

	return sft, sft.saveTransfersList()
}

// AddTransfer creates a new empty SentTransfer and adds it to the transfers
// map.
func (sft *SentFileTransfers) AddTransfer(recipient *id.ID,
	key ftCrypto.TransferKey, parts [][]byte, numFps uint16,
	progressCB interfaces.SentProgressCallback, period time.Duration,
	rng csprng.Source) (ftCrypto.TransferID, error) {

	sft.mux.Lock()
	defer sft.mux.Unlock()

	// Generate new transfer ID
	tid, err := ftCrypto.NewTransferID(rng)
	if err != nil {
		return tid, errors.Errorf(addTransferNewIdErr, err)
	}

	// Generate a new SentTransfer and add it to the map
	sft.transfers[tid], err = NewSentTransfer(
		recipient, tid, key, parts, numFps, progressCB, period, sft.kv)
	if err != nil {
		return tid, errors.Errorf(newSentTransferErr, err)
	}

	// Update list of transfers in storage
	err = sft.saveTransfersList()
	if err != nil {
		return tid, errors.Errorf(saveSentTransfersListErr, err)
	}

	return tid, nil
}

// GetTransfer returns the SentTransfer with the given transfer ID. An error is
// returned if no corresponding transfer is found.
func (sft *SentFileTransfers) GetTransfer(tid ftCrypto.TransferID) (
	*SentTransfer, error) {
	sft.mux.Lock()
	defer sft.mux.Unlock()

	rt, exists := sft.transfers[tid]
	if !exists {
		return nil, errors.New(getSentTransferErr)
	}

	return rt, nil
}

// DeleteTransfer removes the SentTransfer with the associated transfer ID
// from memory and storage.
func (sft *SentFileTransfers) DeleteTransfer(tid ftCrypto.TransferID) error {
	sft.mux.Lock()
	defer sft.mux.Unlock()

	// Return an error if the transfer does not exist
	_, exists := sft.transfers[tid]
	if !exists {
		return errors.New(getSentTransferErr)
	}

	// Delete all data the transfer saved to storage
	err := sft.transfers[tid].delete()
	if err != nil {
		return errors.Errorf(deleteSentTransferErr, tid, err)
	}

	// Delete the transfer from memory
	delete(sft.transfers, tid)

	// Update the transfers list for the removed transfer
	err = sft.saveTransfersList()
	if err != nil {
		return errors.Errorf(saveSentTransfersListErr, err)
	}

	return nil
}

////////////////////////////////////////////////////////////////////////////////
// Storage Functions                                                          //
////////////////////////////////////////////////////////////////////////////////

// LoadSentFileTransfers loads all SentFileTransfers from storage.
func LoadSentFileTransfers(kv *versioned.KV) (*SentFileTransfers, error) {
	sft := &SentFileTransfers{
		transfers: make(map[ftCrypto.TransferID]*SentTransfer),
		kv:        kv.Prefix(sentFileTransfersPrefix),
	}

	// Get the list of transfer IDs corresponding to each sent transfer from
	// storage
	transfersList, err := sft.loadTransfersList()
	if err != nil {
		return nil, errors.Errorf(loadSentTransfersListErr, err)
	}

	// Load each transfer in the list from storage into the map
	err = sft.loadTransfers(transfersList)
	if err != nil {
		return nil, errors.Errorf(loadSentTransfersErr, err)
	}

	return sft, nil
}

// NewOrLoadSentFileTransfers loads all SentFileTransfers from storage, if they
// exist. Otherwise, a new SentFileTransfers is returned.
func NewOrLoadSentFileTransfers(kv *versioned.KV) (*SentFileTransfers, error) {
	sft := &SentFileTransfers{
		transfers: make(map[ftCrypto.TransferID]*SentTransfer),
		kv:        kv.Prefix(sentFileTransfersPrefix),
	}

	// If the transfer list cannot be loaded from storage, then create a new
	// SentFileTransfers
	vo, err := sft.kv.Get(sentFileTransfersKey, sentFileTransfersVersion)
	if err != nil {
		return NewSentFileTransfers(kv)
	}

	// Unmarshal data into list of saved transfer IDs
	transfersList := unmarshalTransfersList(vo.Data)

	// Load each transfer in the list from storage into the map
	err = sft.loadTransfers(transfersList)
	if err != nil {
		return nil, errors.Errorf(loadSentTransfersErr, err)
	}

	return sft, nil
}

// saveTransfersList saves a list of items in the transfers map to storage.
func (sft *SentFileTransfers) saveTransfersList() error {
	// Create new versioned object with a list of items in the transfers map
	obj := &versioned.Object{
		Version:   sentFileTransfersVersion,
		Timestamp: netTime.Now(),
		Data:      sft.marshalTransfersList(),
	}

	// Save list of items in the transfers map to storage
	return sft.kv.Set(sentFileTransfersKey, sentFileTransfersVersion, obj)
}

// loadTransfersList gets the list of transfer IDs corresponding to each saved
// sent transfer from storage.
func (sft *SentFileTransfers) loadTransfersList() ([]ftCrypto.TransferID, error) {
	// Get transfers list from storage
	vo, err := sft.kv.Get(sentFileTransfersKey, sentFileTransfersVersion)
	if err != nil {
		return nil, err
	}

	// Unmarshal data into list of saved transfer IDs
	return unmarshalTransfersList(vo.Data), nil
}

// loadTransfers loads each SentTransfer from the list and adds them to the map.
func (sft *SentFileTransfers) loadTransfers(list []ftCrypto.TransferID) error {
	var err error

	// Load each sentTransfer from storage into the map
	for _, tid := range list {
		sft.transfers[tid], err = loadSentTransfer(tid, sft.kv)
		if err != nil {
			return err
		}
	}

	return nil
}

// marshalTransfersList creates a list of all transfer IDs in the transfers map
// and serialises it.
func (sft *SentFileTransfers) marshalTransfersList() []byte {
	buff := bytes.NewBuffer(nil)
	buff.Grow(ftCrypto.TransferIdLength * len(sft.transfers))

	for tid := range sft.transfers {
		buff.Write(tid.Bytes())
	}

	return buff.Bytes()
}

// unmarshalTransfersList deserializes a byte slice into a list of transfer IDs.
func unmarshalTransfersList(b []byte) []ftCrypto.TransferID {
	buff := bytes.NewBuffer(b)
	list := make([]ftCrypto.TransferID, 0, buff.Len()/ftCrypto.TransferIdLength)

	const size = ftCrypto.TransferIdLength
	for n := buff.Next(size); len(n) == size; n = buff.Next(size) {
		list = append(list, ftCrypto.UnmarshalTransferID(n))
	}

	return list
}