package color

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

	"github.com/mattn/go-colorable"
	"golang.org/x/term"
)

var (
	// NoColor defines if the output is colorized or not. It's dynamically set to
	// false or true based on the stdout's file descriptor referring to a terminal
	// or not. It's also set to true if the NO_COLOR environment variable is
	// set (regardless of its value). This is a global option and affects all
	// colors. For more control over each color block use the methods
	// DisableColor() individually.
	NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" ||
		(!term.IsTerminal(int(os.Stdout.Fd())))

	// Output defines the standard output of the print functions. By default,
	// os.Stdout is used.
	Output = colorable.NewColorableStdout()

	// Error defines a color supporting writer for os.Stderr.
	Error = colorable.NewColorableStderr()

	// colorsCache is used to reduce the count of created Color objects and
	// allows to reuse already created objects with required Attribute.
	colorsCache   = make(map[Attribute]*Color)
	colorsCacheMu sync.Mutex // protects colorsCache
)

// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string.
func noColorIsSet() bool {
	return os.Getenv("NO_COLOR") != ""
}

// Color defines a custom color object which is defined by SGR parameters.
type Color struct {
	params  []Attribute
	noColor *bool
}

// Attribute defines a single SGR Code.
type Attribute int

const escape = "\x1b"

// rgbColorFormatSpecifier represents the SGR parameter for 24-bit RGB color format.
const rgbColorFormatSpecifier Attribute = 2

// Base attributes.
const (
	Reset Attribute = iota
	Bold
	Faint
	Italic
	Underline
	BlinkSlow
	BlinkRapid
	ReverseVideo
	Concealed
	CrossedOut
)

const (
	ResetBold Attribute = iota + 22
	ResetItalic
	ResetUnderline
	ResetBlinking
	_
	ResetReversed
	ResetConcealed
	ResetCrossedOut
)

var mapResetAttributes = map[Attribute]Attribute{
	Bold:         ResetBold,
	Faint:        ResetBold,
	Italic:       ResetItalic,
	Underline:    ResetUnderline,
	BlinkSlow:    ResetBlinking,
	BlinkRapid:   ResetBlinking,
	ReverseVideo: ResetReversed,
	Concealed:    ResetConcealed,
	CrossedOut:   ResetCrossedOut,
}

// Foreground text colors.
const (
	FgBlack Attribute = iota + 30
	FgRed
	FgGreen
	FgYellow
	FgBlue
	FgMagenta
	FgCyan
	FgWhite

	// used internally for 256 and 24-bit coloring.
	foreground
)

// Foreground Hi-Intensity text colors.
const (
	FgHiBlack Attribute = iota + 90
	FgHiRed
	FgHiGreen
	FgHiYellow
	FgHiBlue
	FgHiMagenta
	FgHiCyan
	FgHiWhite
)

// Background text colors.
const (
	BgBlack Attribute = iota + 40
	BgRed
	BgGreen
	BgYellow
	BgBlue
	BgMagenta
	BgCyan
	BgWhite

	// used internally for 256 and 24-bit coloring.
	background
)

// Background Hi-Intensity text colors.
const (
	BgHiBlack Attribute = iota + 100
	BgHiRed
	BgHiGreen
	BgHiYellow
	BgHiBlue
	BgHiMagenta
	BgHiCyan
	BgHiWhite
)

// New returns a newly created color object.
func New(value ...Attribute) *Color {
	c := &Color{
		params: make([]Attribute, 0),
	}

	if noColorIsSet() {
		c.noColor = boolPtr(true)
	}

	c.Add(value...)

	return c
}

// RGB returns a new foreground color in 24-bit RGB.
func RGB(r, g, b int) *Color {
	return New(foreground, rgbColorFormatSpecifier, Attribute(r), Attribute(g), Attribute(b))
}

// BgRGB returns a new background color in 24-bit RGB.
func BgRGB(r, g, b int) *Color {
	return New(background, rgbColorFormatSpecifier, Attribute(r), Attribute(g), Attribute(b))
}

