// Package renderer provides functions to convert various data into a cview primitive.
// Example objects include a Gemini response, and an error.
//
// Rendered lines always end with \r\n, in an effort to be Window compatible.
package renderer

import (
	"bytes"
	"fmt"
	urlPkg "net/url"
	"regexp"
	"strconv"
	"strings"

	"code.rocketnine.space/tslocum/cview"
	"github.com/alecthomas/chroma/formatters"
	"github.com/alecthomas/chroma/lexers"
	"github.com/alecthomas/chroma/styles"
	"github.com/makeworld-the-better-one/amfora/config"
	"github.com/spf13/viper"
)

// Terminal color information, set during display initialization by display/display.go
var TermColor string

// Regex for identifying ANSI color codes
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)

// Regex for identifying possible language string, based on RFC 6838 and lexers used by Chroma
var langRegex = regexp.MustCompile(`^([a-zA-Z0-9]+/)?[a-zA-Z0-9]+([a-zA-Z0-9!_\#\$\&\-\^\.\+]+)*`)

// Regex for removing trailing newline (without disturbing ANSI codes) from code formatted with Chroma
var trailingNewline = regexp.MustCompile(`(\r?\n)(?:\x1b\[[0-9;]*m)*$`)

// RenderANSI renders plain text pages containing ANSI codes.
// Practically, it is used for the text/x-ansi.
func RenderANSI(s string) string {
	s = cview.Escape(s)
	if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
		s = cview.TranslateANSI(s)
	} else {
		s = ansiRegex.ReplaceAllString(s, "")
	}
	return s
}

// RenderPlainText should be used to format plain text pages.
func RenderPlainText(s string) string {
	// It used to add a left margin, now this is done elsewhere.
	// The function is kept for convenience and in case rendering
	// is needed in the future.
	return cview.Escape(s)
}

// wrapLine wraps a line to the provided width, and adds the provided prefix and suffix to each wrapped line.
// It recovers from wrapping panics and should never cause a panic.
// It returns a slice of lines, without newlines at the end.
//
// Set includeFirst to true if the prefix and suffix should be applied to the first wrapped line as well
func wrapLine(line string, width int, prefix, suffix string, includeFirst bool) []string {
	if width < 1 {
		width = 1
	}

	// Anonymous function to allow recovery from potential WordWrap panic
	var ret []string
	func() {
		defer func() {
			if r := recover(); r != nil {
				// Use unwrapped line instead
				if includeFirst {
					ret = []string{prefix + line + suffix}
				} else {
					ret = []string{line}
				}
			}
		}()

		wrapped := cview.WordWrap(line, width)
		for i := range wrapped {
			if !includeFirst && i == 0 {
				continue
			}
			wrapped[i] = prefix + wrapped[i] + suffix
		}
		ret = wrapped
	}()
	return ret
}

