package accounting

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"slices"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/fserrors"
	"github.com/rclone/rclone/fs/rc"
	"github.com/rclone/rclone/lib/terminal"
)

const (
	averagePeriodLength = time.Second
	averageStopAfter    = time.Minute
)

// MaxCompletedTransfers specifies maximum number of completed transfers in startedTransfers list
var MaxCompletedTransfers = 100

// StatsInfo accounts all transfers
// N.B.: if this struct is modified, please remember to also update sum() function in stats_groups
// to correctly count the updated fields
type StatsInfo struct {
	mu                  sync.RWMutex
	ctx                 context.Context
	ci                  *fs.ConfigInfo
	bytes               int64
	errors              int64
	lastError           error
	fatalError          bool
	retryError          bool
	retryAfter          time.Time
	checks              int64
	checking            *transferMap
	checkQueue          int
	checkQueueSize      int64
	transfers           int64
	transferring        *transferMap
	transferQueue       int
	transferQueueSize   int64
	listed              int64
	renames             int64
	renameQueue         int
	renameQueueSize     int64
	deletes             int64
	deletesSize         int64
	deletedDirs         int64
	inProgress          *inProgress
	startedTransfers    []*Transfer   // currently active transfers
	oldTimeRanges       timeRanges    // a merged list of time ranges for the transfers
	oldDuration         time.Duration // duration of transfers we have culled
	group               string
	startTime           time.Time // the moment these stats were initialized or reset
	average             averageValues
	serverSideCopies    int64
	serverSideCopyBytes int64
	serverSideMoves     int64
	serverSideMoveBytes int64
}

type averageValues struct {
	mu      sync.Mutex
	lpBytes int64
	lpTime  time.Time
	speed   float64
	stop    chan bool
	stopped sync.WaitGroup
	started bool
}

// NewStats creates an initialised StatsInfo
func NewStats(ctx context.Context) *StatsInfo {
	ci := fs.GetConfig(ctx)
	s := &StatsInfo{
		ctx:          ctx,
		ci:           ci,
		checking:     newTransferMap(ci.Checkers, "checking"),
		transferring: newTransferMap(ci.Transfers, "transferring"),
		inProgress:   newInProgress(ctx),
		startTime:    time.Now(),
		average:      averageValues{},
	}
	s.startAverageLoop()
	return s
}

// RemoteStats returns stats for rc
//
// If short is true then the transfers and checkers won't be added.
func (s *StatsInfo) RemoteStats(short bool) (out rc.Params, err error) {
	// NB if adding values here - make sure you update the docs in
	// stats_groups.go

	out = make(rc.Params)

	ts := s.calculateTransferStats()
	out["totalChecks"] = ts.totalChecks
	out["totalTransfers"] = ts.totalTransfers
	out["totalBytes"] = ts.totalBytes
	out["transferTime"] = ts.transferTime
	out["speed"] = ts.speed

	s.mu.RLock()
	out["bytes"] = s.bytes
	out["errors"] = s.errors
	out["fatalError"] = s.fatalError
	out["retryError"] = s.retryError
	out["checks"] = s.checks
	out["transfers"] = s.transfers
	out["deletes"] = s.deletes
	out["deletedDirs"] = s.deletedDirs
	out["renames"] = s.renames
	out["listed"] = s.listed
	out["elapsedTime"] = time.Since(s.startTime).Seconds()
	out["serverSideCopies"] = s.serverSideCopies
	out["serverSideCopyBytes"] = s.serverSideCopyBytes
	out["serverSideMoves"] = s.serverSideMoves
	out["serverSideMoveBytes"] = s.serverSideMoveBytes
	eta, etaOK := eta(s.bytes, ts.totalBytes, ts.speed)
	if etaOK {
		out["eta"] = eta.Seconds()
	} else {
		out["eta"] = nil
	}
	s.mu.RUnlock()

	if !short && !s.checking.empty() {
		out["checking"] = s.checking.remotes()
	}
	if !short && !s.transferring.empty() {
		out["transferring"] = s.transferring.rcStats(s.inProgress)
	}
	if s.errors > 0 {
		out["lastError"] = s.lastError.Error()
	}

	return out, nil
}

