package msg

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"
	"sync"

	"github.com/Masterminds/vcs"
)

// Messenger provides the underlying implementation that displays output to
// users.
type Messenger struct {
	sync.Mutex

	// Quiet, if true, suppresses chatty levels, like Info.
	Quiet bool

	// IsDebugging, if true, shows Debug.
	IsDebugging bool

	// NoColor, if true, will not use color in the output.
	NoColor bool

	// Stdout is the location where this prints output.
	Stdout io.Writer

	// Stderr is the location where this prints logs.
	Stderr io.Writer

	// Stdin is the location where input is read.
	Stdin io.Reader

	// PanicOnDie if true Die() will panic instead of exiting.
	PanicOnDie bool

	// The default exit code to use when dyping
	ecode int

	// If an error was been sent.
	hasErrored bool
}

// NewMessenger creates a default Messenger to display output.
func NewMessenger() *Messenger {
	m := &Messenger{
		Quiet:       false,
		IsDebugging: false,
		NoColor:     false,
		Stdout:      os.Stdout,
		Stderr:      os.Stderr,
		Stdin:       os.Stdin,
		PanicOnDie:  false,
		ecode:       1,
	}

	return m
}

// Default contains a default Messenger used by package level functions
var Default = NewMessenger()

// Info logs information
func (m *Messenger) Info(msg string, args ...interface{}) {
	if m.Quiet {
		return
	}
	prefix := m.Color(Green, "[INFO]\t")
	m.Msg(prefix+msg, args...)
}

// Info logs information using the Default Messenger
func Info(msg string, args ...interface{}) {
	Default.Info(msg, args...)
}

// Debug logs debug information
func (m *Messenger) Debug(msg string, args ...interface{}) {
	if m.Quiet || !m.IsDebugging {
		return
	}
	prefix := "[DEBUG]\t"
	m.Msg(prefix+msg, args...)
}

// Debug logs debug information using the Default Messenger
func Debug(msg string, args ...interface{}) {
	Default.Debug(msg, args...)
}

// Warn logs a warning
func (m *Messenger) Warn(msg string, args ...interface{}) {
	prefix := m.Color(Yellow, "[WARN]\t")
	m.Msg(prefix+msg, args...)
}

// Warn logs a warning using the Default Messenger
func Warn(msg string, args ...interface{}) {
	Default.Warn(msg, args...)
}

// Err logs an error.
func (m *Messenger) Err(msg string, args ...interface{}) {
	prefix := m.Color(Red, "[ERROR]\t")
	m.Msg(prefix+msg, args...)
	m.hasErrored = true
}

// Err logs anderror using the Default Messenger
func Err(msg string, args ...interface{}) {
	Default.Err(msg, args...)
}

// Die prints an error message and immediately exits the application.
// If PanicOnDie is set to true a panic will occur instead of os.Exit being
// called.
func (m *Messenger) Die(msg string, args ...interface{}) {
	m.Err(msg, args...)
	if m.PanicOnDie {
		panic("trapped a Die() call")
	}
	os.Exit(m.ecode)
}

// Die prints an error message and immediately exits the application using the
// Default Messenger. If PanicOnDie is set to true a panic will occur instead of
// os.Exit being called.
func Die(msg string, args ...interface{}) {
	Default.Die(msg, args...)
}

// ExitCode sets the exit code used by Die.
//
// The default is 1.
//
// Returns the old error code.
func (m *Messenger) ExitCode(e int) int {
	m.Lock()
	old := m.ecode
	m.ecode = e
	m.Unlock()
	return old
}

// ExitCode sets the exit code used by Die using the Default Messenger.
//
// The default is 1.
//
// Returns the old error code.
func ExitCode(e int) int {
	return Default.ExitCode(e)
}

