package pterm

import (
	"fmt"
	"io"
	"strings"

	"github.com/mattn/go-runewidth"

	"github.com/pterm/pterm/internal"
)

// BoxPrinter is able to render a box around printables.
type BoxPrinter struct {
	Title                   string
	TitleTopLeft            bool
	TitleTopRight           bool
	TitleTopCenter          bool
	TitleBottomLeft         bool
	TitleBottomRight        bool
	TitleBottomCenter       bool
	TextStyle               *Style
	VerticalString          string
	BoxStyle                *Style
	HorizontalString        string
	TopRightCornerString    string
	TopLeftCornerString     string
	BottomLeftCornerString  string
	BottomRightCornerString string
	TopPadding              int
	BottomPadding           int
	RightPadding            int
	LeftPadding             int
	Writer                  io.Writer
}

// DefaultBox is the default BoxPrinter.
var DefaultBox = BoxPrinter{
	VerticalString:          "|",
	TopRightCornerString:    "└",
	TopLeftCornerString:     "┘",
	BottomLeftCornerString:  "┐",
	BottomRightCornerString: "┌",
	HorizontalString:        "─",
	BoxStyle:                &ThemeDefault.BoxStyle,
	TextStyle:               &ThemeDefault.BoxTextStyle,
	RightPadding:            1,
	LeftPadding:             1,
	TopPadding:              0,
	BottomPadding:           0,
	TitleTopLeft:            true,
}

// WithTitle returns a new box with a specific Title.
func (p BoxPrinter) WithTitle(str string) *BoxPrinter {
	p.Title = str
	return &p
}

// WithTitleTopLeft returns a new box with a specific Title alignment.
func (p BoxPrinter) WithTitleTopLeft(b ...bool) *BoxPrinter {
	b2 := internal.WithBoolean(b)
	p.TitleTopLeft = b2
	p.TitleTopRight = false
	p.TitleTopCenter = false
	p.TitleBottomLeft = false
	p.TitleBottomRight = false
	p.TitleBottomCenter = false
	return &p
}

// WithTitleTopRight returns a new box with a specific Title alignment.
func (p BoxPrinter) WithTitleTopRight(b ...bool) *BoxPrinter {
	b2 := internal.WithBoolean(b)
	p.TitleTopLeft = false
	p.TitleTopRight = b2
	p.TitleTopCenter = false
	p.TitleBottomLeft = false
	p.TitleBottomRight = false
	p.TitleBottomCenter = false
	return &p
}

// WithTitleTopCenter returns a new box with a specific Title alignment.
func (p BoxPrinter) WithTitleTopCenter(b ...bool) *BoxPrinter {
	b2 := internal.WithBoolean(b)
	p.TitleTopLeft = false
	p.TitleTopRight = false
	p.TitleTopCenter = b2
	p.TitleBottomLeft = false
	p.TitleBottomRight = false
	p.TitleBottomCenter = false
	return &p
}

// WithTitleBottomLeft returns a new box with a specific Title alignment.
func (p BoxPrinter) WithTitleBottomLeft(b ...bool) *BoxPrinter {
	b2 := internal.WithBoolean(b)
	p.TitleTopLeft = false
	p.TitleTopRight = false
	p.TitleTopCenter = false
	p.TitleBottomLeft = b2
	p.TitleBottomRight = false
	p.TitleBottomCenter = false
	return &p
}

// WithTitleBottomRight returns a new box with a specific Title alignment.
func (p BoxPrinter) WithTitleBottomRight(b ...bool) *BoxPrinter {
	b2 := internal.WithBoolean(b)
	p.TitleTopLeft = false
	p.TitleTopRight = false
	p.TitleTopCenter = false
	p.TitleBottomLeft = false
	p.TitleBottomRight = b2
	p.TitleBottomCenter = false
	return &p
}

// WithTitleBottomCenter returns a new box with a specific Title alignment.
func (p BoxPrinter) WithTitleBottomCenter(b ...bool) *BoxPrinter {
	b2 := internal.WithBoolean(b)
	p.TitleTopLeft = false
	p.TitleTopRight = false
	p.TitleTopCenter = false
	p.TitleBottomLeft = false
	p.TitleBottomRight = false
	p.TitleBottomCenter = b2
	return &p
}

// WithBoxStyle returns a new box with a specific box Style.
func (p BoxPrinter) WithBoxStyle(style *Style) *BoxPrinter {
	p.BoxStyle = style
	return &p
}

// WithTextStyle returns a new box with a specific text Style.
func (p BoxPrinter) WithTextStyle(style *Style) *BoxPrinter {
	p.TextStyle = style
	return &p
}

// WithTopRightCornerString returns a new box with a specific TopRightCornerString.
func (p BoxPrinter) WithTopRightCornerString(str string) *BoxPrinter {
	p.TopRightCornerString = str
	return &p
}

// WithTopLeftCornerString returns a new box with a specific TopLeftCornerString.
func (p BoxPrinter) WithTopLeftCornerString(str string) *BoxPrinter {
	p.TopLeftCornerString = str
	return &p
}