// _speed returns the average speed of the transfer in bytes/second
//
// Call with lock held
func (s *StatsInfo) _speed() float64 {
	return s.average.speed
}

// timeRange is a start and end time of a transfer
type timeRange struct {
	start time.Time
	end   time.Time
}

// timeRanges is a list of non-overlapping start and end times for
// transfers
type timeRanges []timeRange

// merge all the overlapping time ranges
func (trs *timeRanges) merge() {
	Trs := *trs

	// Sort by the starting time.
	sort.Slice(Trs, func(i, j int) bool {
		return Trs[i].start.Before(Trs[j].start)
	})

	// Merge overlaps and add distinctive ranges together
	var (
		newTrs = Trs[:0]
		i, j   = 0, 1
	)
	for i < len(Trs) {
		if j < len(Trs) {
			if !Trs[i].end.Before(Trs[j].start) {
				if Trs[i].end.Before(Trs[j].end) {
					Trs[i].end = Trs[j].end
				}
				j++
				continue
			}
		}
		newTrs = append(newTrs, Trs[i])
		i = j
		j++
	}

	*trs = newTrs
}

// cull remove any ranges whose start and end are before cutoff
// returning their duration sum
func (trs *timeRanges) cull(cutoff time.Time) (d time.Duration) {
	var newTrs = (*trs)[:0]
	for _, tr := range *trs {
		if cutoff.Before(tr.start) || cutoff.Before(tr.end) {
			newTrs = append(newTrs, tr)
		} else {
			d += tr.end.Sub(tr.start)
		}
	}
	*trs = newTrs
	return d
}

// total the time out of the time ranges
func (trs timeRanges) total() (total time.Duration) {
	for _, tr := range trs {
		total += tr.end.Sub(tr.start)
	}
	return total
}

// Total duration is union of durations of all transfers belonging to this
// object.
//
// Needs to be protected by mutex.
func (s *StatsInfo) _totalDuration() time.Duration {
	// copy of s.oldTimeRanges with extra room for the current transfers
	timeRanges := make(timeRanges, len(s.oldTimeRanges), len(s.oldTimeRanges)+len(s.startedTransfers))
	copy(timeRanges, s.oldTimeRanges)

	// Extract time ranges of all transfers.
	now := time.Now()
	for i := range s.startedTransfers {
		start, end := s.startedTransfers[i].TimeRange()
		if end.IsZero() {
			end = now
		}
		timeRanges = append(timeRanges, timeRange{start, end})
	}

	timeRanges.merge()
	return s.oldDuration + timeRanges.total()
}

const (
	etaMaxSeconds = (1<<63 - 1) / int64(time.Second)           // Largest possible ETA as number of seconds
	etaMax        = time.Duration(etaMaxSeconds) * time.Second // Largest possible ETA, which is in second precision, representing "292y24w3d23h47m16s"
)

// eta returns the ETA of the current operation,
// rounded to full seconds.
// If the ETA cannot be determined 'ok' returns false.
func eta(size, total int64, rate float64) (eta time.Duration, ok bool) {
	if total <= 0 || size < 0 || rate <= 0 {
		return 0, false
	}
	remaining := total - size
	if remaining < 0 {
		return 0, false
	}
	seconds := int64(float64(remaining) / rate)
	if seconds < 0 {
		// Got Int64 overflow
		eta = etaMax
	} else if seconds >= etaMaxSeconds {
		// Would get Int64 overflow if converting from seconds to Duration (nanoseconds)
		eta = etaMax
	} else {
		eta = time.Duration(seconds) * time.Second
	}
	return eta, true
}

// etaString returns the ETA of the current operation,
// rounded to full seconds.
// If the ETA cannot be determined it returns "-"
func etaString(done, total int64, rate float64) string {
	d, ok := eta(done, total, rate)
	if !ok {
		return "-"
	}
	if d == etaMax {
		return "-"
	}
	return fs.Duration(d).ShortReadableString()
}

