package progressbar

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"math"
	"os"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/antgroup/hugescm/modules/progressbar/colorstring"
	"github.com/rivo/uniseg"
	"golang.org/x/term"
)

// ProgressBar is a thread-safe, simple
// progress bar
type ProgressBar struct {
	state  state
	config config
	lock   sync.Mutex
}

// State is the basic properties of the bar
type State struct {
	Max            int64
	CurrentNum     int64
	CurrentPercent float64
	CurrentBytes   float64
	SecondsSince   float64
	SecondsLeft    float64
	KBsPerSecond   float64
	Description    string
}

type state struct {
	currentNum        int64
	currentPercent    int
	lastPercent       int
	currentSaucerSize int
	isAltSaucerHead   bool

	lastShown time.Time
	startTime time.Time // time when the progress bar start working

	counterTime         time.Time
	counterNumSinceLast int64
	counterLastTenRates []float64
	spinnerIdx          int // the index of spinner

	maxLineWidth int
	currentBytes float64
	finished     bool
	exit         bool // Progress bar exit halfway

	details []string // details to show,only used when detail row is set to more than 0

	rendered string
}

type config struct {
	max                  int64 // max number of the counter
	maxHumanized         string
	maxHumanizedSuffix   string
	width                int
	writer               io.Writer
	theme                Theme
	renderWithBlankState bool
	description          string
	iterationString      string
	ignoreLength         bool // ignoreLength if max bytes not known

	// whether the output is expected to contain color codes
	colorCodes bool

	// show rate of change in kB/sec or MB/sec
	showBytes bool
	// show the iterations per second
	showIterationsPerSecond bool
	showIterationsCount     bool

	// whether the progress bar should show the total bytes (e.g. 23/24 or 23/-, vs. just 23).
	showTotalBytes bool

	// whether the progress bar should show elapsed time.
	// always enabled if predictTime is true.
	elapsedTime bool

	showElapsedTimeOnFinish bool

	// whether the progress bar should attempt to predict the finishing
	// time of the progress based on the start time and the average
	// number of seconds between  increments.
	predictTime bool

	// minimum time to wait in between updates
	throttleDuration time.Duration

	// clear bar once finished
	clearOnFinish bool

	// spinnerType should be a number between 0-75
	spinnerType int

	// spinnerTypeOptionUsed remembers if the spinnerType was changed manually
	spinnerTypeOptionUsed bool

	// spinnerChangeInterval the change interval of spinner
	// if set this attribute to 0, the spinner only change when renderProgressBar was called
	// for example, each time when Add() was called,which will call renderProgressBar function
	spinnerChangeInterval time.Duration

	// spinner represents the spinner as a slice of string
	spinner []string

	// fullWidth specifies whether to measure and set the bar to a specific width
	fullWidth bool

	// invisible doesn't render the bar at all, useful for debugging
	invisible bool

	onCompletion func()

	// whether the render function should make use of ANSI codes to reduce console I/O
	useANSICodes bool

	// whether to use the IEC units (e.g. MiB) instead of the default SI units (e.g. MB)
	useIECUnits bool

	// showDescriptionAtLineEnd specifies whether description should be written at line end instead of line start
	showDescriptionAtLineEnd bool

	// specifies how many rows of details to show,default value is 0 and no details will be shown
	maxDetailRow int

	stdBuffer bytes.Buffer
}

// Theme defines the elements of the bar
type Theme struct {
	Saucer        string
	AltSaucerHead string
	SaucerHead    string
	SaucerPadding string
	BarStart      string
	BarEnd        string

	// BarStartFilled is used after the Bar starts filling, if set. Otherwise, it defaults to BarStart.
	BarStartFilled string

	// BarEndFilled is used once the Bar finishes, if set. Otherwise, it defaults to BarEnd.
	BarEndFilled string
}

var (
	// ThemeDefault is given by default (if not changed with OptionSetTheme), and it looks like "|████     |".
	ThemeDefault = Theme{Saucer: "█", SaucerPadding: " ", BarStart: "|", BarEnd: "|"}

	// ThemeASCII is a predefined Theme that uses ASCII symbols. It looks like "[===>...]".
	// Configure it with OptionSetTheme(ThemeASCII).
	ThemeASCII = Theme{
		Saucer:        "=",
		SaucerHead:    ">",
		SaucerPadding: ".",
		BarStart:      "[",
		BarEnd:        "]",
	}

	// ThemeUnicode is a predefined Theme that uses Unicode characters, displaying a graphic bar.
	// It looks like "" (rendering will depend on font being used).
	// It requires special symbols usually found in "nerd fonts" [2], or in Fira Code [1], and other sources.
	// Configure it with OptionSetTheme(ThemeUnicode).
	//
	// [1] https://github.com/tonsky/FiraCode
	// [2] https://www.nerdfonts.com/
	ThemeUnicode = Theme{
		Saucer:         "\uEE04", // 
		SaucerHead:     "\uEE04", // 
		SaucerPadding:  "\uEE01", // 
		BarStart:       "\uEE00", // 
		BarStartFilled: "\uEE03", // 
		BarEnd:         "\uEE02", // 
		BarEndFilled:   "\uEE05", // 
	}
)

// Option is the type all options need to adhere to
type Option func(p *ProgressBar)

// OptionSetWidth sets the width of the bar
func OptionSetWidth(s int) Option {
	return func(p *ProgressBar) {
		p.config.width = s
	}
}