// AddRGB is used to chain foreground RGB SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: .Add(34, 0, 12).Add(255, 128, 0).
func (c *Color) AddRGB(r, green, blue int) *Color {
	c.params = append(
		c.params,
		foreground,
		rgbColorFormatSpecifier,
		Attribute(r),
		Attribute(green),
		Attribute(blue),
	)

	return c
}

// AddBgRGB is used to chain background RGB SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: .Add(34, 0, 12).Add(255, 128, 0).
func (c *Color) AddBgRGB(r, green, blue int) *Color {
	c.params = append(
		c.params,
		background,
		rgbColorFormatSpecifier,
		Attribute(r),
		Attribute(green),
		Attribute(blue),
	)

	return c
}

// Set sets the given parameters immediately. It will change the color of
// output with the given SGR parameters until color.Unset() is called.
func Set(p ...Attribute) *Color {
	c := New(p...)
	c.Set()

	return c
}

// Unset resets all escape attributes and clears the output. Usually should
// be called after Set().
func Unset() {
	if NoColor {
		return
	}

	_, _ = fmt.Fprintf(Output, "%s[%dm", escape, Reset)
}

// Set sets the SGR sequence.
func (c *Color) Set() *Color {
	if c.isNoColorSet() {
		return c
	}

	_, _ = fmt.Fprint(Output, c.format())

	return c
}

func (c *Color) unset() {
	if c.isNoColorSet() {
		return
	}

	Unset()
}

// SetWriter is used to set the SGR sequence with the given io.Writer. This is
// a low-level function, and users should use the higher-level functions, such
// as color.Fprint, color.Print, etc.
func (c *Color) SetWriter(w io.Writer) *Color {
	if c.isNoColorSet() {
		return c
	}

	_, _ = fmt.Fprint(w, c.format())

	return c
}

// UnsetWriter resets all escape attributes and clears the output with the give
// io.Writer. Usually should be called after SetWriter().
func (c *Color) UnsetWriter(w io.Writer) {
	if c.isNoColorSet() {
		return
	}

	_, _ = fmt.Fprintf(w, "%s[%dm", escape, Reset)
}

// Add is used to chain SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: Add(color.FgRed, color.Underline).
func (c *Color) Add(value ...Attribute) *Color {
	c.params = append(c.params, value...)

	return c
}

// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprint(w io.Writer, a ...any) (int, error) {
	c.SetWriter(w)
	defer c.UnsetWriter(w)

	n, err := fmt.Fprint(w, a...)

	return n, fmt.Errorf("failed to write colored output: %w", err)
}

// Print formats using the default formats for its operands and writes to
// standard output. Spaces are added between operands when neither is a
// string. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Print(a ...any) (int, error) {
	c.Set()
	defer c.unset()

	n, err := fmt.Fprint(Output, a...)

	return n, fmt.Errorf("failed to write colored output: %w", err)
}

// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintf(w io.Writer, format string, a ...any) (int, error) {
	c.SetWriter(w)
	defer c.UnsetWriter(w)

	n, err := fmt.Fprintf(w, format, a...)

	return n, fmt.Errorf("failed to write colored output: %w", err)
}

// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
// This is the standard fmt.Printf() method wrapped with the given color.
func (c *Color) Printf(format string, a ...any) (int, error) {
	c.Set()
	defer c.unset()

	n, err := fmt.Fprintf(Output, format, a...)

	return n, fmt.Errorf("failed to write colored output: %w", err)
}

// Fprintln formats using the default formats for its operands and writes to w.
// Spaces are always added between operands and a newline is appended.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintln(w io.Writer, a ...any) (int, error) {
	n, err := fmt.Fprintln(w, c.wrap(sprintln(a...)))

	return n, fmt.Errorf("failed to write colored output: %w", err)
}

// Println formats using the default formats for its operands and writes to
// standard output. Spaces are always added between operands and a newline is
// appended. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Println(a ...any) (int, error) {
	n, err := fmt.Fprintln(Output, c.wrap(sprintln(a...)))

	return n, fmt.Errorf("failed to write colored output: %w", err)
}