// percent returns a/b as a percentage rounded to the nearest integer
// as a string
//
// if the percentage is invalid it returns "-"
func percent(a int64, b int64) string {
	if a < 0 || b <= 0 {
		return "-"
	}
	return fmt.Sprintf("%d%%", int(float64(a)*100/float64(b)+0.5))
}

// returned from calculateTransferStats
type transferStats struct {
	totalChecks    int64
	totalTransfers int64
	totalBytes     int64
	transferTime   float64
	speed          float64
}

// calculateTransferStats calculates some additional transfer stats not
// stored directly in StatsInfo
func (s *StatsInfo) calculateTransferStats() (ts transferStats) {
	// checking and transferring have their own locking so read
	// here before lock to prevent deadlock on GetBytes
	transferring, checking := s.transferring.count(), s.checking.count()
	transferringBytesDone, transferringBytesTotal := s.transferring.progress(s)

	s.mu.RLock()
	defer s.mu.RUnlock()

	ts.totalChecks = int64(s.checkQueue) + s.checks + int64(checking)
	ts.totalTransfers = int64(s.transferQueue) + s.transfers + int64(transferring)
	// note that s.bytes already includes transferringBytesDone so
	// we take it off here to avoid double counting
	ts.totalBytes = s.transferQueueSize + s.bytes + transferringBytesTotal - transferringBytesDone
	s.average.mu.Lock()
	ts.speed = s.average.speed
	s.average.mu.Unlock()
	dt := s._totalDuration()
	ts.transferTime = dt.Seconds()

	return ts
}

func (s *StatsInfo) averageLoop() {
	var period float64

	ticker := time.NewTicker(averagePeriodLength)
	defer ticker.Stop()

	a := &s.average
	defer a.stopped.Done()

	shouldRun := false

	for {
		select {
		case now := <-ticker.C:
			a.mu.Lock()

			if !shouldRun {
				a.mu.Unlock()
				continue
			}

			avg := 0.0
			elapsed := now.Sub(a.lpTime).Seconds()
			if elapsed > 0 {
				avg = float64(a.lpBytes) / elapsed
			}

			if period < averagePeriod {
				period++
			}

			a.speed = (avg + a.speed*(period-1)) / period
			a.lpBytes = 0
			a.lpTime = now

			a.mu.Unlock()

		case stop, ok := <-a.stop:
			if !ok {
				return // Channel closed, exit the loop
			}

			a.mu.Lock()

			// If we are resuming, store the current time
			if !shouldRun && !stop {
				a.lpTime = time.Now()
			}
			shouldRun = !stop

			a.mu.Unlock()
		}
	}
}

// Resume the average loop
func (s *StatsInfo) resumeAverageLoop() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.average.stop <- false
}

// Pause the average loop
func (s *StatsInfo) pauseAverageLoop() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.average.stop <- true
}

// Start the average loop
//
// Call with the mutex held
func (s *StatsInfo) _startAverageLoop() {
	if !s.average.started {
		s.average.stop = make(chan bool)
		s.average.started = true
		s.average.stopped.Add(1)
		go s.averageLoop()
	}
}

// Start the average loop
func (s *StatsInfo) startAverageLoop() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s._startAverageLoop()
}

// Stop the average loop
//
// Call with the mutex held
func (s *StatsInfo) _stopAverageLoop() {
	if s.average.started {
		close(s.average.stop)
		s.average.stopped.Wait()
	}
}