// OptionSetSpinnerChangeInterval sets the spinner change interval
// the spinner will change according to this value.
// By default, this value is 100 * time.Millisecond
// If you don't want to let this progressbar update by specified time interval
// you can  set this value to zero, then the spinner will change each time rendered,
// such as when Add() or Describe() was called
func OptionSetSpinnerChangeInterval(interval time.Duration) Option {
	return func(p *ProgressBar) {
		p.config.spinnerChangeInterval = interval
	}
}

// OptionSpinnerType sets the type of spinner used for indeterminate bars
func OptionSpinnerType(spinnerType int) Option {
	return func(p *ProgressBar) {
		p.config.spinnerTypeOptionUsed = true
		p.config.spinnerType = spinnerType
	}
}

// OptionSpinnerCustom sets the spinner used for indeterminate bars to the passed
// slice of string
func OptionSpinnerCustom(spinner []string) Option {
	return func(p *ProgressBar) {
		p.config.spinner = spinner
	}
}

// OptionSetTheme sets the elements the bar is constructed with.
// There are two pre-defined themes you can use: ThemeASCII and ThemeUnicode.
func OptionSetTheme(t Theme) Option {
	return func(p *ProgressBar) {
		p.config.theme = t
	}
}

// OptionSetVisibility sets the visibility
func OptionSetVisibility(visibility bool) Option {
	return func(p *ProgressBar) {
		p.config.invisible = !visibility
	}
}

// OptionFullWidth sets the bar to be full width
func OptionFullWidth() Option {
	return func(p *ProgressBar) {
		p.config.fullWidth = true
	}
}

// OptionSetWriter sets the output writer (defaults to os.StdOut)
func OptionSetWriter(w io.Writer) Option {
	return func(p *ProgressBar) {
		p.config.writer = w
	}
}

// OptionSetRenderBlankState sets whether or not to render a 0% bar on construction
func OptionSetRenderBlankState(r bool) Option {
	return func(p *ProgressBar) {
		p.config.renderWithBlankState = r
	}
}

// OptionSetDescription sets the description of the bar to render in front of it
func OptionSetDescription(description string) Option {
	return func(p *ProgressBar) {
		p.config.description = description
	}
}

// OptionEnableColorCodes enables or disables support for color codes
// using mitchellh/colorstring
func OptionEnableColorCodes(colorCodes bool) Option {
	return func(p *ProgressBar) {
		p.config.colorCodes = colorCodes
	}
}

// OptionSetElapsedTime will enable elapsed time. Always enabled if OptionSetPredictTime is true.
func OptionSetElapsedTime(elapsedTime bool) Option {
	return func(p *ProgressBar) {
		p.config.elapsedTime = elapsedTime
	}
}

// OptionSetPredictTime will also attempt to predict the time remaining.
func OptionSetPredictTime(predictTime bool) Option {
	return func(p *ProgressBar) {
		p.config.predictTime = predictTime
	}
}

// OptionShowCount will also print current count out of total
func OptionShowCount() Option {
	return func(p *ProgressBar) {
		p.config.showIterationsCount = true
	}
}

// OptionShowIts will also print the iterations/second
func OptionShowIts() Option {
	return func(p *ProgressBar) {
		p.config.showIterationsPerSecond = true
	}
}

// OptionShowElapsedTimeOnFinish will keep the display of elapsed time on finish.
func OptionShowElapsedTimeOnFinish() Option {
	return func(p *ProgressBar) {
		p.config.showElapsedTimeOnFinish = true
	}
}

// OptionShowTotalBytes will keep the display of total bytes.
func OptionShowTotalBytes(flag bool) Option {
	return func(p *ProgressBar) {
		p.config.showTotalBytes = flag
	}
}

// OptionSetItsString sets what's displayed for iterations a second. The default is "it" which would display: "it/s"
func OptionSetItsString(iterationString string) Option {
	return func(p *ProgressBar) {
		p.config.iterationString = iterationString
	}
}

// OptionThrottle will wait the specified duration before updating again. The default
// duration is 0 seconds.
func OptionThrottle(duration time.Duration) Option {
	return func(p *ProgressBar) {
		p.config.throttleDuration = duration
	}
}

// OptionClearOnFinish will clear the bar once its finished.
func OptionClearOnFinish() Option {
	return func(p *ProgressBar) {
		p.config.clearOnFinish = true
	}
}

// OptionOnCompletion will invoke cmpl function once its finished
func OptionOnCompletion(cmpl func()) Option {
	return func(p *ProgressBar) {
		p.config.onCompletion = cmpl
	}
}

// OptionShowBytes will update the progress bar
// configuration settings to display/hide kBytes/Sec
func OptionShowBytes(val bool) Option {
	return func(p *ProgressBar) {
		p.config.showBytes = val
	}
}

// OptionUseANSICodes will use more optimized terminal i/o.
//
// Only useful in environments with support for ANSI escape sequences.
func OptionUseANSICodes(val bool) Option {
	return func(p *ProgressBar) {
		p.config.useANSICodes = val
	}
}

// OptionUseIECUnits will enable IEC units (e.g. MiB) instead of the default
// SI units (e.g. MB).
func OptionUseIECUnits(val bool) Option {
	return func(p *ProgressBar) {
		p.config.useIECUnits = val
	}
}

// OptionShowDescriptionAtLineEnd defines whether description should be written at line end instead of line start
func OptionShowDescriptionAtLineEnd() Option {
	return func(p *ProgressBar) {
		p.config.showDescriptionAtLineEnd = true
	}
}