// Sprint is just like Print, but returns a string instead of printing it.
func (c *Color) Sprint(a ...any) string {
	return c.wrap(fmt.Sprint(a...))
}

// Sprintln is just like Println, but returns a string instead of printing it.
func (c *Color) Sprintln(a ...any) string {
	return c.wrap(sprintln(a...)) + "\n"
}

// Sprintf is just like Printf, but returns a string instead of printing it.
func (c *Color) Sprintf(format string, a ...any) string {
	return c.wrap(fmt.Sprintf(format, a...))
}

// FprintFunc returns a new function that prints the passed arguments as
// colorized with color.Fprint().
func (c *Color) FprintFunc() func(w io.Writer, a ...any) {
	return func(w io.Writer, a ...any) {
		_, _ = c.Fprint(w, a...)
	}
}

// PrintFunc returns a new function that prints the passed arguments as
// colorized with color.Print().
func (c *Color) PrintFunc() func(a ...any) {
	return func(a ...any) {
		_, _ = c.Print(a...)
	}
}

// FprintfFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintf().
func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...any) {
	return func(w io.Writer, format string, a ...any) {
		_, _ = c.Fprintf(w, format, a...)
	}
}

// PrintfFunc returns a new function that prints the passed arguments as
// colorized with color.Printf().
func (c *Color) PrintfFunc() func(format string, a ...any) {
	return func(format string, a ...any) {
		_, _ = c.Printf(format, a...)
	}
}

// FprintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintln().
func (c *Color) FprintlnFunc() func(w io.Writer, a ...any) {
	return func(w io.Writer, a ...any) {
		_, _ = c.Fprintln(w, a...)
	}
}

// PrintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Println().
func (c *Color) PrintlnFunc() func(a ...any) {
	return func(a ...any) {
		_, _ = c.Println(a...)
	}
}

// SprintFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprint(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output, example:
//
//	put := New(FgYellow).SprintFunc()
//	fmt.Fprintf(color.Output, "This is a %s", put("warning"))
func (c *Color) SprintFunc() func(a ...any) string {
	return func(a ...any) string {
		return c.wrap(fmt.Sprint(a...))
	}
}

// SprintfFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintf(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintfFunc() func(format string, a ...any) string {
	return func(format string, a ...any) string {
		return c.wrap(fmt.Sprintf(format, a...))
	}
}

// SprintlnFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintln(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintlnFunc() func(a ...any) string {
	return func(a ...any) string {
		return c.wrap(sprintln(a...)) + "\n"
	}
}

// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m"
// an example output might be: "1;36" -> bold cyan.
func (c *Color) sequence() string {
	format := make([]string, len(c.params))
	for i, v := range c.params {
		format[i] = strconv.Itoa(int(v))
	}

	return strings.Join(format, ";")
}

// wrap wraps the s string with the colors attributes. The string is ready to
// be printed.
func (c *Color) wrap(s string) string {
	if c.isNoColorSet() {
		return s
	}

	return c.format() + s + c.unformat()
}

func (c *Color) format() string {
	return fmt.Sprintf("%s[%sm", escape, c.sequence())
}

func (c *Color) unformat() string {
	// return fmt.Sprintf("%s[%dm", escape, Reset)
	// for each element in sequence let's use the specific reset escape, or the generic one if not found
	format := make([]string, len(c.params))
	for i, v := range c.params {
		format[i] = strconv.Itoa(int(Reset))

		ra, ok := mapResetAttributes[v]
		if ok {
			format[i] = strconv.Itoa(int(ra))
		}
	}

	return fmt.Sprintf("%s[%sm", escape, strings.Join(format, ";"))
}

// DisableColor disables the color output. Useful to not change any existing
// code and still being able to output. Can be used for flags like
// "--no-color". To enable back use EnableColor() method.
func (c *Color) DisableColor() {
	c.noColor = boolPtr(true)
}