// String convert the StatsInfo to a string for printing
func (s *StatsInfo) String() string {
	// NB if adding more stats in here, remember to add them into
	// RemoteStats() too.

	ts := s.calculateTransferStats()

	s.mu.RLock()

	var (
		buf                    = &bytes.Buffer{}
		xfrchkString           = ""
		dateString             = ""
		elapsedTime            = time.Since(s.startTime)
		elapsedTimeSecondsOnly = elapsedTime.Truncate(time.Second/10) % time.Minute
		displaySpeedString     string
	)

	if s.ci.DataRateUnit == "bits" {
		displaySpeedString = fs.SizeSuffix(ts.speed * 8).BitRateUnit()
	} else {
		displaySpeedString = fs.SizeSuffix(ts.speed).ByteRateUnit()
	}

	if !s.ci.StatsOneLine {
		_, _ = fmt.Fprintf(buf, "\nTransferred:   	")
	} else {
		xfrchk := []string{}
		if ts.totalTransfers > 0 && s.transferQueue > 0 {
			xfrchk = append(xfrchk, fmt.Sprintf("xfr#%d/%d", s.transfers, ts.totalTransfers))
		}
		if ts.totalChecks > 0 && s.checkQueue > 0 {
			xfrchk = append(xfrchk, fmt.Sprintf("chk#%d/%d", s.checks, ts.totalChecks))
		}
		if len(xfrchk) > 0 {
			xfrchkString = fmt.Sprintf(" (%s)", strings.Join(xfrchk, ", "))
		}
		if s.ci.StatsOneLineDate {
			t := time.Now()
			dateString = t.Format(s.ci.StatsOneLineDateFormat) // Including the separator so people can customize it
		}
	}

	_, _ = fmt.Fprintf(buf, "%s%13s / %s, %s, %s, ETA %s%s",
		dateString,
		fs.SizeSuffix(s.bytes).ByteUnit(),
		fs.SizeSuffix(ts.totalBytes).ByteUnit(),
		percent(s.bytes, ts.totalBytes),
		displaySpeedString,
		etaString(s.bytes, ts.totalBytes, ts.speed),
		xfrchkString,
	)

	if s.ci.ProgressTerminalTitle {
		// Writes ETA to the terminal title
		terminal.WriteTerminalTitle("ETA: " + etaString(s.bytes, ts.totalBytes, ts.speed))
	}

	if !s.ci.StatsOneLine {
		_, _ = buf.WriteRune('\n')
		errorDetails := ""
		switch {
		case s.fatalError:
			errorDetails = " (fatal error encountered)"
		case s.retryError:
			errorDetails = " (retrying may help)"
		case s.errors != 0:
			errorDetails = " (no need to retry)"

		}

		// Add only non zero stats
		if s.errors != 0 {
			_, _ = fmt.Fprintf(buf, "Errors:        %10d%s\n",
				s.errors, errorDetails)
		}
		if s.checks != 0 || ts.totalChecks != 0 || s.listed != 0 {
			_, _ = fmt.Fprintf(buf, "Checks:        %10d / %d, %s, Listed %d\n",
				s.checks, ts.totalChecks, percent(s.checks, ts.totalChecks), s.listed)
		}
		if s.deletes != 0 || s.deletedDirs != 0 {
			_, _ = fmt.Fprintf(buf, "Deleted:       %10d (files), %d (dirs), %s (freed)\n", s.deletes, s.deletedDirs, fs.SizeSuffix(s.deletesSize).ByteUnit())
		}
		if s.renames != 0 {
			_, _ = fmt.Fprintf(buf, "Renamed:       %10d\n", s.renames)
		}
		if s.transfers != 0 || ts.totalTransfers != 0 {
			_, _ = fmt.Fprintf(buf, "Transferred:   %10d / %d, %s\n",
				s.transfers, ts.totalTransfers, percent(s.transfers, ts.totalTransfers))
		}
		if s.serverSideCopies != 0 || s.serverSideCopyBytes != 0 {
			_, _ = fmt.Fprintf(buf, "Server Side Copies:%6d @ %s\n",
				s.serverSideCopies, fs.SizeSuffix(s.serverSideCopyBytes).ByteUnit(),
			)
		}
		if s.serverSideMoves != 0 || s.serverSideMoveBytes != 0 {
			_, _ = fmt.Fprintf(buf, "Server Side Moves:%7d @ %s\n",
				s.serverSideMoves, fs.SizeSuffix(s.serverSideMoveBytes).ByteUnit(),
			)
		}
		_, _ = fmt.Fprintf(buf, "Elapsed time:  %10ss\n", strings.TrimRight(fs.Duration(elapsedTime.Truncate(time.Minute)).ReadableString(), "0s")+fmt.Sprintf("%.1f", elapsedTimeSecondsOnly.Seconds()))
	}

	// checking and transferring have their own locking so unlock
	// here to prevent deadlock on GetBytes
	s.mu.RUnlock()

	// Add per transfer stats if required
	if !s.ci.StatsOneLine {
		if !s.checking.empty() {
			_, _ = fmt.Fprintf(buf, "Checking:\n%s\n", s.checking.String(s.ctx, s.inProgress, s.transferring))
		}
		if !s.transferring.empty() {
			_, _ = fmt.Fprintf(buf, "Transferring:\n%s\n", s.transferring.String(s.ctx, s.inProgress, nil))
		}
	}

	return buf.String()
}