// OptionSetMaxDetailRow sets the max row of details
// the row count should be less than the terminal height, otherwise it will not give you the output you want
func OptionSetMaxDetailRow(row int) Option {
	return func(p *ProgressBar) {
		p.config.maxDetailRow = row
	}
}

// OptionSeekTo seek to offset
func OptionSeekTo(offset int64) Option {
	return func(p *ProgressBar) {
		p.state.currentNum = offset
	}
}

// NewOptions constructs a new instance of ProgressBar, with any options you specify
func NewOptions(max int, options ...Option) *ProgressBar {
	return NewOptions64(int64(max), options...)
}

// NewOptions64 constructs a new instance of ProgressBar, with any options you specify
func NewOptions64(max int64, options ...Option) *ProgressBar {
	b := ProgressBar{
		state: state{
			startTime:   time.Time{},
			lastShown:   time.Time{},
			counterTime: time.Time{},
		},
		config: config{
			writer:                os.Stdout,
			theme:                 ThemeDefault,
			iterationString:       "it",
			width:                 40,
			max:                   max,
			throttleDuration:      0 * time.Nanosecond,
			elapsedTime:           max == -1,
			predictTime:           true,
			spinnerType:           9,
			invisible:             false,
			spinnerChangeInterval: 100 * time.Millisecond,
			showTotalBytes:        true,
		},
	}

	for _, o := range options {
		o(&b)
	}

	if b.config.spinnerType < 0 || b.config.spinnerType > 75 {
		panic("invalid spinner type, must be between 0 and 75")
	}

	if b.config.maxDetailRow < 0 {
		panic("invalid max detail row, must be greater than 0")
	}

	// ignoreLength if max bytes not known
	if b.config.max == -1 {
		b.lengthUnknown()
	}

	b.config.maxHumanized, b.config.maxHumanizedSuffix = humanizeBytes(float64(b.config.max),
		b.config.useIECUnits)

	if b.config.renderWithBlankState {
		_ = b.RenderBlank()
	}

	// if the render time interval attribute is set
	if b.config.spinnerChangeInterval != 0 && !b.config.invisible && b.config.ignoreLength {
		go func() {
			ticker := time.NewTicker(b.config.spinnerChangeInterval)
			defer ticker.Stop()
			for range ticker.C {
				if b.IsFinished() {
					return
				}
				if b.IsStarted() {
					b.lock.Lock()
					_ = b.render()
					b.lock.Unlock()
				}
			}
		}()
	}

	return &b
}

func getBasicState() state {
	now := time.Now()
	return state{
		startTime:   now,
		lastShown:   now,
		counterTime: now,
	}
}

// New returns a new ProgressBar
// with the specified maximum
func New(max int) *ProgressBar {
	return NewOptions(max)
}

// DefaultBytes provides a progressbar to measure byte
// throughput with recommended defaults.
// Set maxBytes to -1 to use as a spinner.
func DefaultBytes(maxBytes int64, description ...string) *ProgressBar {
	desc := ""
	if len(description) > 0 {
		desc = description[0]
	}
	return NewOptions64(
		maxBytes,
		OptionSetDescription(desc),
		OptionSetWriter(os.Stderr),
		OptionShowBytes(true),
		OptionShowTotalBytes(true),
		OptionSetWidth(10),
		OptionThrottle(65*time.Millisecond),
		OptionShowCount(),
		OptionOnCompletion(func() {
			fmt.Fprint(os.Stderr, "\n")
		}),
		OptionSpinnerType(14),
		OptionFullWidth(),
		OptionSetRenderBlankState(true),
	)
}

// DefaultBytesSilent is the same as DefaultBytes, but does not output anywhere.
// String() can be used to get the output instead.
func DefaultBytesSilent(maxBytes int64, description ...string) *ProgressBar {
	// Mostly the same bar as DefaultBytes

	desc := ""
	if len(description) > 0 {
		desc = description[0]
	}
	return NewOptions64(
		maxBytes,
		OptionSetDescription(desc),
		OptionSetWriter(io.Discard),
		OptionShowBytes(true),
		OptionShowTotalBytes(true),
		OptionSetWidth(10),
		OptionThrottle(65*time.Millisecond),
		OptionShowCount(),
		OptionSpinnerType(14),
		OptionFullWidth(),
	)
}

// Default provides a progressbar with recommended defaults.
// Set max to -1 to use as a spinner.
func Default(max int64, description ...string) *ProgressBar {
	desc := ""
	if len(description) > 0 {
		desc = description[0]
	}
	return NewOptions64(
		max,
		OptionSetDescription(desc),
		OptionSetWriter(os.Stderr),
		OptionSetWidth(10),
		OptionShowTotalBytes(true),
		OptionThrottle(65*time.Millisecond),
		OptionShowCount(),
		OptionShowIts(),
		OptionOnCompletion(func() {
			fmt.Fprint(os.Stderr, "\n")
		}),
		OptionSpinnerType(14),
		OptionFullWidth(),
		OptionSetRenderBlankState(true),
	)
}

// DefaultSilent is the same as Default, but does not output anywhere.
// String() can be used to get the output instead.
func DefaultSilent(max int64, description ...string) *ProgressBar {
	// Mostly the same bar as Default

	desc := ""
	if len(description) > 0 {
		desc = description[0]
	}
	return NewOptions64(
		max,
		OptionSetDescription(desc),
		OptionSetWriter(io.Discard),
		OptionSetWidth(10),
		OptionShowTotalBytes(true),
		OptionThrottle(65*time.Millisecond),
		OptionShowCount(),
		OptionShowIts(),
		OptionSpinnerType(14),
		OptionFullWidth(),
	)
}

