package tview

import (
	"bytes"
	"fmt"
	"io"
	"strconv"
	"strings"
)

// The states of the ANSI escape code parser.
const (
	ansiText = iota
	ansiEscape
	ansiSubstring
	ansiControlSequence
)

// ansi is a io.Writer which translates ANSI escape codes into tview color
// tags.
type ansi struct {
	io.Writer

	// Reusable buffers.
	buffer                        *bytes.Buffer // The entire output text of one Write().
	csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings.
	attributes                    string        // The buffer's current text attributes (a tview attribute string).

	// The current state of the parser. One of the ansi constants.
	state int
}

// ANSIWriter returns an io.Writer which translates any ANSI escape codes
// written to it into tview style tags. Other escape codes don't have an effect
// and are simply removed. The translated text is written to the provided
// writer.
func ANSIWriter(writer io.Writer) io.Writer {
	return &ansi{
		Writer:          writer,
		buffer:          new(bytes.Buffer),
		csiParameter:    new(bytes.Buffer),
		csiIntermediate: new(bytes.Buffer),
		state:           ansiText,
	}
}

// Write parses the given text as a string of runes, translates ANSI escape
// codes to style tags and writes them to the output writer.
func (a *ansi) Write(text []byte) (int, error) {
	defer func() {
		a.buffer.Reset()
	}()

	for _, r := range string(text) {
		switch a.state {

		// We just entered an escape sequence.
		case ansiEscape:
			switch r {
			case '[': // Control Sequence Introducer.
				a.csiParameter.Reset()
				a.csiIntermediate.Reset()
				a.state = ansiControlSequence
			case 'c': // Reset.
				fmt.Fprint(a.buffer, "[-:-:-]")
				a.state = ansiText
			case 'P', ']', 'X', '^', '_': // Substrings and commands.
				a.state = ansiSubstring
			default: // Ignore.
				a.state = ansiText
			}

		// CSI Sequences.
		case ansiControlSequence:
			switch {
			case r >= 0x30 && r <= 0x3f: // Parameter bytes.
				if _, err := a.csiParameter.WriteRune(r); err != nil {
					return 0, err
				}
			case r >= 0x20 && r <= 0x2f: // Intermediate bytes.
				if _, err := a.csiIntermediate.WriteRune(r); err != nil {
					return 0, err
				}
			case r >= 0x40 && r <= 0x7e: // Final byte.
				switch r {
				case 'E': // Next line.
					count, _ := strconv.Atoi(a.csiParameter.String())
					if count == 0 {
						count = 1
					}
					fmt.Fprint(a.buffer, strings.Repeat("\n", count))
				case 'm': // Select Graphic Rendition.
					var background, foreground string
					params := a.csiParameter.String()
					fields := strings.Split(params, ";")
					if len(params) == 0 || fields[0] == "" || fields[0] == "0" {
						// Reset.
						a.attributes = ""
						if _, err := a.buffer.WriteString("[-:-:-]"); err != nil {
							return 0, err
						}
						break
					}
					lookupColor := func(colorNumber int) string {
						if colorNumber < 0 || colorNumber > 15 {
							return "black"
						}
						return []string{
							"black",
							"maroon",
							"green",
							"olive",
							"navy",
							"purple",
							"teal",
							"silver",
							"gray",
							"red",
							"lime",
							"yellow",
							"blue",
							"fuchsia",
							"aqua",
							"white",
						}[colorNumber]
					}
				FieldLoop:
					for index, field := range fields {
						switch field {
						case "1", "01":
							if !strings.ContainsRune(a.attributes, 'b') {
								a.attributes += "b"
							}
						case "2", "02":
							if !strings.ContainsRune(a.attributes, 'd') {
								a.attributes += "d"
							}
						case "3", "03":
							if !strings.ContainsRune(a.attributes, 'i') {
								a.attributes += "i"
							}
						case "4", "04":
							if !strings.ContainsRune(a.attributes, 'u') {
								a.attributes += "u"
							}
						case "5", "05":
							if !strings.ContainsRune(a.attributes, 'l') {
								a.attributes += "l"
							}
						case "7", "07":
							if !strings.ContainsRune(a.attributes, 'r') {
								a.attributes += "r"
							}
						case "9", "09":
							if !strings.ContainsRune(a.attributes, 's') {
								a.attributes += "s"
							}
						case "22":
							if i := strings.IndexRune(a.attributes, 'b'); i >= 0 {
								a.attributes = a.attributes[:i] + a.attributes[i+1:]
							}
							if i := strings.IndexRune(a.attributes, 'd'); i >= 0 {
								a.attributes = a.attributes[:i] + a.attributes[i+1:]
							}
						case "23":
							if i := strings.IndexRune(a.attributes, 'i'); i >= 0 {
								a.attributes = a.attributes[:i] + a.attributes[i+1:]
							}
						case "24":
							if i := strings.IndexRune(a.attributes, 'u'); i >= 0 {
								a.attributes = a.attributes[:i] + a.attributes[i+1:]
							}
						case "25":
							if i := strings.IndexRune(a.attributes, 'l'); i >= 0 {
								a.attributes = a.attributes[:i] + a.attributes[i+1:]
							}
						case "27":
							if i := strings.IndexRune(a.attributes, 'r'); i >= 0 {
								a.attributes = a.attributes[:i] + a.attributes[i+1:]
							}
						case "29":
							if i := strings.IndexRune(a.attributes, 's'); i >= 0 {
								a.attributes = a.attributes[:i] + a.attributes[i+1:]
							}
						case "30", "31", "32", "33", "34", "35", "36", "37":
							colorNumber, _ := strconv.Atoi(field)
							foreground = lookupColor(colorNumber - 30)
						case "39":
							foreground = "-"
						case "40", "41", "42", "43", "44", "45", "46", "47":
							colorNumber, _ := strconv.Atoi(field)
							background = lookupColor(colorNumber - 40)
						case "49":
							background = "-"
						case "90", "91", "92", "93", "94", "95", "96", "97":
							colorNumber, _ := strconv.Atoi(field)
							foreground = lookupColor(colorNumber - 82)
						case "100", "101", "102", "103", "104", "105", "106", "107":
							colorNumber, _ := strconv.Atoi(field)
							background = lookupColor(colorNumber - 92)
						case "38", "48":
							var color string
							if len(fields) > index+1 {
								if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors.
									colorNumber, _ := strconv.Atoi(fields[index+2])
									if colorNumber <= 15 {
										color = lookupColor(colorNumber)
									} else if colorNumber <= 231 {
										red := (colorNumber - 16) / 36
										green := ((colorNumber - 16) / 6) % 6
										blue := (colorNumber - 16) % 6
										color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5)
									} else if colorNumber <= 255 {
										grey := 255 * (colorNumber - 232) / 23
										color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey)
									}
								} else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors.
									red, _ := strconv.Atoi(fields[index+2])
									green, _ := strconv.Atoi(fields[index+3])
									blue, _ := strconv.Atoi(fields[index+4])
									color = fmt.Sprintf("#%02x%02x%02x", red, green, blue)
								}
							}
							if len(color) > 0 {
								if field == "38" {
									foreground = color
								} else {
									background = color
								}
							}
							break FieldLoop
						}
					}
					var colon string
					if len(a.attributes) > 0 {
						colon = ":"
					}
					if len(foreground) > 0 || len(background) > 0 || len(a.attributes) > 0 {
						fmt.Fprintf(a.buffer, "[%s:%s%s%s]", foreground, background, colon, a.attributes)
					}
				}
				a.state = ansiText
			default: // Undefined byte.
				a.state = ansiText // Abort CSI.
			}

			// We just entered a substring/command sequence.
		case ansiSubstring:
			if r == 27 { // Most likely the end of the substring.
				a.state = ansiEscape
			} // Ignore all other characters.

			// "ansiText" and all others.
		default:
			if r == 27 {
				// This is the start of an escape sequence.
				a.state = ansiEscape
			} else {
				// Just a regular rune. Send to buffer.
				if _, err := a.buffer.WriteRune(r); err != nil {
					return 0, err
				}
			}
		}
	}

	// Write buffer to target writer.
	n, err := a.buffer.WriteTo(a.Writer)
	if err != nil {
		return int(n), err
	}
	return len(text), nil
}

// TranslateANSI replaces ANSI escape sequences found in the provided string
// with tview's style tags and returns the resulting string.
func TranslateANSI(text string) string {
	var buffer bytes.Buffer
	writer := ANSIWriter(&buffer)
	writer.Write([]byte(text))
	return buffer.String()
}
