package progress import ( "fmt" "time" ) // ETAEstimator calculates estimated time remaining for operations type ETAEstimator struct { startTime time.Time operation string totalItems int itemsComplete int lastUpdate time.Time } // NewETAEstimator creates a new ETA estimator func NewETAEstimator(operation string, totalItems int) *ETAEstimator { now := time.Now() return &ETAEstimator{ startTime: now, operation: operation, totalItems: totalItems, itemsComplete: 0, lastUpdate: now, } } // UpdateProgress updates the completed item count func (e *ETAEstimator) UpdateProgress(itemsComplete int) { e.itemsComplete = itemsComplete e.lastUpdate = time.Now() } // GetElapsed returns elapsed time since start func (e *ETAEstimator) GetElapsed() time.Duration { return time.Since(e.startTime) } // GetETA calculates estimated time remaining func (e *ETAEstimator) GetETA() time.Duration { if e.itemsComplete == 0 || e.totalItems == 0 { return 0 } elapsed := e.GetElapsed() avgTimePerItem := elapsed / time.Duration(e.itemsComplete) remainingItems := e.totalItems - e.itemsComplete return avgTimePerItem * time.Duration(remainingItems) } // GetProgress returns current progress as percentage func (e *ETAEstimator) GetProgress() float64 { if e.totalItems == 0 { return 0 } return float64(e.itemsComplete) / float64(e.totalItems) * 100 } // FormatElapsed returns formatted elapsed time (e.g., "25m 30s") func (e *ETAEstimator) FormatElapsed() string { return FormatDuration(e.GetElapsed()) } // FormatETA returns formatted ETA (e.g., "~40m remaining") func (e *ETAEstimator) FormatETA() string { eta := e.GetETA() if eta == 0 { return "calculating..." } return "~" + FormatDuration(eta) + " remaining" } // FormatProgress returns formatted progress string (e.g., "5/13 (38%)") func (e *ETAEstimator) FormatProgress() string { return fmt.Sprintf("%d/%d (%.0f%%)", e.itemsComplete, e.totalItems, e.GetProgress()) } // GetFullStatus returns complete status line with all info func (e *ETAEstimator) GetFullStatus(baseMessage string) string { if e.totalItems == 0 { // No items to track, just show elapsed return fmt.Sprintf("%s | Elapsed: %s", baseMessage, e.FormatElapsed()) } if e.itemsComplete == 0 { // Just started return fmt.Sprintf("%s | 0/%d | Starting...", baseMessage, e.totalItems) } // Full status with progress and ETA return fmt.Sprintf("%s | %s | Elapsed: %s | ETA: %s", baseMessage, e.FormatProgress(), e.FormatElapsed(), e.FormatETA()) } // FormatDuration formats a duration in human-readable format func FormatDuration(d time.Duration) string { if d < time.Second { return "< 1s" } hours := int(d.Hours()) minutes := int(d.Minutes()) % 60 seconds := int(d.Seconds()) % 60 if hours > 0 { if minutes > 0 { return fmt.Sprintf("%dh %dm", hours, minutes) } return fmt.Sprintf("%dh", hours) } if minutes > 0 { if seconds > 5 { // Only show seconds if > 5 return fmt.Sprintf("%dm %ds", minutes, seconds) } return fmt.Sprintf("%dm", minutes) } return fmt.Sprintf("%ds", seconds) } // EstimateSizeBasedDuration estimates duration based on size (fallback when no progress tracking) func EstimateSizeBasedDuration(sizeBytes int64, cores int) time.Duration { sizeMB := float64(sizeBytes) / (1024 * 1024) // Base estimate: ~100MB per minute on average hardware baseMinutes := sizeMB / 100.0 // Adjust for CPU cores (more cores = faster, but not linear) // Use square root to represent diminishing returns if cores > 1 { speedup := 1.0 + (0.3 * (float64(cores) - 1)) // 30% improvement per core baseMinutes = baseMinutes / speedup } // Add 20% buffer for safety baseMinutes = baseMinutes * 1.2 return time.Duration(baseMinutes * float64(time.Minute)) }