// String returns the current rendered version of the progress bar.
// It will never return an empty string while the progress bar is running.
func (p *ProgressBar) String() string {
	return p.state.rendered
}

// RenderBlank renders the current bar state, you can use this to render a 0% state
func (p *ProgressBar) RenderBlank() error {
	p.lock.Lock()
	defer p.lock.Unlock()

	if p.config.invisible {
		return nil
	}
	if p.state.currentNum == 0 {
		p.state.lastShown = time.Time{}
	}
	return p.render()
}

// StartWithoutRender will start the progress bar without rendering it
// this method is created for the use case where you want to start the progress
// but don't want to render it immediately.
// If you want to start the progress and render it immediately, use RenderBlank instead,
// or maybe you can use Add to start it automatically, but it will make the time calculation less precise.
func (p *ProgressBar) StartWithoutRender() {
	p.lock.Lock()
	defer p.lock.Unlock()

	if p.IsStarted() {
		return
	}

	p.state.startTime = time.Now()
	// the counterTime should be set to the current time
	p.state.counterTime = time.Now()
}

// Reset will reset the clock that is used
// to calculate current time and the time left.
func (p *ProgressBar) Reset() {
	p.lock.Lock()
	defer p.lock.Unlock()

	p.state = getBasicState()
}

// Finish will fill the bar to full
func (p *ProgressBar) Finish() error {
	p.lock.Lock()
	p.state.currentNum = p.config.max
	if !p.config.ignoreLength {
		p.state.currentBytes = float64(p.config.max)
	}
	p.lock.Unlock()
	return p.Add(0)
}

// Exit will exit the bar to keep current state
func (p *ProgressBar) Exit() error {
	p.lock.Lock()
	defer p.lock.Unlock()

	p.state.exit = true
	if p.config.onCompletion != nil {
		p.config.onCompletion()
	}
	return nil
}

// Add will add the specified amount to the progressbar
func (p *ProgressBar) Add(num int) error {
	return p.Add64(int64(num))
}

// Set will set the bar to a current number
func (p *ProgressBar) Set(num int) error {
	return p.Set64(int64(num))
}

// Set64 will set the bar to a current number
func (p *ProgressBar) Set64(num int64) error {
	p.lock.Lock()
	toAdd := num - int64(p.state.currentBytes)
	p.lock.Unlock()
	return p.Add64(toAdd)
}

// Add64 will add the specified amount to the progressbar
func (p *ProgressBar) Add64(num int64) error {
	if p.config.invisible {
		return nil
	}
	p.lock.Lock()
	defer p.lock.Unlock()

	if p.state.exit {
		return nil
	}

	// error out since OptionSpinnerCustom will always override a manually set spinnerType
	if p.config.spinnerTypeOptionUsed && len(p.config.spinner) > 0 {
		return errors.New("OptionSpinnerType and OptionSpinnerCustom cannot be used together")
	}

	if p.config.max == 0 {
		return errors.New("max must be greater than 0")
	}

	if p.state.currentNum < p.config.max {
		if p.config.ignoreLength {
			p.state.currentNum = (p.state.currentNum + num) % p.config.max
		} else {
			p.state.currentNum += num
		}
	}

	p.state.currentBytes += float64(num)

	if p.state.counterTime.IsZero() {
		p.state.counterTime = time.Now()
	}

	// reset the countdown timer every second to take rolling average
	p.state.counterNumSinceLast += num
	if time.Since(p.state.counterTime).Seconds() > 0.5 {
		p.state.counterLastTenRates = append(p.state.counterLastTenRates, float64(p.state.counterNumSinceLast)/time.Since(p.state.counterTime).Seconds())
		if len(p.state.counterLastTenRates) > 10 {
			p.state.counterLastTenRates = p.state.counterLastTenRates[1:]
		}
		p.state.counterTime = time.Now()
		p.state.counterNumSinceLast = 0
	}

	percent := float64(p.state.currentNum) / float64(p.config.max)
	p.state.currentSaucerSize = int(percent * float64(p.config.width))
	p.state.currentPercent = int(percent * 100)
	updateBar := p.state.currentPercent != p.state.lastPercent && p.state.currentPercent > 0

	p.state.lastPercent = p.state.currentPercent
	if p.state.currentNum > p.config.max {
		return errors.New("current number exceeds max")
	}

	// always update if show bytes/second or its/second
	if updateBar || p.config.showIterationsPerSecond || p.config.showIterationsCount {
		return p.render()
	}

	return nil
}

// AddDetail adds a detail to the progress bar. Only used when maxDetailRow is set to a value greater than 0
func (p *ProgressBar) AddDetail(detail string) error {
	if p.config.maxDetailRow == 0 {
		return errors.New("maxDetailRow is set to 0, cannot add detail")
	}
	if p.IsFinished() {
		return errors.New("cannot add detail to a finished progress bar")
	}

	p.lock.Lock()
	defer p.lock.Unlock()
	if p.state.details == nil {
		// if we add a detail before the first add, it will be weird that we have detail but don't have the progress bar in the top.
		// so when we add the first detail, we will render the progress bar first.
		if err := p.render(); err != nil {
			return err
		}
	}
	p.state.details = append(p.state.details, detail)
	if len(p.state.details) > p.config.maxDetailRow {
		p.state.details = p.state.details[1:]
	}
	if err := p.renderDetails(); err != nil {
		return err
	}
	return nil
}

