package progress

import (
	"fmt"
	"io"
	"os"
	"time"
)

// Bar - a simple, unobtrusive progress indicator
// Usage:
//   bar := New()
//   bar.SetTotal(3)
//   defer bar.Done()
//   bar.Increment("Step 1")
//   bar.Increment("Step 2")
//   bar.Increment("Step 3")
//   bar.Complete("Done")
//
// Instantiation creates a progress bar consisiting of an optional spinner
// prefix followed by an indicator of the current step, the total steps, and a
// trailing message The behavior differs substantially if verbosity is enbled
// or not. When in verbose mode, it is expected that the process is otherwise
// printing status information to output, in which case this bar only prints a
// single status update to output when the current step is updated,
// interspersing the status line with standard program output.  When verbosity
// is not enabled (the default), it is expected that in general output is left
// clean. In this case the status is continually written to standard output
// along with an animated spinner. The status is written one line above current,
// such that any errors from the rest of the process begin at the beinning of a
// line.  Finally, the bar respects headless mode, omitting any printing unless
// explicitly configured to do so, and even then not doing the continual write
// method but the single status update in the same manner as when verbosity is
// enabled.
// Format:
//   [spinner] i/n t
type Bar struct {
	out   io.Writer
	index int    // Current step index
	total int    // Total steps
	text  string // Current display text

	// verbose mode disables progress spinner and line overwrites, instead
	// printing single, full line updates.
	Verbose bool

	// print verbose-style updates even when not attached to an interactive terminal.
	printWhileHeadless bool

	// print N/M step counter with messages
	printWithStepCounter bool

	// Ticker for animated progress when non-verbose, interactive terminal.
	ticker *time.Ticker
}

type Option func(*Bar)

func WithOutput(w io.Writer) Option {
	return func(b *Bar) {
		b.out = w
	}
}

// WithPrintHeadless allows for overriding the default behavior of
// squelching all output when the terminal is detected as headless
// (noninteractive)
func WithPrintWhileHeadless(p bool) Option {
	return func(b *Bar) {
		b.printWhileHeadless = p
	}
}

func WithPrintStepCounter(s bool) Option {
	return func(b *Bar) {
		b.printWithStepCounter = s
	}
}

func New(options ...Option) *Bar {
	b := &Bar{
		out: os.Stdout,
	}
	for _, o := range options {
		o(b)
	}
	return b
}

// SetTotal number of steps.
func (b *Bar) SetTotal(n int) {
	b.total = n
}

// Increment the currenly active step, including beginning printing on first call.
func (b *Bar) Increment(text string) {
	if b.index < b.total {
		b.index++
	}
	b.text = text

	// If this is not an interactive terminal, only print if explicitly set to
	// print while headless, and even then, only a simple line write.
	if !interactiveTerminal() {
		if b.printWhileHeadless {
			b.write()
		}
		return
	}

	// If we're in verbose mode, do a simple write
	if b.Verbose {
		b.write()
		return
	}

	// Start the spinner if not already started
	if b.ticker == nil {
		fmt.Println()
		b.ticker = time.NewTicker(100 * time.Millisecond)
		go b.spin(b.ticker.C)
	}

	// Otherwise we are in non-verbose, interactive mode.  Do a line-overwrite.
	b.overwrite("   ") // Write with space for the spinner
}

// Complete the spinner by advancing to the last step, printing the final text and stopping the write loop.
func (b *Bar) Complete(text string) {
	if !interactiveTerminal() && !b.printWhileHeadless {
		return
	}
	b.index = b.total // Skip to last step
	b.text = text

	// If this is not an interactive terminal, only print if explicitly set to
	// print while headless, and even then, only a simple line write.
	if !interactiveTerminal() {
		if b.printWhileHeadless {
			b.write()
		}
		return
	}

	// If we're interactive, but in verbose mode do a simple write
	if b.Verbose {
		b.write()
		return
	}

	// Otherwise we are in non-verbose mode with an interactive terminal.
	// We should stop the spinner and write an unindented line.
	b.Done() // stop spinner
}

// Done cancels the write loop if being used.
// Call in a defer statement after creation to ensure that the spinner stops
func (b *Bar) Done() {
	if b.ticker != nil {
		b.ticker.Stop()
		b.ticker = nil
	}
}

// Write a simple line status update.
func (b *Bar) write() {
	fmt.Fprintln(b.out, b)
}

// interactiveTerminal returns whether or not the currently attached process
// terminal is interactive.  Used for determining whether or not to
// interactively prompt the user to confirm default choices, etc.
func interactiveTerminal() bool {
	fi, err := os.Stdin.Stat()
	return err == nil && ((fi.Mode() & os.ModeCharDevice) != 0)
}

const (
	up    = "\033[1A"
	down  = "\033[1B"
	clear = "\033[K"
)

// overwrite the line prior with the bar text, optional prefix, creating an empty
// current line for potential errors/warnings.
func (b *Bar) overwrite(prefix string) {
	//  1 Move to the front of the current line
	//  2 Move up one line
	//  3 Clear to the end of the line
	//  4 Print status text with optional prefix (spinner)
	//  5 Print linebreak such that subsequent messages print correctly.
	fmt.Fprintf(b.out, "\r%v%v%v%v\n", up, clear, prefix, b)
}

func (b *Bar) String() string {
	if b.printWithStepCounter {
		return fmt.Sprintf("%v/%v %v", b.index, b.total, b.text)
	}
	return b.text
}

// Write a spinner at the beginning of the previous line.
func (b *Bar) spin(ch <-chan time.Time) {
	if b.Verbose {
		return
	}
	// Various options for spinners.
	// spinner := []string{"|", "/", "-", "\\"}
	// spinner := []string{"◢", "◣", "◤", "◥"}
	spinner := []string{
		"🕛 ",
		"🕐 ",
		"🕑 ",
		"🕒 ",
		"🕓 ",
		"🕔 ",
		"🕕 ",
		"🕖 ",
		"🕗 ",
		"🕘 ",
		"🕙 ",
		"🕚 ",
	}
	idx := 0
	for range ch {
		// Writes the spinner frame at the beginning of the previous line, moving
		// the cursor back to the beginning of the current line for any errors or
		// informative messages.
		fmt.Fprintf(b.out, "\r%v%v%v\r", up, spinner[idx], down)
		idx = (idx + 1) % len(spinner)
	}
}