// convertRegularGemini converts non-preformatted blocks of text/gemini
// into a cview-compatible format.
// Since this only works on non-preformatted blocks, RenderGemini
// should always be used instead.
//
// It also returns a slice of link URLs.
// numLinks is the number of links that exist so far.
// width is the number of columns to wrap to.
//
//
// proxied is whether the request is through the gemini:// scheme.
// If it's not a gemini:// page, set this to true.
func convertRegularGemini(s string, numLinks, width int, proxied bool) (string, []string) {
	links := make([]string, 0)
	lines := strings.Split(s, "\n")
	wrappedLines := make([]string, 0) // Final result

	for i := range lines {
		lines[i] = strings.TrimRight(lines[i], " \r\t\n")

		if strings.HasPrefix(lines[i], "#") {
			// Headings
			var tag string
			if viper.GetBool("a-general.color") {
				if strings.HasPrefix(lines[i], "###") {
					tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_3"))
				} else if strings.HasPrefix(lines[i], "##") {
					tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_2"))
				} else if strings.HasPrefix(lines[i], "#") {
					tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_1"))
				}
				wrappedLines = append(wrappedLines, wrapLine(lines[i], width, tag, "[-::-]", true)...)
			} else {
				// Just bold, no colors
				wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[::b]", "[-::-]", true)...)
			}

			// Links
		} else if strings.HasPrefix(lines[i], "=>") && len([]rune(lines[i])) >= 3 {
			// Trim whitespace and separate link from link text

			lines[i] = strings.Trim(lines[i][2:], " \t") // Remove `=>` part too
			delim := strings.IndexAny(lines[i], " \t")   // Whitespace between link and link text

			var url string
			var linkText string
			if delim == -1 {
				// No link text
				url = lines[i]
				linkText = url
			} else {
				// There is link text
				url = lines[i][:delim]
				linkText = strings.Trim(lines[i][delim:], " \t")
				if viper.GetBool("a-general.show_link") {
					linkText += " (" + url + ")"
				}
			}

			if strings.TrimSpace(lines[i]) == "" || strings.TrimSpace(url) == "" {
				// Link was just whitespace, reset it and move on
				lines[i] = "=>"
				wrappedLines = append(wrappedLines, lines[i])
				continue
			}

			links = append(links, url)
			num := numLinks + len(links) // Visible link number, one-indexed

			var indent int
			if num > 99 {
				// Indent link text by 3 or more spaces
				indent = len(strconv.Itoa(num)) + 4 // +4 indent for spaces and brackets
			} else {
				// One digit and two digit links have the same spacing - see #60
				indent = 5 // +4 indent for spaces and brackets, and 1 for link number
			}

			// Spacing after link number: 1 or 2 spaces?
			var spacing string
			if num > 9 {
				// One space to keep it in line with other links - see #60
				spacing = " "
			} else {
				// One digit numbers use two spaces
				spacing = "  "
			}

			// Underline non-gemini links if enabled
			var linkTag string
			if viper.GetBool("a-general.underline") {
				linkTag = `[` + config.GetColorString("foreign_link") + `::u]`
			} else {
				linkTag = `[` + config.GetColorString("foreign_link") + `]`
			}

			// Wrap and add link text
			// Wrap the link text, but add some spaces to indent the wrapped lines past the link number
			// Set the style tags
			// Add them to the first line

			var wrappedLink []string

			pU, err := urlPkg.Parse(url)
			if !proxied && err == nil &&
				(pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") {
				// A gemini link

				if viper.GetBool("a-general.color") {
					// Add the link text in blue (in a region), and a gray link number to the left of it
					// Those are the default colors, anyway

					wrappedLink = wrapLine(linkText, width-indent,
						strings.Repeat(" ", indent)+
							`["`+strconv.Itoa(num-1)+`"][`+config.GetColorString("amfora_link")+`]`,
						`[-][""]`,
						false, // Don't indent the first line, it's the one with link number
					)

					// Add special stuff to first line, like the link number
					wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) +
						strconv.Itoa(num) + "[]" + "[-::-]" + spacing +
						`["` + strconv.Itoa(num-1) + `"][` + config.GetColorString("amfora_link") + `]` +
						wrappedLink[0] + `[-][""]`
				} else {
					// No color

					wrappedLink = wrapLine(linkText, width-indent,
						strings.Repeat(" ", indent)+ // +4 for spaces and brackets
							`["`+strconv.Itoa(num-1)+`"]`,
						`[""]`,
						false, // Don't indent the first line, it's the one with link number
					)

					wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-]  " +
						`["` + strconv.Itoa(num-1) + `"]` +
						wrappedLink[0] + `[""]`
				}
			} else {
				// Not a gemini link

				if viper.GetBool("a-general.color") {
					// Color

					wrappedLink = wrapLine(linkText, width-indent,
						strings.Repeat(" ", indent)+
							`["`+strconv.Itoa(num-1)+`"]`+linkTag,
						`[-::-][""]`,
						false, // Don't indent the first line, it's the one with link number
					)

					wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) +
						strconv.Itoa(num) + "[][-::-]" + spacing +
						`["` + strconv.Itoa(num-1) + `"]` + linkTag +
						wrappedLink[0] + `[-::-][""]`
				} else {
					// No color

					wrappedLink = wrapLine(linkText, width-indent,
						strings.Repeat(" ", indent)+
							`["`+strconv.Itoa(num-1)+`"]`,
						`[::-][""]`,
						false, // Don't indent the first line, it's the one with link number
					)

					wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-]" + spacing +
						`["` + strconv.Itoa(num-1) + `"]` +
						wrappedLink[0] + `[::-][""]`
				}
			}

			wrappedLines = append(wrappedLines, wrappedLink...)

			// Lists
		} else if strings.HasPrefix(lines[i], "* ") {
			if viper.GetBool("a-general.bullets") {
				// Wrap list item, and indent wrapped lines past the bullet
				wrappedItem := wrapLine(lines[i][1:],
					width-4, // Subtract the 4 indent spaces
					fmt.Sprintf("    [%s]", config.GetColorString("list_text")),
					"[-]", false)
				// Add bullet
				wrappedItem[0] = fmt.Sprintf(" [%s]\u2022", config.GetColorString("list_text")) +
					wrappedItem[0] + "[-]"
				wrappedLines = append(wrappedLines, wrappedItem...)
			} else {
				wrappedItem := wrapLine(lines[i][1:],
					width-4, // Subtract the 4 indent spaces
					fmt.Sprintf("    [%s]", config.GetColorString("list_text")),
					"[-]", false)
				// Add "*"
				wrappedItem[0] = fmt.Sprintf(" [%s]*", config.GetColorString("list_text")) +
					wrappedItem[0] + "[-]"
				wrappedLines = append(wrappedLines, wrappedItem...)

			}
			// Optionally list lines could be colored here too, if color is enabled
		} else if strings.HasPrefix(lines[i], ">") {
			// It's a quote line, add extra quote symbols and italics to the start of each wrapped line

			if len(lines[i]) == 1 {
				// Just an empty quote line
				wrappedLines = append(wrappedLines, fmt.Sprintf("[%s::i]>[-::-]", config.GetColorString("quote_text")))
			} else {
				// Remove beginning quote and maybe space
				lines[i] = strings.TrimPrefix(lines[i], ">")
				lines[i] = strings.TrimPrefix(lines[i], " ")
				wrappedLines = append(wrappedLines,
					wrapLine(lines[i],
						width-2, // Subtract 2 for width of prefix string
						fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")),
						"[-::-]", true)...,
				)
			}

		} else if strings.TrimSpace(lines[i]) == "" {
			// Just add empty line without processing
			wrappedLines = append(wrappedLines, "")
		} else {
			// Regular line, just wrap it
			wrappedLines = append(wrappedLines, wrapLine(lines[i], width,
				fmt.Sprintf("[%s]", config.GetColorString("regular_text")),
				"[-]", true)...)
		}
	}

	return strings.Join(wrappedLines, "\r\n"), links
}