// Transferred returns list of all completed transfers including checked and
// failed ones.
func (s *StatsInfo) Transferred() []TransferSnapshot {
	s.mu.RLock()
	defer s.mu.RUnlock()
	ts := make([]TransferSnapshot, 0, len(s.startedTransfers))

	for _, tr := range s.startedTransfers {
		if tr.IsDone() {
			ts = append(ts, tr.Snapshot())
		}
	}

	return ts
}

// Log outputs the StatsInfo to the log
func (s *StatsInfo) Log() {
	if s.ci.UseJSONLog {
		out, _ := s.RemoteStats(false)
		fs.LogLevelPrintf(s.ci.StatsLogLevel, nil, "%v%v\n", s, fs.LogValueHide("stats", out))
	} else {
		fs.LogLevelPrintf(s.ci.StatsLogLevel, nil, "%v\n", s)
	}

}

// Bytes updates the stats for bytes bytes
func (s *StatsInfo) Bytes(bytes int64) {
	s.average.mu.Lock()
	s.average.lpBytes += bytes
	s.average.mu.Unlock()

	s.mu.Lock()
	defer s.mu.Unlock()
	s.bytes += bytes
}

// BytesNoNetwork updates the stats for bytes bytes but doesn't include the transfer stats
func (s *StatsInfo) BytesNoNetwork(bytes int64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.bytes += bytes
}

// GetBytes returns the number of bytes transferred so far
func (s *StatsInfo) GetBytes() int64 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.bytes
}

// GetBytesWithPending returns the number of bytes transferred and remaining transfers
func (s *StatsInfo) GetBytesWithPending() int64 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	pending := int64(0)
	for _, tr := range s.startedTransfers {
		if tr.acc != nil {
			bytesRead, size := tr.acc.progress()
			if bytesRead < size {
				pending += size - bytesRead
			}
		}
	}
	return s.bytes + pending
}

// Errors updates the stats for errors
func (s *StatsInfo) Errors(errors int64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.errors += errors
}

// GetErrors reads the number of errors
func (s *StatsInfo) GetErrors() int64 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.errors
}

// GetLastError returns the lastError
func (s *StatsInfo) GetLastError() error {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.lastError
}

// GetChecks returns the number of checks
func (s *StatsInfo) GetChecks() int64 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.checks
}

// FatalError sets the fatalError flag
func (s *StatsInfo) FatalError() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.fatalError = true
}

// HadFatalError returns whether there has been at least one FatalError
func (s *StatsInfo) HadFatalError() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.fatalError
}

// RetryError sets the retryError flag
func (s *StatsInfo) RetryError() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.retryError = true
}

// HadRetryError returns whether there has been at least one non-NoRetryError
func (s *StatsInfo) HadRetryError() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.retryError
}

var (
	errMaxDelete     = fserrors.FatalError(errors.New("--max-delete threshold reached"))
	errMaxDeleteSize = fserrors.FatalError(errors.New("--max-delete-size threshold reached"))
)

// DeleteFile updates the stats for deleting a file
//
// It may return fatal errors if the threshold for --max-delete or
// --max-delete-size have been reached.
func (s *StatsInfo) DeleteFile(ctx context.Context, size int64) error {
	ci := fs.GetConfig(ctx)
	s.mu.Lock()
	defer s.mu.Unlock()
	if size < 0 {
		size = 0
	}
	if ci.MaxDelete >= 0 && s.deletes+1 > ci.MaxDelete {
		return errMaxDelete
	}
	if ci.MaxDeleteSize >= 0 && s.deletesSize+size > int64(ci.MaxDeleteSize) {
		return errMaxDeleteSize
	}
	s.deletes++
	s.deletesSize += size
	return nil
}

