package ansi

import (
	"fmt"
	"github.com/rivo/uniseg"
	"strconv"
	"strings"
)

// TextStyle is a type representing the
// ansi text styles
type TextStyle int

const (
	// Bold Style
	Bold TextStyle = 1 << 0
	// Faint Style
	Faint TextStyle = 1 << 1
	// Italic Style
	Italic TextStyle = 1 << 2
	// Blinking Style
	Blinking TextStyle = 1 << 3
	// Inversed Style
	Inversed TextStyle = 1 << 4
	// Invisible Style
	Invisible TextStyle = 1 << 5
	// Underlined Style
	Underlined TextStyle = 1 << 6
	// Strikethrough Style
	Strikethrough TextStyle = 1 << 7
	// Bright Style
	Bright TextStyle = 1 << 8
)

type ColourMode int

const (
	Default    ColourMode = 0
	TwoFiveSix ColourMode = 1
	TrueColour ColourMode = 2
)

var invalid = fmt.Errorf("invalid ansi string")
var missingTerminator = fmt.Errorf("missing escape terminator 'm'")
var invalidTrueColorSequence = fmt.Errorf("invalid TrueColor sequence")
var invalid256ColSequence = fmt.Errorf("invalid 256 colour sequence")

const (
	// Default colors uses foreground color codes [30-37].
	// See ColourMap and case for background colors.
	defaultForegroundColor = "37"
	defaultBackgroundColor = "30"
)

// StyledText represents a single formatted string
type StyledText struct {
	Label      string
	FgCol      *Col
	BgCol      *Col
	Style      TextStyle
	ColourMode ColourMode
	// Offset is the offset into the input string where the StyledText begins
	Offset int
	// Len is the length in bytes of the substring of the input text that
	// contains the styled text
	Len int
}

func (s *StyledText) styleToParams() []string {
	var params []string
	if s.Bold() {
		params = append(params, "1")
	}
	if s.Faint() {
		params = append(params, "2")
	}
	if s.Italic() {
		params = append(params, "3")
	}
	if s.Underlined() {
		params = append(params, "4")
	}
	if s.Blinking() {
		params = append(params, "5")
	}
	if s.Inversed() {
		params = append(params, "7")
	}
	if s.Invisible() {
		params = append(params, "8")
	}
	if s.Strikethrough() {
		params = append(params, "9")
	}
	if s.FgCol != nil {
		// Do we have an ID?
		switch s.ColourMode {
		case Default:
			offset := 30
			id := s.FgCol.Id
			// Adjust when bold has been applied to the id
			if (s.Bold() || s.Bright()) && id > 7 && id < 16 {
				id -= 8
			}
			if s.Bright() {
				offset = 90
			}
			params = append(params, fmt.Sprintf("%d", id+offset))
		case TwoFiveSix:
			params = append(params, []string{"38", "5", fmt.Sprintf("%d", s.FgCol.Id)}...)
		case TrueColour:
			r := fmt.Sprintf("%d", s.FgCol.Rgb.R)
			g := fmt.Sprintf("%d", s.FgCol.Rgb.G)
			b := fmt.Sprintf("%d", s.FgCol.Rgb.B)
			params = append(params, []string{"38", "2", r, g, b}...)
		}
	}
	if s.BgCol != nil {
		// Do we have an ID?
		switch s.ColourMode {
		case Default:
			id := s.BgCol.Id
			offset := 40
			if s.Bright() {
				offset = 100
			}
			// Adjust when bold has been applied to the id
			if (s.Bold() || s.Bright()) && id > 7 && id < 16 {
				id -= 8
			}
			params = append(params, fmt.Sprintf("%d", id+offset))
		case TwoFiveSix:
			params = append(params, []string{"48", "5", fmt.Sprintf("%d", s.BgCol.Id)}...)
		case TrueColour:
			r := fmt.Sprintf("%d", s.BgCol.Rgb.R)
			g := fmt.Sprintf("%d", s.BgCol.Rgb.G)
			b := fmt.Sprintf("%d", s.BgCol.Rgb.B)
			params = append(params, []string{"48", "2", r, g, b}...)
		}
	}
	return params
}