// renderDetails renders the details of the progress bar
func (p *ProgressBar) renderDetails() error {
	if p.config.invisible {
		return nil
	}
	if p.state.finished {
		return nil
	}
	if p.config.maxDetailRow == 0 {
		return nil
	}

	b := strings.Builder{}
	b.WriteString("\n")

	// render the details row
	for _, detail := range p.state.details {
		b.WriteString(fmt.Sprintf("\u001B[K\r%s\n", detail))
	}
	// add empty lines to fill the maxDetailRow
	for i := len(p.state.details); i < p.config.maxDetailRow; i++ {
		b.WriteString("\u001B[K\n")
	}

	// move the cursor up to the start of the details row
	b.WriteString(fmt.Sprintf("\u001B[%dF", p.config.maxDetailRow+1))

	_ = writeString(p.config, b.String())

	return nil
}

// Clear erases the progress bar from the current line
func (p *ProgressBar) Clear() error {
	return clearProgressBar(p.config, p.state)
}

// Describe will change the description shown before the progress, which
// can be changed on the fly (as for a slow running process).
func (p *ProgressBar) Describe(description string) {
	p.lock.Lock()
	defer p.lock.Unlock()
	p.config.description = description
	if p.config.invisible {
		return
	}
	_ = p.render()
}

// New64 returns a new ProgressBar
// with the specified maximum
func New64(max int64) *ProgressBar {
	return NewOptions64(max)
}

// GetMax returns the max of a bar
func (p *ProgressBar) GetMax() int {
	p.lock.Lock()
	defer p.lock.Unlock()

	return int(p.config.max)
}

// GetMax64 returns the current max
func (p *ProgressBar) GetMax64() int64 {
	p.lock.Lock()
	defer p.lock.Unlock()

	return p.config.max
}

// ChangeMax takes in a int
// and changes the max value
// of the progress bar
func (p *ProgressBar) ChangeMax(newMax int) {
	p.ChangeMax64(int64(newMax))
}

// ChangeMax64 is basically
// the same as ChangeMax,
// but takes in a int64
// to avoid casting
func (p *ProgressBar) ChangeMax64(newMax int64) {
	p.lock.Lock()

	p.config.max = newMax

	if p.config.showBytes {
		p.config.maxHumanized, p.config.maxHumanizedSuffix = humanizeBytes(float64(p.config.max),
			p.config.useIECUnits)
	}

	if newMax == -1 {
		p.lengthUnknown()
	} else {
		p.lengthKnown(newMax)
	}
	p.lock.Unlock() // so p.Add can lock

	_ = p.Add(0) // re-render
}

// AddMax takes in a int
// and adds it to the max
// value of the progress bar
func (p *ProgressBar) AddMax(added int) {
	p.AddMax64(int64(added))
}

// AddMax64 is basically
// the same as AddMax,
// but takes in a int64
// to avoid casting
func (p *ProgressBar) AddMax64(added int64) {
	p.lock.Lock()

	p.config.max += added

	if p.config.showBytes {
		p.config.maxHumanized, p.config.maxHumanizedSuffix = humanizeBytes(float64(p.config.max),
			p.config.useIECUnits)
	}

	if p.config.max == -1 {
		p.lengthUnknown()
	} else {
		p.lengthKnown(p.config.max)
	}
	p.lock.Unlock() // so p.Add can lock

	_ = p.Add(0) // re-render
}

// IsFinished returns true if progress bar is completed
func (p *ProgressBar) IsFinished() bool {
	p.lock.Lock()
	defer p.lock.Unlock()

	return p.state.finished
}

// IsStarted returns true if progress bar is started
func (p *ProgressBar) IsStarted() bool {
	return !p.state.startTime.IsZero()
}

// render renders the progress bar, updating the maximum
// rendered line width. this function is not thread-safe,
// so it must be called with an acquired lock.
func (p *ProgressBar) render() error {
	// make sure that the rendering is not happening too quickly
	// but always show if the currentNum reaches the max
	if !p.IsStarted() {
		p.state.startTime = time.Now()
	} else if time.Since(p.state.lastShown).Nanoseconds() < p.config.throttleDuration.Nanoseconds() &&
		p.state.currentNum < p.config.max {
		return nil
	}

	if !p.config.useANSICodes {
		// first, clear the existing progress bar, if not yet finished.
		if !p.state.finished {
			err := clearProgressBar(p.config, p.state)
			if err != nil {
				return err
			}
		}
	}

	// check if the progress bar is finished
	if !p.state.finished && p.state.currentNum >= p.config.max {
		p.state.finished = true
		if !p.config.clearOnFinish {
			_, _ = io.Copy(p.config.writer, &p.config.stdBuffer)
			_, _ = renderProgressBar(p.config, &p.state)
		}
		if p.config.maxDetailRow > 0 {
			_ = p.renderDetails()
			// put the cursor back to the last line of the details
			_ = writeString(p.config, fmt.Sprintf("\u001B[%dB\r\u001B[%dC", p.config.maxDetailRow, len(p.state.details[len(p.state.details)-1])))
		}
		if p.config.onCompletion != nil {
			p.config.onCompletion()
		}
	}
	if p.state.finished {
		// when using ANSI codes we don't pre-clean the current line
		if p.config.useANSICodes && p.config.clearOnFinish {
			err := clearProgressBar(p.config, p.state)
			if err != nil {
				return err
			}
		}
		return nil
	}

	// then, re-render the current progress bar
	_, _ = io.Copy(p.config.writer, &p.config.stdBuffer)
	w, err := renderProgressBar(p.config, &p.state)
	if err != nil {
		return err
	}

	if w > p.state.maxLineWidth {
		p.state.maxLineWidth = w
	}

	p.state.lastShown = time.Now()

	return nil
}