// RenderGemini converts text/gemini into a cview displayable format.
// It also returns a slice of link URLs.
//
// width is the number of columns to wrap to.
// leftMargin is the number of blank spaces to prepend to each line.
//
// proxied is whether the request is through the gemini:// scheme.
// If it's not a gemini:// page, set this to true.
func RenderGemini(s string, width int, proxied bool) (string, []string) {
	s = cview.Escape(s)

	lines := strings.Split(s, "\n")
	links := make([]string, 0)

	// Process and wrap non preformatted lines
	rendered := "" // Final result
	pre := false
	buf := "" // Block of regular or preformatted lines

	// Language, formatter, and style for syntax highlighting
	lang := ""
	formatterName := TermColor
	styleName := viper.GetString("a-general.highlight_style")

	// processPre is for rendering preformatted blocks
	processPre := func() {

		syntaxHighlighted := false

		// Perform syntax highlighting if language is set
		if lang != "" {
			style := styles.Get(styleName)
			if style == nil {
				style = styles.Fallback
			}
			formatter := formatters.Get(formatterName)
			if formatter == nil {
				formatter = formatters.Fallback
			}
			lexer := lexers.Get(lang)
			if lexer == nil {
				lexer = lexers.Fallback
			}

			// Tokenize and format the text after stripping ANSI codes, replacing buffer if there are no errors
			iterator, err := lexer.Tokenise(nil, ansiRegex.ReplaceAllString(buf, ""))
			if err == nil {
				formattedBuffer := new(bytes.Buffer)
				if formatter.Format(formattedBuffer, style, iterator) == nil {
					// Strip extra newline added by Chroma and replace buffer
					buf = string(trailingNewline.ReplaceAll(formattedBuffer.Bytes(), []byte{}))
				}
				syntaxHighlighted = true
			}
		}

		// Support ANSI color codes in preformatted blocks - see #59
		// This will also execute if code highlighting was successful for this block
		if viper.GetBool("a-general.color") && (viper.GetBool("a-general.ansi") || syntaxHighlighted) {
			buf = cview.TranslateANSI(buf)
			// The TranslateANSI function will reset the colors when it encounters
			// an ANSI reset code, injecting a full reset tag: [-:-:-]
			// This uses the default foreground and background colors of the
			// application, but in this case we want it to use the preformatted text
			// color as the foreground, as we're still in a preformat block.
			buf = strings.ReplaceAll(
				buf, "[-:-:-]",
				fmt.Sprintf("[%s:-:-]", config.GetColorString("preformatted_text")),
			)
		} else {
			buf = ansiRegex.ReplaceAllString(buf, "")
		}

		// The final newline is removed (and re-added) to prevent background glitches
		// where the terminal background color slips through. This only happens on
		// preformatted blocks with ANSI characters.
		//
		// Lines are modified below to always end with \r\n
		buf = strings.TrimSuffix(buf, "\r\n")

		if viper.GetBool("a-general.color") {
			rendered += fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) +
				buf + fmt.Sprintf("[%s:%s:-]\r\n", config.GetColorString("regular_text"), config.GetColorString("bg"))
		} else {
			rendered += buf + "\r\n"
		}
	}

	// processRegular processes non-preformatted sections
	processRegular := func() {
		// ANSI not allowed in regular text - see #59
		buf = ansiRegex.ReplaceAllString(buf, "")

		ren, lks := convertRegularGemini(buf, len(links), width, proxied)
		links = append(links, lks...)
		rendered += ren
	}

	for i := range lines {
		if strings.HasPrefix(lines[i], "```") {
			if pre {
				// In a preformatted block, so add the text as is
				// Don't add the current line with backticks
				processPre()

				// Clear the language
				lang = ""
			} else {
				// Not preformatted, regular text
				processRegular()

				if viper.GetBool("a-general.highlight_code") {
					// Check for alt text indicating a language that Chroma can highlight
					alt := strings.TrimSpace(strings.TrimPrefix(lines[i], "```"))
					if matches := langRegex.FindStringSubmatch(alt); matches != nil {
						if lexers.Get(matches[0]) != nil {
							lang = matches[0]
						}
					}
				}
			}
			buf = "" // Clear buffer for next block
			pre = !pre
			continue
		}
		// Lines always end with \r\n for Windows compatibility
		buf += strings.TrimSuffix(lines[i], "\r") + "\r\n"
	}
	// Gone through all the lines, but there still is likely a block in the buffer
	if pre {
		// File ended without closing the preformatted block
		processPre()
	} else {
		// Not preformatted, regular text
		processRegular()
	}

	return rendered, links
}