func (s *StyledText) String() string {
	params := strings.Join(s.styleToParams(), ";")
	return "\033[0;" + params + "m" + s.Label + "\033[0m"
}

// Bold will return true if the text has a Bold style
func (s *StyledText) Bold() bool {
	return s.Style&Bold == Bold
}

// Faint will return true if the text has a Faint style
func (s *StyledText) Faint() bool {
	return s.Style&Faint == Faint
}

// Italic will return true if the text has an Italic style
func (s *StyledText) Italic() bool {
	return s.Style&Italic == Italic
}

// Blinking will return true if the text has a Blinking style
func (s *StyledText) Blinking() bool {
	return s.Style&Blinking == Blinking
}

// Inversed will return true if the text has an Inversed style
func (s *StyledText) Inversed() bool {
	return s.Style&Inversed == Inversed
}

// Invisible will return true if the text has an Invisible style
func (s *StyledText) Invisible() bool {
	return s.Style&Invisible == Invisible
}

// Underlined will return true if the text has an Underlined style
func (s *StyledText) Underlined() bool {
	return s.Style&Underlined == Underlined
}

// Strikethrough will return true if the text has a Strikethrough style
func (s *StyledText) Strikethrough() bool {
	return s.Style&Strikethrough == Strikethrough
}

// Bright will return true if the text has a Bright style
func (s *StyledText) Bright() bool {
	return s.Style&Bright == Bright
}

// ColourMap maps ansi identifiers to a colour
var ColourMap = map[string]map[string]*Col{
	"Regular": {
		"30":  Cols[0],
		"31":  Cols[1],
		"32":  Cols[2],
		"33":  Cols[3],
		"34":  Cols[4],
		"35":  Cols[5],
		"36":  Cols[6],
		"37":  Cols[7],
		"90":  Cols[8],
		"91":  Cols[9],
		"92":  Cols[10],
		"93":  Cols[11],
		"94":  Cols[12],
		"95":  Cols[13],
		"96":  Cols[14],
		"97":  Cols[15],
		"100": Cols[8],
		"101": Cols[9],
		"102": Cols[10],
		"103": Cols[11],
		"104": Cols[12],
		"105": Cols[13],
		"106": Cols[14],
		"107": Cols[15],
	},
	"Bold": {
		"30":  Cols[8],
		"31":  Cols[9],
		"32":  Cols[10],
		"33":  Cols[11],
		"34":  Cols[12],
		"35":  Cols[13],
		"36":  Cols[14],
		"37":  Cols[15],
		"90":  Cols[8],
		"91":  Cols[9],
		"92":  Cols[10],
		"93":  Cols[11],
		"94":  Cols[12],
		"95":  Cols[13],
		"96":  Cols[14],
		"97":  Cols[15],
		"100": Cols[8],
		"101": Cols[9],
		"102": Cols[10],
		"103": Cols[11],
		"104": Cols[12],
		"105": Cols[13],
		"106": Cols[14],
		"107": Cols[15],
	},
	"Faint": {
		"30": Cols[0],
		"31": Cols[1],
		"32": Cols[2],
		"33": Cols[3],
		"34": Cols[4],
		"35": Cols[5],
		"36": Cols[6],
		"37": Cols[7],
	},
}