// Msg prints a message with optional arguments, that can be printed, of
// varying types.
func (m *Messenger) Msg(msg string, args ...interface{}) {
	// When operations in Glide are happening concurrently messaging needs to be
	// locked to avoid displaying one message in the middle of another one.
	m.Lock()
	defer m.Unlock()
	// Get rid of the annoying fact that messages need \n at the end, but do
	// it in a backward compatible way.
	if !strings.HasSuffix(msg, "\n") {
		msg += "\n"
	}

	if len(args) == 0 {
		fmt.Fprint(m.Stderr, msg)
	} else {
		fmt.Fprintf(m.Stderr, msg, args...)
	}

	// If an arg is a vcs error print the output if in debug mode. This is
	// capured here rather than calling Debug because concurrent operations
	// could cause other messages to appear between the initial error and the
	// debug output by unlocking and calling Debug.
	if len(args) != 0 && !m.Quiet && m.IsDebugging {
		if err, ok := args[len(args)-1].(error); ok {
			switch t := err.(type) {
			case *vcs.LocalError:
				fmt.Fprintf(m.Stderr, "[DEBUG]\tOutput was: %s", strings.TrimSpace(t.Out()))
			case *vcs.RemoteError:
				fmt.Fprintf(m.Stderr, "[DEBUG]\tOutput was: %s", strings.TrimSpace(t.Out()))
			}
		}
	}
}

// Msg prints a message with optional arguments, that can be printed, of
// varying types using the Default Messenger.
func Msg(msg string, args ...interface{}) {
	Default.Msg(msg, args...)
}

// Puts formats a message and then prints to Stdout.
//
// It does not prefix the message, does not color it, or otherwise decorate it.
//
// It does add a line feed.
func (m *Messenger) Puts(msg string, args ...interface{}) {
	// When operations in Glide are happening concurrently messaging needs to be
	// locked to avoid displaying one message in the middle of another one.
	m.Lock()
	defer m.Unlock()

	fmt.Fprintf(m.Stdout, msg, args...)
	fmt.Fprintln(m.Stdout)
}

// Puts formats a message and then prints to Stdout using the Default Messenger.
//
// It does not prefix the message, does not color it, or otherwise decorate it.
//
// It does add a line feed.
func Puts(msg string, args ...interface{}) {
	Default.Puts(msg, args...)
}

// Print prints exactly the string given.
//
// It prints to Stdout.
func (m *Messenger) Print(msg string) {
	// When operations in Glide are happening concurrently messaging needs to be
	// locked to avoid displaying one message in the middle of another one.
	m.Lock()
	defer m.Unlock()

	fmt.Fprint(m.Stdout, msg)
}

// Print prints exactly the string given using the Default Messenger.
//
// It prints to Stdout.
func Print(msg string) {
	Default.Print(msg)
}

// HasErrored returns if Error has been called.
//
// This is useful if you want to known if Error was called to exit with a
// non-zero exit code.
func (m *Messenger) HasErrored() bool {
	return m.hasErrored
}

// HasErrored returns if Error has been called on the Default Messenger.
//
// This is useful if you want to known if Error was called to exit with a
// non-zero exit code.
func HasErrored() bool {
	return Default.HasErrored()
}

// Color returns a string in a certain color if colors are enabled and
// available on that platform.
func Color(code, msg string) string {
	return Default.Color(code, msg)
}

// PromptUntil provides a prompt until one of the passed in strings has been
// entered and return is hit. Note, the comparisons are case insensitive meaning
// Y == y. The returned value is the one from the passed in options (same case).
func (m *Messenger) PromptUntil(opts []string) (string, error) {
	reader := bufio.NewReader(os.Stdin)
	for {
		text, err := reader.ReadString('\n')
		if err != nil {
			return "", err
		}

		for _, c := range opts {
			if strings.EqualFold(c, strings.TrimSpace(text)) {
				return c, nil
			}
		}
	}
}

// PromptUntil provides a prompt until one of the passed in strings has been
// entered and return is hit. Note, the comparisons are case insensitive meaning
// Y == y. The returned value is the one from the passed in options (same case).
// Uses the default setup.
func PromptUntil(opts []string) (string, error) {
	return Default.PromptUntil(opts)
}

// PromptUntilYorN provides a prompt until the user chooses yes or no. This is
// not case sensitive and they can input other options such as Y or N.
// In the response Yes is bool true and No is bool false.
func (m *Messenger) PromptUntilYorN() bool {
	res, err := m.PromptUntil([]string{"y", "yes", "n", "no"})
	if err != nil {
		m.Die("Error processing response: %s", err)
	}

	if res == "y" || res == "yes" {
		return true
	}

	return false
}

// PromptUntilYorN provides a prompt until the user chooses yes or no. This is
// not case sensitive and they can input other options such as Y or N.
// In the response Yes is bool true and No is bool false.
// Uses the default setup.
func PromptUntilYorN() bool {
	return Default.PromptUntilYorN()
}