// lengthUnknown sets the progress bar to ignore the length
func (p *ProgressBar) lengthUnknown() {
	p.config.ignoreLength = true
	p.config.max = int64(p.config.width)
	p.config.predictTime = false
}

// lengthKnown sets the progress bar to do not ignore the length
func (p *ProgressBar) lengthKnown(max int64) {
	p.config.ignoreLength = false
	p.config.max = max
	p.config.predictTime = true
}

// State returns the current state
func (p *ProgressBar) State() State {
	p.lock.Lock()
	defer p.lock.Unlock()
	s := State{}
	s.CurrentNum = p.state.currentNum
	s.Max = p.config.max
	if p.config.ignoreLength {
		s.Max = -1
	}
	s.CurrentPercent = float64(p.state.currentNum) / float64(p.config.max)
	s.CurrentBytes = p.state.currentBytes
	if p.IsStarted() {
		s.SecondsSince = time.Since(p.state.startTime).Seconds()
	} else {
		s.SecondsSince = 0
	}

	if p.state.currentNum > 0 {
		s.SecondsLeft = s.SecondsSince / float64(p.state.currentNum) * (float64(p.config.max) - float64(p.state.currentNum))
	}
	s.KBsPerSecond = float64(p.state.currentBytes) / 1024.0 / s.SecondsSince
	s.Description = p.config.description
	return s
}

// regex matching ansi escape codes
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)

func getStringWidth(c config, str string, _ bool) int {
	if c.colorCodes {
		// convert any color codes in the progress bar into the respective ANSI codes
		str = colorstring.Color(str)
	}

	// the width of the string, if printed to the console
	// does not include the carriage return character
	cleanString := strings.ReplaceAll(str, "\r", "")

	if c.colorCodes {
		// the ANSI codes for the colors do not take up space in the console output,
		// so they do not count towards the output string width
		cleanString = ansiRegex.ReplaceAllString(cleanString, "")
	}

	// get the amount of runes in the string instead of the
	// character count of the string, as some runes span multiple characters.
	// see https://stackoverflow.com/a/12668840/2733724
	stringWidth := uniseg.StringWidth(cleanString)
	return stringWidth
}

