package text

import (
	"fmt"
	"strconv"
	"strings"
	"unicode/utf8"
)

// Align denotes how text is to be aligned horizontally.
type Align int

// Align enumerations
const (
	AlignDefault Align = iota // same as AlignLeft
	AlignLeft                 // "left        "
	AlignCenter               // "   center   "
	AlignJustify              // "justify   it"
	AlignRight                // "       right"
)

// Apply aligns the text as directed. For ex.:
//  * AlignDefault.Apply("Jon Snow", 12) returns "Jon Snow    "
//  * AlignLeft.Apply("Jon Snow",    12) returns "Jon Snow    "
//  * AlignCenter.Apply("Jon Snow",  12) returns "  Jon Snow  "
//  * AlignJustify.Apply("Jon Snow", 12) returns "Jon     Snow"
//  * AlignRight.Apply("Jon Snow",   12) returns "    Jon Snow"
func (a Align) Apply(text string, maxLength int) string {
	text = a.trimString(text)
	sLen := utf8.RuneCountInString(text)
	sLenWoE := RuneCount(text)
	numEscChars := sLen - sLenWoE

	// now, align the text
	switch a {
	case AlignDefault, AlignLeft:
		return fmt.Sprintf("%-"+strconv.Itoa(maxLength+numEscChars)+"s", text)
	case AlignCenter:
		if sLenWoE < maxLength {
			// left pad with half the number of spaces needed before using %text
			return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s",
				text+strings.Repeat(" ", int((maxLength-sLenWoE)/2)))
		}
	case AlignJustify:
		return a.justifyText(text, sLenWoE, maxLength)
	}
	return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text)
}

// HTMLProperty returns the equivalent HTML horizontal-align tag property.
func (a Align) HTMLProperty() string {
	switch a {
	case AlignLeft:
		return "align=\"left\""
	case AlignCenter:
		return "align=\"center\""
	case AlignJustify:
		return "align=\"justify\""
	case AlignRight:
		return "align=\"right\""
	default:
		return ""
	}
}

// MarkdownProperty returns the equivalent Markdown horizontal-align separator.
func (a Align) MarkdownProperty() string {
	switch a {
	case AlignLeft:
		return ":--- "
	case AlignCenter:
		return ":---:"
	case AlignRight:
		return " ---:"
	default:
		return " --- "
	}
}

func (a Align) justifyText(text string, textLength int, maxLength int) string {
	// split the text into individual words
	wordsUnfiltered := strings.Split(text, " ")
	words := Filter(wordsUnfiltered, func(item string) bool {
		return item != ""
	})
	// empty string implies spaces for maxLength
	if len(words) == 0 {
		return strings.Repeat(" ", maxLength)
	}
	// get the number of spaces to insert into the text
	numSpacesNeeded := maxLength - textLength + strings.Count(text, " ")
	numSpacesNeededBetweenWords := 0
	if len(words) > 1 {
		numSpacesNeededBetweenWords = numSpacesNeeded / (len(words) - 1)
	}
	// create the output string word by word with spaces in between
	var outText strings.Builder
	outText.Grow(maxLength)
	for idx, word := range words {
		if idx > 0 {
			// insert spaces only after the first word
			if idx == len(words)-1 {
				// insert all the remaining space before the last word
				outText.WriteString(strings.Repeat(" ", numSpacesNeeded))
				numSpacesNeeded = 0
			} else {
				// insert the determined number of spaces between each word
				outText.WriteString(strings.Repeat(" ", numSpacesNeededBetweenWords))
				// and reduce the number of spaces needed after this
				numSpacesNeeded -= numSpacesNeededBetweenWords
			}
		}
		outText.WriteString(word)
		if idx == len(words)-1 && numSpacesNeeded > 0 {
			outText.WriteString(strings.Repeat(" ", numSpacesNeeded))
		}
	}
	return outText.String()
}

func (a Align) trimString(text string) string {
	switch a {
	case AlignDefault, AlignLeft:
		if strings.HasSuffix(text, " ") {
			return strings.TrimRight(text, " ")
		}
	case AlignRight:
		if strings.HasPrefix(text, " ") {
			return strings.TrimLeft(text, " ")
		}
	default:
		if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") {
			return strings.Trim(text, " ")
		}
	}
	return text
}