// EnableColor enables the color output. Use it in conjunction with
// DisableColor(). Otherwise, this method has no side effects.
func (c *Color) EnableColor() {
	c.noColor = boolPtr(false)
}

func (c *Color) isNoColorSet() bool {
	// check first if we have user set action
	if c.noColor != nil {
		return *c.noColor
	}

	// if not return the global option, which is disabled by default
	return NoColor
}

// Equals returns a boolean value indicating whether two colors are equal.
func (c *Color) Equals(colorToCompare *Color) bool {
	if c == nil && colorToCompare == nil {
		return true
	}

	if c == nil || colorToCompare == nil {
		return false
	}

	if len(c.params) != len(colorToCompare.params) {
		return false
	}

	for _, attr := range c.params {
		if !colorToCompare.attrExists(attr) {
			return false
		}
	}

	return true
}

func (c *Color) attrExists(a Attribute) bool {
	for _, attr := range c.params {
		if attr == a {
			return true
		}
	}

	return false
}

func boolPtr(v bool) *bool {
	return &v
}

// getCachedColor retrieves a cached color object for the given attribute, creating it if necessary.
func getCachedColor(attribute Attribute) *Color {
	colorsCacheMu.Lock()
	defer colorsCacheMu.Unlock()

	c, ok := colorsCache[attribute]
	if !ok {
		c = New(attribute)
		colorsCache[attribute] = c
	}

	return c
}

// colorPrint prints formatted text with the specified color attribute.
func colorPrint(format string, attribute Attribute, arguments ...any) {
	c := getCachedColor(attribute)

	if !strings.HasSuffix(format, "\n") {
		format += "\n"
	}

	if len(arguments) == 0 {
		_, _ = c.Print(format)
	} else {
		_, _ = c.Printf(format, arguments...)
	}
}

// colorString returns a formatted string with the specified color attribute.
func colorString(format string, attribute Attribute, arguments ...any) string {
	c := getCachedColor(attribute)

	if len(arguments) == 0 {
		return c.SprintFunc()(format)
	}

	return c.SprintfFunc()(format, arguments...)
}

// Blackf is a convenient helper function to print with black foreground. A
// newline is appended to format by default.
func Blackf(format string, a ...any) { colorPrint(format, FgBlack, a...) }

// Redf is a convenient helper function to print with red foreground. A
// newline is appended to format by default.
func Redf(format string, a ...any) { colorPrint(format, FgRed, a...) }

// Greenf is a convenient helper function to print with green foreground. A
// newline is appended to format by default.
func Greenf(format string, a ...any) { colorPrint(format, FgGreen, a...) }

// Yellowf is a convenient helper function to print with yellow foreground.
// A newline is appended to format by default.
func Yellowf(format string, a ...any) { colorPrint(format, FgYellow, a...) }

// Bluef is a convenient helper function to print with blue foreground. A
// newline is appended to format by default.
func Bluef(format string, a ...any) { colorPrint(format, FgBlue, a...) }

// Magentaf is a convenient helper function to print with magenta foreground.
// A newline is appended to format by default.
func Magentaf(format string, a ...any) { colorPrint(format, FgMagenta, a...) }

// Cyanf is a convenient helper function to print with cyan foreground. A
// newline is appended to format by default.
func Cyanf(format string, a ...any) { colorPrint(format, FgCyan, a...) }

// Whitef is a convenient helper function to print with white foreground. A
// newline is appended to format by default.
func Whitef(format string, a ...any) { colorPrint(format, FgWhite, a...) }

// BlackString is a convenient helper function to return a string with black
// foreground.
func BlackString(format string, a ...any) string { return colorString(format, FgBlack, a...) }

// RedString is a convenient helper function to return a string with red
// foreground.
func RedString(format string, a ...any) string { return colorString(format, FgRed, a...) }

// GreenString is a convenient helper function to return a string with green
// foreground.
func GreenString(format string, a ...any) string { return colorString(format, FgGreen, a...) }

// YellowString is a convenient helper function to return a string with yellow
// foreground.
func YellowString(format string, a ...any) string { return colorString(format, FgYellow, a...) }