func renderProgressBar(c config, s *state) (int, error) {
	var sb strings.Builder

	averageRate := average(s.counterLastTenRates)
	if len(s.counterLastTenRates) == 0 || s.finished {
		// if no average samples, or if finished,
		// then average rate should be the total rate
		if t := time.Since(s.startTime).Seconds(); t > 0 {
			averageRate = s.currentBytes / t
		} else {
			averageRate = 0
		}
	}

	// show iteration count in "current/total" iterations format
	if c.showIterationsCount {
		if sb.Len() == 0 {
			sb.WriteString("(")
		} else {
			sb.WriteString(", ")
		}
		if !c.ignoreLength {
			if c.showBytes {
				currentHumanize, currentSuffix := humanizeBytes(s.currentBytes, c.useIECUnits)
				if currentSuffix == c.maxHumanizedSuffix {
					if c.showTotalBytes {
						sb.WriteString(fmt.Sprintf("%s/%s%s",
							currentHumanize, c.maxHumanized, c.maxHumanizedSuffix))
					} else {
						sb.WriteString(fmt.Sprintf("%s%s",
							currentHumanize, c.maxHumanizedSuffix))
					}
				} else if c.showTotalBytes {
					sb.WriteString(fmt.Sprintf("%s%s/%s%s",
						currentHumanize, currentSuffix, c.maxHumanized, c.maxHumanizedSuffix))
				} else {
					sb.WriteString(fmt.Sprintf("%s%s", currentHumanize, currentSuffix))
				}
			} else if c.showTotalBytes {
				sb.WriteString(fmt.Sprintf("%.0f/%d", s.currentBytes, c.max))
			} else {
				sb.WriteString(fmt.Sprintf("%.0f", s.currentBytes))
			}
		} else {
			if c.showBytes {
				currentHumanize, currentSuffix := humanizeBytes(s.currentBytes, c.useIECUnits)
				sb.WriteString(fmt.Sprintf("%s%s", currentHumanize, currentSuffix))
			} else if c.showTotalBytes {
				sb.WriteString(fmt.Sprintf("%.0f/%s", s.currentBytes, "-"))
			} else {
				sb.WriteString(fmt.Sprintf("%.0f", s.currentBytes))
			}
		}
	}

	// show rolling average rate
	if c.showBytes && averageRate > 0 && !math.IsInf(averageRate, 1) {
		if sb.Len() == 0 {
			sb.WriteString("(")
		} else {
			sb.WriteString(", ")
		}
		currentHumanize, currentSuffix := humanizeBytes(averageRate, c.useIECUnits)
		sb.WriteString(fmt.Sprintf("%s%s/s", currentHumanize, currentSuffix))
	}

	// show iterations rate
	if c.showIterationsPerSecond {
		if sb.Len() == 0 {
			sb.WriteString("(")
		} else {
			sb.WriteString(", ")
		}
		if averageRate > 1 {
			sb.WriteString(fmt.Sprintf("%0.0f %s/s", averageRate, c.iterationString))
		} else if averageRate*60 > 1 {
			sb.WriteString(fmt.Sprintf("%0.0f %s/min", 60*averageRate, c.iterationString))
		} else {
			sb.WriteString(fmt.Sprintf("%0.0f %s/hr", 3600*averageRate, c.iterationString))
		}
	}
	if sb.Len() > 0 {
		sb.WriteString(")")
	}

	leftBrac, rightBrac, saucer, saucerHead := "", "", "", ""
	barStart, barEnd := c.theme.BarStart, c.theme.BarEnd
	if s.finished && c.theme.BarEndFilled != "" {
		barEnd = c.theme.BarEndFilled
	}

	// show time prediction in "current/total" seconds format
	switch {
	case c.predictTime:
		rightBracNum := (time.Duration((1/averageRate)*(float64(c.max)-float64(s.currentNum))) * time.Second)
		if rightBracNum.Seconds() < 0 {
			rightBracNum = 0 * time.Second
		}
		rightBrac = rightBracNum.String()
		fallthrough
	case c.elapsedTime || c.showElapsedTimeOnFinish:
		leftBrac = (time.Duration(time.Since(s.startTime).Seconds()) * time.Second).String()
	}

	if c.fullWidth && !c.ignoreLength {
		width, err := termWidth()
		if err != nil {
			width = 80
		}

		if width > 120 {
			width = 120
		}

		amend := 1 // an extra space at eol
		switch {
		case leftBrac != "" && rightBrac != "":
			amend = 4 // space, square brackets and colon
		case leftBrac != "" && rightBrac == "":
			amend = 4 // space and square brackets and another space
		case leftBrac == "" && rightBrac != "":
			amend = 3 // space and square brackets
		}
		if c.showDescriptionAtLineEnd {
			amend += 1 // another space
		}

		c.width = width - getStringWidth(c, c.description, true) - 10 - amend - sb.Len() - len(leftBrac) - len(rightBrac)
		s.currentSaucerSize = int(float64(s.currentPercent) / 100.0 * float64(c.width))
	}
	if (s.currentSaucerSize > 0 || s.currentPercent > 0) && c.theme.BarStartFilled != "" {
		barStart = c.theme.BarStartFilled
	}
	if s.currentSaucerSize > 0 {
		if c.ignoreLength {
			saucer = strings.Repeat(c.theme.SaucerPadding, s.currentSaucerSize-1)
		} else {
			saucer = strings.Repeat(c.theme.Saucer, s.currentSaucerSize-1)
		}

		// Check if an alternate saucer head is set for animation
		if c.theme.AltSaucerHead != "" && s.isAltSaucerHead {
			saucerHead = c.theme.AltSaucerHead
			s.isAltSaucerHead = false
		} else if c.theme.SaucerHead == "" || s.currentSaucerSize == c.width {
			// use the saucer for the saucer head if it hasn't been set
			// to preserve backwards compatibility
			saucerHead = c.theme.Saucer
		} else {
			saucerHead = c.theme.SaucerHead
			s.isAltSaucerHead = true
		}
	}

	/*
		Progress Bar format
		Description % |------        |  (kb/s) (iteration count) (iteration rate) (predict time)

		or if showDescriptionAtLineEnd is enabled
		% |------        |  (kb/s) (iteration count) (iteration rate) (predict time) Description
	*/

	repeatAmount := max(c.width-s.currentSaucerSize, 0)

	str := ""

	if c.ignoreLength {
		selectedSpinner := spinners[c.spinnerType]
		if len(c.spinner) > 0 {
			selectedSpinner = c.spinner
		}

		var spinner string
		if c.spinnerChangeInterval != 0 {
			// if the spinner is changed according to an interval, calculate it
			spinner = selectedSpinner[int(math.Round(math.Mod(float64(time.Since(s.startTime).Nanoseconds()/c.spinnerChangeInterval.Nanoseconds()), float64(len(selectedSpinner)))))]
		} else {
			// if the spinner is changed according to the number render was called
			spinner = selectedSpinner[s.spinnerIdx]
			s.spinnerIdx = (s.spinnerIdx + 1) % len(selectedSpinner)
		}
		if c.elapsedTime {
			if c.showDescriptionAtLineEnd {
				str = fmt.Sprintf("\r%s %s [%s] %s ",
					spinner,
					sb.String(),
					leftBrac,
					c.description)
			} else {
				str = fmt.Sprintf("\r%s %s %s [%s] ",
					spinner,
					c.description,
					sb.String(),
					leftBrac)
			}
		} else {
			if c.showDescriptionAtLineEnd {
				str = fmt.Sprintf("\r%s %s %s ",
					spinner,
					sb.String(),
					c.description)
			} else {
				str = fmt.Sprintf("\r%s %s %s ",
					spinner,
					c.description,
					sb.String())
			}
		}
	} else if rightBrac == "" {
		str = fmt.Sprintf("%4d%% %s%s%s%s%s %s",
			s.currentPercent,
			barStart,
			saucer,
			saucerHead,
			strings.Repeat(c.theme.SaucerPadding, repeatAmount),
			barEnd,
			sb.String())
		if (s.currentPercent == 100 && c.showElapsedTimeOnFinish) || c.elapsedTime {
			str = fmt.Sprintf("%s [%s]", str, leftBrac)
		}

		if c.showDescriptionAtLineEnd {
			str = fmt.Sprintf("\r%s %s ", str, c.description)
		} else {
			str = fmt.Sprintf("\r%s%s ", c.description, str)
		}
	} else {
		if s.currentPercent == 100 {
			str = fmt.Sprintf("%4d%% %s%s%s%s%s %s",
				s.currentPercent,
				barStart,
				saucer,
				saucerHead,
				strings.Repeat(c.theme.SaucerPadding, repeatAmount),
				barEnd,
				sb.String())

			if c.showElapsedTimeOnFinish {
				str = fmt.Sprintf("%s [%s]", str, leftBrac)
			}

			if c.showDescriptionAtLineEnd {
				str = fmt.Sprintf("\r%s %s", str, c.description)
			} else {
				str = fmt.Sprintf("\r%s%s", c.description, str)
			}
		} else {
			str = fmt.Sprintf("%4d%% %s%s%s%s%s %s [%s:%s]",
				s.currentPercent,
				barStart,
				saucer,
				saucerHead,
				strings.Repeat(c.theme.SaucerPadding, repeatAmount),
				barEnd,
				sb.String(),
				leftBrac,
				rightBrac)

			if c.showDescriptionAtLineEnd {
				str = fmt.Sprintf("\r%s %s", str, c.description)
			} else {
				str = fmt.Sprintf("\r%s%s", c.description, str)
			}
		}
	}

	if c.colorCodes {
		// convert any color codes in the progress bar into the respective ANSI codes
		str = colorstring.Color(str)
	}

	s.rendered = str

	return getStringWidth(c, str, false), writeString(c, str)
}