// WithBottomRightCornerString returns a new box with a specific BottomRightCornerString.
func (p BoxPrinter) WithBottomRightCornerString(str string) *BoxPrinter {
	p.BottomRightCornerString = str
	return &p
}

// WithBottomLeftCornerString returns a new box with a specific BottomLeftCornerString.
func (p BoxPrinter) WithBottomLeftCornerString(str string) *BoxPrinter {
	p.BottomLeftCornerString = str
	return &p
}

// WithVerticalString returns a new box with a specific VerticalString.
func (p BoxPrinter) WithVerticalString(str string) *BoxPrinter {
	p.VerticalString = str
	return &p
}

// WithHorizontalString returns a new box with a specific HorizontalString.
func (p BoxPrinter) WithHorizontalString(str string) *BoxPrinter {
	p.HorizontalString = str
	return &p
}

// WithTopPadding returns a new box with a specific TopPadding.
func (p BoxPrinter) WithTopPadding(padding int) *BoxPrinter {
	if padding < 0 {
		padding = 0
	}
	p.TopPadding = padding
	return &p
}

// WithBottomPadding returns a new box with a specific BottomPadding.
func (p BoxPrinter) WithBottomPadding(padding int) *BoxPrinter {
	if padding < 0 {
		padding = 0
	}
	p.BottomPadding = padding
	return &p
}

// WithRightPadding returns a new box with a specific RightPadding.
func (p BoxPrinter) WithRightPadding(padding int) *BoxPrinter {
	if padding < 0 {
		padding = 0
	}
	p.RightPadding = padding
	return &p
}

// WithLeftPadding returns a new box with a specific LeftPadding.
func (p BoxPrinter) WithLeftPadding(padding int) *BoxPrinter {
	if padding < 0 {
		padding = 0
	}
	p.LeftPadding = padding
	return &p
}

// WithWriter sets the custom Writer.
func (p BoxPrinter) WithWriter(writer io.Writer) *BoxPrinter {
	p.Writer = writer
	return &p
}

// Sprint formats using the default formats for its operands and returns the resulting string.
// Spaces are added between operands when neither is a string.
func (p BoxPrinter) Sprint(a ...interface{}) string {
	if p.BoxStyle == nil {
		p.BoxStyle = &ThemeDefault.BoxStyle
	}
	if p.TextStyle == nil {
		p.TextStyle = &ThemeDefault.BoxTextStyle
	}
	maxWidth := internal.GetStringMaxWidth(Sprint(a...))

	var topLine string
	var bottomLine string

	if p.Title == "" {
		topLine = p.BoxStyle.Sprint(p.BottomRightCornerString) + strings.Repeat(p.BoxStyle.Sprint(p.HorizontalString),
			maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.BottomLeftCornerString)
		bottomLine = p.BoxStyle.Sprint(p.TopRightCornerString) + strings.Repeat(p.BoxStyle.Sprint(p.HorizontalString),
			maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.TopLeftCornerString)
	} else {
		p.Title = strings.ReplaceAll(p.Title, "\n", " ")
		if (maxWidth + p.RightPadding + p.LeftPadding - 4) < internal.GetStringMaxWidth(p.Title) {
			p.RightPadding = internal.GetStringMaxWidth(p.Title) - (maxWidth + p.RightPadding + p.LeftPadding - 5)
		}
		if p.TitleTopLeft {
			topLine = p.BoxStyle.Sprint(p.BottomRightCornerString) + internal.AddTitleToLine(p.Title, p.BoxStyle.Sprint(p.HorizontalString), maxWidth+p.LeftPadding+p.RightPadding, true) + p.BoxStyle.Sprint(p.BottomLeftCornerString)
			bottomLine = p.BoxStyle.Sprint(p.TopRightCornerString) + strings.Repeat(p.BoxStyle.Sprint(p.HorizontalString),
				maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.TopLeftCornerString)
		} else if p.TitleTopRight {
			topLine = p.BoxStyle.Sprint(p.BottomRightCornerString) + internal.AddTitleToLine(p.Title, p.BoxStyle.Sprint(p.HorizontalString), maxWidth+p.LeftPadding+p.RightPadding, false) + p.BoxStyle.Sprint(p.BottomLeftCornerString)
			bottomLine = p.BoxStyle.Sprint(p.TopRightCornerString) + strings.Repeat(p.BoxStyle.Sprint(p.HorizontalString),
				maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.TopLeftCornerString)
		} else if p.TitleTopCenter {
			topLine = p.BoxStyle.Sprint(p.BottomRightCornerString) + internal.AddTitleToLineCenter(p.Title, p.BoxStyle.Sprint(p.HorizontalString), maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.BottomLeftCornerString)
			bottomLine = p.BoxStyle.Sprint(p.TopRightCornerString) + strings.Repeat(p.BoxStyle.Sprint(p.HorizontalString),
				maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.TopLeftCornerString)
		} else if p.TitleBottomLeft {
			topLine = p.BoxStyle.Sprint(p.BottomRightCornerString) + strings.Repeat(p.BoxStyle.Sprint(p.HorizontalString),
				maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.BottomLeftCornerString)
			bottomLine = p.BoxStyle.Sprint(p.TopRightCornerString) + internal.AddTitleToLine(p.Title, p.BoxStyle.Sprint(p.HorizontalString), maxWidth+p.LeftPadding+p.RightPadding, true) + p.BoxStyle.Sprint(p.TopLeftCornerString)
		} else if p.TitleBottomRight {
			topLine = p.BoxStyle.Sprint(p.BottomRightCornerString) + strings.Repeat(p.BoxStyle.Sprint(p.HorizontalString),
				maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.BottomLeftCornerString)
			bottomLine = p.BoxStyle.Sprint(p.TopRightCornerString) + internal.AddTitleToLine(p.Title, p.BoxStyle.Sprint(p.HorizontalString), maxWidth+p.LeftPadding+p.RightPadding, false) + p.BoxStyle.Sprint(p.TopLeftCornerString)
		} else if p.TitleBottomCenter {
			topLine = p.BoxStyle.Sprint(p.BottomRightCornerString) + strings.Repeat(p.BoxStyle.Sprint(p.HorizontalString),
				maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.BottomLeftCornerString)
			bottomLine = p.BoxStyle.Sprint(p.TopRightCornerString) + internal.AddTitleToLineCenter(p.Title, p.BoxStyle.Sprint(p.HorizontalString), maxWidth+p.LeftPadding+p.RightPadding) + p.BoxStyle.Sprint(p.TopLeftCornerString)
		}
	}

	boxString := strings.Repeat("\n", p.TopPadding) + Sprint(a...) + strings.Repeat("\n", p.BottomPadding)

	ss := strings.Split(boxString, "\n")
	for i, s2 := range ss {
		if runewidth.StringWidth(RemoveColorFromString(s2)) < maxWidth {
			ss[i] = p.BoxStyle.Sprint(p.VerticalString) + strings.Repeat(" ", p.LeftPadding) + p.TextStyle.Sprint(s2) +
				strings.Repeat(" ", maxWidth-runewidth.StringWidth(RemoveColorFromString(s2))+p.RightPadding) +
				p.BoxStyle.Sprint(p.VerticalString)
		} else {
			ss[i] = p.BoxStyle.Sprint(p.VerticalString) + strings.Repeat(" ", p.LeftPadding) + p.TextStyle.Sprint(s2) +
				strings.Repeat(" ", p.RightPadding) + p.BoxStyle.Sprint(p.VerticalString)
		}
	}
	return topLine + "\n" + strings.Join(ss, "\n") + "\n" + bottomLine
}