// GetDeletes returns the number of deletes
func (s *StatsInfo) GetDeletes() int64 {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.deletes
}

// DeletedDirs updates the stats for deletedDirs
func (s *StatsInfo) DeletedDirs(deletedDirs int64) int64 {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.deletedDirs += deletedDirs
	return s.deletedDirs
}

// Renames updates the stats for renames
func (s *StatsInfo) Renames(renames int64) int64 {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.renames += renames
	return s.renames
}

// Listed updates the stats for listed objects
func (s *StatsInfo) Listed(listed int64) int64 {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.listed += listed
	return s.listed
}

// ResetCounters sets the counters (bytes, checks, errors, transfers, deletes, renames, listed) to 0 and resets lastError, fatalError and retryError
func (s *StatsInfo) ResetCounters() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.bytes = 0
	s.errors = 0
	s.lastError = nil
	s.fatalError = false
	s.retryError = false
	s.retryAfter = time.Time{}
	s.checks = 0
	s.transfers = 0
	s.deletes = 0
	s.deletesSize = 0
	s.deletedDirs = 0
	s.renames = 0
	s.listed = 0
	s.startedTransfers = nil
	s.oldDuration = 0

	s._stopAverageLoop()
	s.average = averageValues{}
	s._startAverageLoop()
}

// ResetErrors sets the errors count to 0 and resets lastError, fatalError and retryError
func (s *StatsInfo) ResetErrors() {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.errors = 0
	s.lastError = nil
	s.fatalError = false
	s.retryError = false
	s.retryAfter = time.Time{}
}

// Errored returns whether there have been any errors
func (s *StatsInfo) Errored() bool {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.errors != 0
}

