// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmdutil

import (
	"fmt"
	"time"
	"unicode/utf8"

	"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
)

// NewSpinnerAndTicker returns a new Spinner and a ticker that will fire an event when the next call
// to Spinner.Tick() should be called.  NewSpinnerAndTicket takes into account if stdout is
// connected to a tty or not and returns either a nice animated spinner that updates quickly, using
// the specified ttyFrames, or a simple spinner that just prints a dot on each tick and updates
// slowly.
func NewSpinnerAndTicker(prefix string, ttyFrames []string,
	color colors.Colorization, timesPerSecond time.Duration,
	suppressProgress bool,
) (Spinner, *time.Ticker) {
	if ttyFrames == nil {
		// If explicit tick frames weren't specified, default to unicode for Mac and ASCII for Windows/Linux.
		if Emoji {
			ttyFrames = DefaultEmojiSpinFrames
		} else {
			ttyFrames = DefaultASCIISpinFrames
		}
	}

	if suppressProgress {
		return &noopSpinner{}, time.NewTicker(time.Second * 20)
	}

	if Interactive() {
		return &ttySpinner{
			prefix: prefix,
			frames: ttyFrames,
		}, time.NewTicker(time.Second / timesPerSecond)
	}
	return &dotSpinner{
		color:  color,
		prefix: prefix,
	}, time.NewTicker(time.Second * 20)
}

// Spinner represents a very simple progress reporter.
type Spinner interface {
	// Tick prints the next frame of the spinner. After Tick() has been called, there should be no writes to Stdout before
	// calling Reset().
	Tick()

	// Reset is called to release ownership of stdout, so others may write to it.
	Reset()
}

var (
	// DefaultASCIISpinFrames is the default set of symbols to show while spinning in an ASCII TTY setting.
	DefaultASCIISpinFrames = []string{
		"|", "/", "-", "\\",
	}
	// DefaultEmojiSpinFrames is the default set of symbols to show while spinning in a Unicode-enabled TTY setting.
	DefaultEmojiSpinFrames = []string{
		"⠋", "⠙", "⠚", "⠒", "⠂", "⠂", "⠒", "⠲", "⠴", "⠦", "⠖", "⠒", "⠐", "⠐", "⠒", "⠓", "⠋",
	}
)

// ttySpinner is the spinner that can be used when standard out is a tty. When we are connected to a TTY we can erase
// characters we've written and provide a nice quick progress spinner.
type ttySpinner struct {
	prefix      string
	frames      []string
	index       int
	lastWritten int
}

func (spin *ttySpinner) Tick() {
	if spin.lastWritten > 0 {
		for i := 0; i < spin.lastWritten; i++ {
			fmt.Print("\b \b")
		}
	} else {
		fmt.Print(spin.prefix)
	}
	frame := spin.frames[spin.index]
	fmt.Print(frame)
	spin.lastWritten = utf8.RuneCountInString(frame)
	spin.index = (spin.index + 1) % len(spin.frames)
}

func (spin *ttySpinner) Reset() {
	if spin.lastWritten > 0 {
		for i := 0; i < len(spin.prefix)+spin.lastWritten; i++ {
			fmt.Print("\b \b")
		}
	}
	spin.index = 0
	spin.lastWritten = 0
}

// dotSpinner is the spinner that can be used when standard out is not a tty. In this case, we just write a single
// dot on each tick.
type dotSpinner struct {
	color      colors.Colorization
	prefix     string
	hasWritten bool
}

func (spin *dotSpinner) Tick() {
	if !spin.hasWritten {
		fmt.Print(spin.color.Colorize(colors.Yellow + spin.prefix + colors.Reset))
	}
	fmt.Print(spin.color.Colorize(colors.Yellow + "." + colors.Reset))
	spin.hasWritten = true
}

func (spin *dotSpinner) Reset() {
	if spin.hasWritten {
		fmt.Println()
	}
	spin.hasWritten = false
}

type noopSpinner struct{}

func (spin *noopSpinner) Tick()  {}
func (spin *noopSpinner) Reset() {}