// Sprintln formats using the default formats for its operands and returns the resulting string.
// Spaces are always added between operands and a newline is appended.
func (p BoxPrinter) Sprintln(a ...interface{}) string {
	return p.Sprint(strings.TrimSuffix(Sprintln(a...), "\n")) + "\n"
}

// Sprintf formats according to a format specifier and returns the resulting string.
func (p BoxPrinter) Sprintf(format string, a ...interface{}) string {
	return p.Sprint(Sprintf(format, a...))
}

// Sprintfln formats according to a format specifier and returns the resulting string.
// Spaces are always added between operands and a newline is appended.
func (p BoxPrinter) Sprintfln(format string, a ...interface{}) string {
	return p.Sprintf(format, a...) + "\n"
}

// 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.
func (p BoxPrinter) Print(a ...interface{}) *TextPrinter {
	Fprint(p.Writer, p.Sprint(a...))
	tp := TextPrinter(p)
	return &tp
}

// 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.
func (p BoxPrinter) Println(a ...interface{}) *TextPrinter {
	Fprint(p.Writer, p.Sprintln(a...))
	tp := TextPrinter(p)
	return &tp
}

// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
func (p BoxPrinter) Printf(format string, a ...interface{}) *TextPrinter {
	Fprint(p.Writer, p.Sprintf(format, a...))
	tp := TextPrinter(p)
	return &tp
}

// Printfln formats according to a format specifier 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.
func (p BoxPrinter) Printfln(format string, a ...interface{}) *TextPrinter {
	Fprint(p.Writer, p.Sprintfln(format, a...))
	tp := TextPrinter(p)
	return &tp
}

// PrintOnError prints every error which is not nil.
// If every error is nil, nothing will be printed.
// This can be used for simple error checking.
func (p BoxPrinter) PrintOnError(a ...interface{}) *TextPrinter {
	for _, arg := range a {
		if err, ok := arg.(error); ok {
			if err != nil {
				p.Println(err)
			}
		}
	}

	tp := TextPrinter(p)
	return &tp
}

// PrintOnErrorf wraps every error which is not nil and prints it.
// If every error is nil, nothing will be printed.
// This can be used for simple error checking.
func (p BoxPrinter) PrintOnErrorf(format string, a ...interface{}) *TextPrinter {
	for _, arg := range a {
		if err, ok := arg.(error); ok {
			if err != nil {
				p.Println(fmt.Errorf(format, err))
			}
		}
	}

	tp := TextPrinter(p)
	return &tp
}
