package formatter

import (
	"fmt"
	"regexp"
	"strings"
)

const COLS = 80

type ColorMode uint8

const (
	ColorModeNone ColorMode = iota
	ColorModeTerminal
	ColorModePassthrough
)

var SingletonFormatter = New(ColorModeTerminal)

func F(format string, args ...interface{}) string {
	return SingletonFormatter.F(format, args...)
}

func Fi(indentation uint, format string, args ...interface{}) string {
	return SingletonFormatter.Fi(indentation, format, args...)
}

func Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string {
	return SingletonFormatter.Fiw(indentation, maxWidth, format, args...)
}

type Formatter struct {
	ColorMode                ColorMode
	colors                   map[string]string
	styleRe                  *regexp.Regexp
	preserveColorStylingTags bool
}

func NewWithNoColorBool(noColor bool) Formatter {
	if noColor {
		return New(ColorModeNone)
	}
	return New(ColorModeTerminal)
}

func New(colorMode ColorMode) Formatter {
	f := Formatter{
		ColorMode: colorMode,
		colors: map[string]string{
			"/":         "\x1b[0m",
			"bold":      "\x1b[1m",
			"underline": "\x1b[4m",

			"red":          "\x1b[38;5;9m",
			"orange":       "\x1b[38;5;214m",
			"coral":        "\x1b[38;5;204m",
			"magenta":      "\x1b[38;5;13m",
			"green":        "\x1b[38;5;10m",
			"dark-green":   "\x1b[38;5;28m",
			"yellow":       "\x1b[38;5;11m",
			"light-yellow": "\x1b[38;5;228m",
			"cyan":         "\x1b[38;5;14m",
			"gray":         "\x1b[38;5;243m",
			"light-gray":   "\x1b[38;5;246m",
			"blue":         "\x1b[38;5;12m",
		},
	}
	colors := []string{}
	for color := range f.colors {
		colors = append(colors, color)
	}
	f.styleRe = regexp.MustCompile("{{(" + strings.Join(colors, "|") + ")}}")
	return f
}

func (f Formatter) F(format string, args ...interface{}) string {
	return f.Fi(0, format, args...)
}

func (f Formatter) Fi(indentation uint, format string, args ...interface{}) string {
	return f.Fiw(indentation, 0, format, args...)
}

func (f Formatter) Fiw(indentation uint, maxWidth uint, format string, args ...interface{}) string {
	out := fmt.Sprintf(f.style(format), args...)

	if indentation == 0 && maxWidth == 0 {
		return out
	}

	lines := strings.Split(out, "\n")

	if maxWidth != 0 {
		outLines := []string{}

		maxWidth = maxWidth - indentation*2
		for _, line := range lines {
			if f.length(line) <= maxWidth {
				outLines = append(outLines, line)
				continue
			}
			outWords := []string{}
			length := uint(0)
			words := strings.Split(line, " ")
			for _, word := range words {
				wordLength := f.length(word)
				if length+wordLength <= maxWidth {
					length += wordLength
					outWords = append(outWords, word)
					continue
				}
				outLines = append(outLines, strings.Join(outWords, " "))
				outWords = []string{word}
				length = wordLength
			}
			if len(outWords) > 0 {
				outLines = append(outLines, strings.Join(outWords, " "))
			}
		}

		lines = outLines
	}

	if indentation == 0 {
		return strings.Join(lines, "\n")
	}

	padding := strings.Repeat("  ", int(indentation))
	for i := range lines {
		if lines[i] != "" {
			lines[i] = padding + lines[i]
		}
	}

	return strings.Join(lines, "\n")
}

func (f Formatter) length(styled string) uint {
	n := uint(0)
	inStyle := false
	for _, b := range styled {
		if inStyle {
			if b == 'm' {
				inStyle = false
			}
			continue
		}
		if b == '\x1b' {
			inStyle = true
			continue
		}
		n += 1
	}
	return n
}

func (f Formatter) CycleJoin(elements []string, joiner string, cycle []string) string {
	if len(elements) == 0 {
		return ""
	}
	n := len(cycle)
	out := ""
	for i, text := range elements {
		out += cycle[i%n] + text
		if i < len(elements)-1 {
			out += joiner
		}
	}
	out += "{{/}}"
	return f.style(out)
}

func (f Formatter) style(s string) string {
	switch f.ColorMode {
	case ColorModeNone:
		return f.styleRe.ReplaceAllString(s, "")
	case ColorModePassthrough:
		return s
	case ColorModeTerminal:
		return f.styleRe.ReplaceAllStringFunc(s, func(match string) string {
			if out, ok := f.colors[strings.Trim(match, "{}")]; ok {
				return out
			}
			return match
		})
	}

	return ""
}