// Parse will convert an ansi encoded string and return
// a slice of StyledText structs that represent the text.
// If parsing is unsuccessful, an error is returned.
func Parse(input string, options ...ParseOption) ([]*StyledText, error) {
	var result []*StyledText
	index := 0
	offset := 0
	escapeCodeLen := 0
	var currentStyledText = &StyledText{}

	if len(input) == 0 {
		return []*StyledText{currentStyledText}, nil
	}

	for {
		// Read all chars to next escape code
		esc := strings.Index(input, "\033[")

		// If no more esc chars, save what's left and return
		if esc == -1 {
			text := input[index:]
			if len(text) > 0 {
				currentStyledText.Label = text
				currentStyledText.Offset = offset
				currentStyledText.Len = len(text) + escapeCodeLen
				result = append(result, currentStyledText)
			}
			return result, nil
		}
		label := input[:esc]
		if len(label) > 0 {
			currentStyledText.Label = label
			currentStyledText.Offset = offset
			currentStyledText.Len = len(label) + escapeCodeLen
			offset += currentStyledText.Len
			result = append(result, currentStyledText)
			currentStyledText = &StyledText{
				Label: "",
				FgCol: currentStyledText.FgCol,
				BgCol: currentStyledText.BgCol,
				Style: currentStyledText.Style,
			}
			escapeCodeLen = 0
		}
		input = input[esc:]
		// skip
		input = input[2:]

		// Read in params
		endesc := strings.Index(input, "m")
		if endesc == -1 {
			return nil, missingTerminator
		}
		paramText := input[:endesc]
		input = input[endesc+1:]
		escapeCodeLen += 2 + endesc + 1
		params := strings.Split(paramText, ";")
		colourMap := ColourMap["Regular"]
		skip := 0
		for index, param := range params {
			if skip > 0 {
				skip--
				continue
			}
			switch param {
			case "0", "":
				colourMap = ColourMap["Regular"]
				currentStyledText.Style = 0
				currentStyledText.FgCol = nil
				currentStyledText.BgCol = nil
			case "1":
				// Bold
				colourMap = ColourMap["Bold"]
				currentStyledText.Style |= Bold
			case "2":
				// Dim/Feint
				colourMap = ColourMap["Faint"]
				currentStyledText.Style |= Faint
			case "3":
				// Italic
				currentStyledText.Style |= Italic
			case "4":
				// Underlined
				currentStyledText.Style |= Underlined
			case "5":
				// Blinking
				currentStyledText.Style |= Blinking
			case "7":
				// Inversed
				currentStyledText.Style |= Inversed
			case "8":
				// Invisible
				currentStyledText.Style |= Invisible
			case "9":
				// Strikethrough
				currentStyledText.Style |= Strikethrough
			case "30", "31", "32", "33", "34", "35", "36", "37":
				currentStyledText.FgCol = colourMap[param]
			case "90", "91", "92", "93", "94", "95", "96", "97":
				currentStyledText.FgCol = colourMap[param]
				currentStyledText.Style |= Bright
			case "100", "101", "102", "103", "104", "105", "106", "107":
				currentStyledText.BgCol = colourMap[param]
				currentStyledText.Style |= Bright
			case "40", "41", "42", "43", "44", "45", "46", "47":
				bgcol := "3" + param[1:] // Equivalent of -10
				currentStyledText.BgCol = colourMap[bgcol]
			case "38", "48":
				if len(params)-index < 3 {
					return nil, invalid
				}
				// 256 colours
				if params[index+1] == "5" {
					skip = 2
					colIndexText := params[index+2]
					colIndex, err := strconv.Atoi(colIndexText)
					if err != nil {
						return nil, invalid256ColSequence
					}
					if colIndex < 0 || colIndex > 255 {
						return nil, invalid256ColSequence
					}
					currentStyledText.ColourMode = TwoFiveSix
					if param == "38" {
						currentStyledText.FgCol = Cols[colIndex]
						continue
					}
					currentStyledText.BgCol = Cols[colIndex]
					continue
				}
				// we must have 4 params left
				if len(params)-index < 5 {
					return nil, invalidTrueColorSequence
				}
				if params[index+1] != "2" {
					return nil, invalidTrueColorSequence
				}
				var r, g, b uint8
				ri, err := strconv.Atoi(params[index+2])
				if err != nil {
					return nil, invalidTrueColorSequence
				}
				gi, err := strconv.Atoi(params[index+3])
				if err != nil {
					return nil, invalidTrueColorSequence
				}
				bi, err := strconv.Atoi(params[index+4])
				if err != nil {
					return nil, invalidTrueColorSequence
				}
				if bi > 255 || gi > 255 || ri > 255 {
					return nil, invalidTrueColorSequence
				}
				if bi < 0 || gi < 0 || ri < 0 {
					return nil, invalidTrueColorSequence
				}
				r = uint8(ri)
				g = uint8(gi)
				b = uint8(bi)
				skip = 4
				colvalue := fmt.Sprintf("#%02x%02x%02x", r, g, b)
				currentStyledText.ColourMode = TrueColour
				if param == "38" {
					currentStyledText.FgCol = &Col{Id: 256, Hex: colvalue, Rgb: Rgb{r, g, b}}
					continue
				}
				currentStyledText.BgCol = &Col{Id: 256, Hex: colvalue, Rgb: Rgb{r, g, b}}
			case "39":
				// Lookup for default foreground color.
				foregroundColor := colourMap[defaultForegroundColor]
				for _, option := range options {
					if option.ansiForegroundColor != "" {
						foregroundColor = colourMap[option.ansiForegroundColor]
						break
					}
				}

				// Set selected foreground color.
				currentStyledText.FgCol = foregroundColor
			case "49":
				// Lookup for default background color.
				backgroundColor := colourMap[defaultBackgroundColor]
				for _, option := range options {
					if option.ansiBackgroundColor != "" {
						backgroundColor = colourMap[option.ansiBackgroundColor]
						break
					}
				}

				// Set selected background color.
				currentStyledText.BgCol = backgroundColor
			default:
				// Unexpected codes may be ignored.
				unexpectedCodeIgnored := false
				for _, option := range options {
					if option.ignoreUnexpectedCode {
						unexpectedCodeIgnored = true
						break
					}
				}
				if !unexpectedCodeIgnored {
					return nil, invalid
				}
			}
		}
	}
}