// BlueString is a convenient helper function to return a string with blue
// foreground.
func BlueString(format string, a ...any) string { return colorString(format, FgBlue, a...) }

// MagentaString is a convenient helper function to return a string with magenta
// foreground.
func MagentaString(format string, a ...any) string {
	return colorString(format, FgMagenta, a...)
}

// CyanString is a convenient helper function to return a string with cyan
// foreground.
func CyanString(format string, a ...any) string { return colorString(format, FgCyan, a...) }

// WhiteString is a convenient helper function to return a string with white
// foreground.
func WhiteString(format string, a ...any) string { return colorString(format, FgWhite, a...) }

// HiBlackf is a convenient helper function to print with hi-intensity black foreground. A
// newline is appended to format by default.
func HiBlackf(format string, a ...any) { colorPrint(format, FgHiBlack, a...) }

// HiRedf is a convenient helper function to print with hi-intensity red foreground. A
// newline is appended to format by default.
func HiRedf(format string, a ...any) { colorPrint(format, FgHiRed, a...) }

// HiGreenf is a convenient helper function to print with hi-intensity green foreground. A
// newline is appended to format by default.
func HiGreenf(format string, a ...any) { colorPrint(format, FgHiGreen, a...) }

// HiYellowf is a convenient helper function to print with hi-intensity yellow foreground.
// A newline is appended to format by default.
func HiYellowf(format string, a ...any) { colorPrint(format, FgHiYellow, a...) }

// HiBluef is a convenient helper function to print with hi-intensity blue foreground. A
// newline is appended to format by default.
func HiBluef(format string, a ...any) { colorPrint(format, FgHiBlue, a...) }

// HiMagentaf is a convenient helper function to print with hi-intensity magenta foreground.
// A newline is appended to format by default.
func HiMagentaf(format string, a ...any) { colorPrint(format, FgHiMagenta, a...) }

// HiCyanf is a convenient helper function to print with hi-intensity cyan foreground. A
// newline is appended to format by default.
func HiCyanf(format string, a ...any) { colorPrint(format, FgHiCyan, a...) }

// HiWhitef is a convenient helper function to print with hi-intensity white foreground. A
// newline is appended to format by default.
func HiWhitef(format string, a ...any) { colorPrint(format, FgHiWhite, a...) }

// HiBlackString is a convenient helper function to return a string with hi-intensity black
// foreground.
func HiBlackString(format string, a ...any) string {
	return colorString(format, FgHiBlack, a...)
}

// HiRedString is a convenient helper function to return a string with hi-intensity red
// foreground.
func HiRedString(format string, a ...any) string { return colorString(format, FgHiRed, a...) }

// HiGreenString is a convenient helper function to return a string with hi-intensity green
// foreground.
func HiGreenString(format string, a ...any) string {
	return colorString(format, FgHiGreen, a...)
}

// HiYellowString is a convenient helper function to return a string with hi-intensity yellow
// foreground.
func HiYellowString(format string, a ...any) string {
	return colorString(format, FgHiYellow, a...)
}

// HiBlueString is a convenient helper function to return a string with hi-intensity blue
// foreground.
func HiBlueString(format string, a ...any) string { return colorString(format, FgHiBlue, a...) }

// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta
// foreground.
func HiMagentaString(format string, a ...any) string {
	return colorString(format, FgHiMagenta, a...)
}

// HiCyanString is a convenient helper function to return a string with hi-intensity cyan
// foreground.
func HiCyanString(format string, a ...any) string { return colorString(format, FgHiCyan, a...) }

// HiWhiteString is a convenient helper function to return a string with hi-intensity white
// foreground.
func HiWhiteString(format string, a ...any) string {
	return colorString(format, FgHiWhite, a...)
}

// sprintln is a helper function to format a string with fmt.Sprintln and trim the trailing newline.
func sprintln(a ...any) string {
	return strings.TrimSuffix(fmt.Sprintln(a...), "\n")
}