func clearProgressBar(c config, s state) error {
	if s.maxLineWidth == 0 {
		return nil
	}
	if c.useANSICodes {
		// write the "clear current line" ANSI escape sequence
		return writeString(c, "\033[2K\r")
	}
	// fill the empty content
	// to overwrite the progress bar and jump
	// back to the beginning of the line
	str := fmt.Sprintf("\r%s\r", strings.Repeat(" ", s.maxLineWidth))
	return writeString(c, str)
	// the following does not show correctly if the previous line is longer than subsequent line
	// return writeString(c, "\r")
}

func writeString(c config, str string) error {
	if _, err := io.WriteString(c.writer, str); err != nil {
		return err
	}

	if f, ok := c.writer.(*os.File); ok {
		// ignore any errors in Sync(), as stdout
		// can't be synced on some operating systems
		// like Debian 9 (Stretch)
		_ = f.Sync()
	}

	return nil
}

// Reader is the progressbar io.Reader struct
type Reader struct {
	io.Reader
	bar *ProgressBar
}

// NewReader return a new Reader with a given progress bar.
func NewReader(r io.Reader, bar *ProgressBar) Reader {
	return Reader{
		Reader: r,
		bar:    bar,
	}
}

// Read will read the data and add the number of bytes to the progressbar
func (r *Reader) Read(p []byte) (n int, err error) {
	n, err = r.Reader.Read(p)
	_ = r.bar.Add(n)
	return
}

// Close the reader when it implements io.Closer
func (r *Reader) Close() (err error) {
	if closer, ok := r.Reader.(io.Closer); ok {
		return closer.Close()
	}
	_ = r.bar.Finish()
	return
}

// Write implement io.Writer
func (p *ProgressBar) Write(b []byte) (n int, err error) {
	n = len(b)
	err = p.Add(n)
	return
}

// Read implement io.Reader
func (p *ProgressBar) Read(b []byte) (n int, err error) {
	n = len(b)
	err = p.Add(n)
	return
}

func (p *ProgressBar) Close() (err error) {
	err = p.Finish()
	return
}

func average(xs []float64) float64 {
	total := 0.0
	for _, v := range xs {
		total += v
	}
	return total / float64(len(xs))
}

func humanizeBytes(s float64, iec bool) (string, string) {
	sizes := []string{" B", " kB", " MB", " GB", " TB", " PB", " EB"}
	base := 1000.0

	if iec {
		sizes = []string{" B", " KiB", " MiB", " GiB", " TiB", " PiB", " EiB"}
		base = 1024.0
	}

	if s < 10 {
		return fmt.Sprintf("%2.0f", s), sizes[0]
	}
	e := math.Floor(logn(float64(s), base))
	suffix := sizes[int(e)]
	val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
	f := "%.0f"
	if val < 10 {
		f = "%.1f"
	}

	return fmt.Sprintf(f, val), suffix
}

func logn(n, b float64) float64 {
	return math.Log(n) / math.Log(b)
}

// termWidth function returns the visible width of the current terminal
// and can be redefined for testing
var termWidth = func() (width int, err error) {
	width, _, err = term.GetSize(int(os.Stdout.Fd()))
	if err == nil {
		return width, nil
	}

	return 0, err
}

func shouldCacheOutput(pb *ProgressBar) bool {
	return !pb.state.finished && !pb.state.exit && !pb.config.invisible
}

func Bprintln(pb *ProgressBar, a ...any) (int, error) {
	pb.lock.Lock()
	defer pb.lock.Unlock()
	if !shouldCacheOutput(pb) {
		return fmt.Fprintln(pb.config.writer, a...)
	}
	return fmt.Fprintln(&pb.config.stdBuffer, a...)
}

func Bprintf(pb *ProgressBar, format string, a ...any) (int, error) {
	pb.lock.Lock()
	defer pb.lock.Unlock()
	if !shouldCacheOutput(pb) {
		return fmt.Fprintf(pb.config.writer, format, a...)
	}
	return fmt.Fprintf(&pb.config.stdBuffer, format, a...)
}