// HasEscapeCodes tests that input has escape codes.
func HasEscapeCodes(input string) bool {
	return strings.IndexAny(input, "\033[") != -1
}

// String builds an ANSI string for specified StyledText slice.
func String(input []*StyledText) string {
	var result strings.Builder
	for _, text := range input {
		params := text.styleToParams()
		if len(params) == 0 {
			result.WriteString(text.Label)
			continue
		}
		result.WriteString(text.String())
	}
	return result.String()
}

// Truncate truncates text to length but preserves control symbols in ANSI string.
func Truncate(input string, maxChars int, options ...ParseOption) (string, error) {
	parsed, err := Parse(input, options...)
	if err != nil {
		return "", err
	}
	charsLeft := maxChars
	var result []*StyledText
	for _, element := range parsed {
		userPerceivedChars := uniseg.GraphemeClusterCount(element.Label)
		if userPerceivedChars >= charsLeft {
			var newLabel []rune
			graphemes := uniseg.NewGraphemes(element.Label)
			for graphemes.Next() {
				newLabel = append(newLabel, graphemes.Runes()...)
				charsLeft--
				if charsLeft == 0 {
					element.Label = string(newLabel)
					result = append(result, element)
					return String(result), nil
				}
			}
		}
		result = append(result, element)
		charsLeft -= userPerceivedChars
	}
	return String(result), nil
}

// Cleanse removes ANSI control symbols from the string.
func Cleanse(input string, options ...ParseOption) (string, error) {
	if input == "" {
		return "", nil
	}
	parsed, err := Parse(input, options...)
	if err != nil {
		return "", err
	}
	var result strings.Builder
	for _, element := range parsed {
		result.WriteString(element.Label)
	}
	return result.String(), nil
}

// Length calculates count of user-perceived characters in ANSI string.
func Length(input string, options ...ParseOption) (int, error) {
	if input == "" {
		return 0, nil
	}
	parsed, err := Parse(input, options...)
	if err != nil {
		return -1, err
	}
	var result int
	for _, element := range parsed {
		userPerceivedChars := uniseg.GraphemeClusterCount(element.Label)
		result += userPerceivedChars
	}
	return result, nil
}