// Error adds a single error into the stats, assigns lastError and eventually sets fatalError or retryError
func (s *StatsInfo) Error(err error) error {
	if err == nil || fserrors.IsCounted(err) {
		return err
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.errors++
	s.lastError = err
	err = fserrors.FsError(err)
	fserrors.Count(err)
	switch {
	case fserrors.IsFatalError(err):
		s.fatalError = true
	case fserrors.IsRetryAfterError(err):
		retryAfter := fserrors.RetryAfterErrorTime(err)
		if s.retryAfter.IsZero() || retryAfter.Sub(s.retryAfter) > 0 {
			s.retryAfter = retryAfter
		}
		s.retryError = true
	case !fserrors.IsNoRetryError(err):
		s.retryError = true
	}
	return err
}

// RetryAfter returns the time to retry after if it is set.  It will
// be Zero if it isn't set.
func (s *StatsInfo) RetryAfter() time.Time {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.retryAfter
}

// NewCheckingTransfer adds a checking transfer to the stats, from the object.
func (s *StatsInfo) NewCheckingTransfer(obj fs.DirEntry, what string) *Transfer {
	tr := newCheckingTransfer(s, obj, what)
	s.checking.add(tr)
	return tr
}

// DoneChecking removes a check from the stats
func (s *StatsInfo) DoneChecking(remote string) {
	s.checking.del(remote)
	s.mu.Lock()
	s.checks++
	s.mu.Unlock()
}

// GetTransfers reads the number of transfers
func (s *StatsInfo) GetTransfers() int64 {
	s.mu.RLock()
	defer s.mu.RUnlock()
	return s.transfers
}

// NewTransfer adds a transfer to the stats from the object.
//
// The obj is uses as the srcFs, the dstFs must be supplied
func (s *StatsInfo) NewTransfer(obj fs.DirEntry, dstFs fs.Fs) *Transfer {
	var srcFs fs.Fs
	if oi, ok := obj.(fs.ObjectInfo); ok {
		if f, ok := oi.Fs().(fs.Fs); ok {
			srcFs = f
		}
	}
	tr := newTransfer(s, obj, srcFs, dstFs)
	s.transferring.add(tr)
	s.resumeAverageLoop()
	return tr
}

// NewTransferRemoteSize adds a transfer to the stats based on remote and size.
func (s *StatsInfo) NewTransferRemoteSize(remote string, size int64, srcFs, dstFs fs.Fs) *Transfer {
	tr := newTransferRemoteSize(s, remote, size, false, "", srcFs, dstFs)
	s.transferring.add(tr)
	s.resumeAverageLoop()
	return tr
}

// DoneTransferring removes a transfer from the stats
//
// if ok is true and it was in the transfermap (to avoid incrementing in case of nested calls, #6213) then it increments the transfers count
func (s *StatsInfo) DoneTransferring(remote string, ok bool) {
	existed := s.transferring.del(remote)
	if ok && existed {
		s.mu.Lock()
		s.transfers++
		s.mu.Unlock()
	}
	if s.transferring.empty() && s.checking.empty() {
		s.pauseAverageLoop()
	}
}

// SetCheckQueue sets the number of queued checks
func (s *StatsInfo) SetCheckQueue(n int, size int64) {
	s.mu.Lock()
	s.checkQueue = n
	s.checkQueueSize = size
	s.mu.Unlock()
}

// SetTransferQueue sets the number of queued transfers
func (s *StatsInfo) SetTransferQueue(n int, size int64) {
	s.mu.Lock()
	s.transferQueue = n
	s.transferQueueSize = size
	s.mu.Unlock()
}

// SetRenameQueue sets the number of queued transfers
func (s *StatsInfo) SetRenameQueue(n int, size int64) {
	s.mu.Lock()
	s.renameQueue = n
	s.renameQueueSize = size
	s.mu.Unlock()
}

// AddTransfer adds reference to the started transfer.
func (s *StatsInfo) AddTransfer(transfer *Transfer) {
	s.mu.Lock()
	s.startedTransfers = append(s.startedTransfers, transfer)
	s.mu.Unlock()
}

// _removeTransfer removes a reference to the started transfer in
// position i.
//
// Must be called with the lock held
func (s *StatsInfo) _removeTransfer(transfer *Transfer, i int) {
	now := time.Now()

	// add finished transfer onto old time ranges
	start, end := transfer.TimeRange()
	if end.IsZero() {
		end = now
	}
	s.oldTimeRanges = append(s.oldTimeRanges, timeRange{start, end})
	s.oldTimeRanges.merge()

	// remove the found entry
	s.startedTransfers = slices.Delete(s.startedTransfers, i, i+1)

	// Find youngest active transfer
	oldestStart := now
	for i := range s.startedTransfers {
		start, _ := s.startedTransfers[i].TimeRange()
		if start.Before(oldestStart) {
			oldestStart = start
		}
	}

	// remove old entries older than that
	s.oldDuration += s.oldTimeRanges.cull(oldestStart)
}

// RemoveTransfer removes a reference to the started transfer.
func (s *StatsInfo) RemoveTransfer(transfer *Transfer) {
	s.mu.Lock()
	for i, tr := range s.startedTransfers {
		if tr == transfer {
			s._removeTransfer(tr, i)
			break
		}
	}
	s.mu.Unlock()
}

// PruneTransfers makes sure there aren't too many old transfers by removing
// single finished transfer.
func (s *StatsInfo) PruneTransfers() {
	if MaxCompletedTransfers < 0 {
		return
	}
	s.mu.Lock()
	// remove a transfer from the start if we are over quota
	if len(s.startedTransfers) > MaxCompletedTransfers+s.ci.Transfers {
		for i, tr := range s.startedTransfers {
			if tr.IsDone() {
				s._removeTransfer(tr, i)
				break
			}
		}
	}
	s.mu.Unlock()
}

// AddServerSideMove counts a server side move
func (s *StatsInfo) AddServerSideMove(n int64) {
	s.mu.Lock()
	s.serverSideMoves += 1
	s.serverSideMoveBytes += n
	s.mu.Unlock()
}

// AddServerSideCopy counts a server side copy
func (s *StatsInfo) AddServerSideCopy(n int64) {
	s.mu.Lock()
	s.serverSideCopies += 1
	s.serverSideCopyBytes += n
	s.mu.Unlock()
}
