package vt

import (
	"fmt"
	"os"
	"strconv"
	"strings"

	"github.com/xyproto/env/v2"
)

const (
	cursorHome         = "\033[H"
	cursorHomeTemplate = "\033[%d;%dH"
	cursorUp           = "\033[A"
	cursorDown         = "\033[B"
	cursorForward      = "\033[C"
	cursorBackward     = "\033[D"
	saveCursor         = "\033[s"
	restoreCursor      = "\033[u"
	saveCursorAttrs    = "\033[7"
	restoreCursorAttrs = "\033[8"
	resetDevice        = "\033c"
	eraseScreen        = "\033[2J"
	eraseEndOfLine     = "\033[K"
	eraseStartOfLine   = "\033[1K"
	eraseLine          = "\033[2K"
	eraseDown          = "\033[J"
	eraseUp            = "\033[1J"
	enableLineWrap     = "\033[?7h"
	disableLineWrap    = "\033[?7l"
	showCursor         = "\033[?25h"
	hideCursor         = "\033[?25l"
	echoOff            = "\033[12h"
	attributeTemplate  = "\033[%sm"
	beginSyncUpdate    = "\033[?2026h"
	endSyncUpdate      = "\033[?2026l"
)

// NoColor is the escape sequence for resetting all color attributes
const NoColor string = "\033[0m"

// Stop returns the escape sequence for resetting all color attributes,
// or "" when NO_COLOR is set.
func Stop() string {
	return envResetSeq
}

// writeAllToStdout writes the given byte slice to stdout, retrying on partial writes
func writeAllToStdout(data []byte) bool {
	for len(data) > 0 {
		n, err := os.Stdout.Write(data)
		if err != nil || n <= 0 {
			return false
		}
		data = data[n:]
	}
	return true
}

// SetXY moves the cursor to the given position (0,0 is top left)
func SetXY(x, y uint) {
	fmt.Printf(cursorHomeTemplate, y+1, x+1)
}

// Home moves the cursor to the top-left corner
func Home() {
	fmt.Print(cursorHome)
}

// Reset sends the terminal reset sequence
func Reset() {
	fmt.Print(resetDevice)
}

// Clear erases the entire screen
func Clear() {
	fmt.Print(eraseScreen)
}

// SetNoColor resets all color attributes
func SetNoColor() {
	fmt.Print(NoColor)
}

// underTMUX is true if running inside TMUX
var underTMUX = env.Has("TMUX")

// underScreen is true if running inside GNU Screen
var underScreen = env.Has("STY")

// underDvtm is true if running inside dvtm
var underDvtm = env.Has("DVTM")

// underAbduco is true if running inside abduco
var underAbduco = env.Has("ABDUCO")

// multiplexed is true when running inside any known terminal multiplexer
var multiplexed = underTMUX || underScreen || underDvtm || underAbduco

// xtermLike is true when $TERM looks like an xterm-class emulator
var xtermLike = strings.HasPrefix(env.Str("TERM"), "xterm")

// safeReset is true when it is safe to send \033c (RIS) and \033[12h (SRM).
// These are skipped under multiplexers, on the Linux console, and on
// non-xterm consoles where the behaviour is undefined or destructive.
var safeReset = xtermLike && !multiplexed

// Multiplexed returns true when running inside a terminal multiplexer
func Multiplexed() bool {
	return multiplexed
}

// xtermLike returns true when $TERM looks like an xterm-class emulator
func XtermLike() bool {
	return xtermLike
}

// HasAltArrows returns true when the terminal is expected to emit
// alt-arrow key sequences (ESC [1;3A/B/C/D for alt-up/down/right/left).
// These are only generated by xterm-class emulators.
func HasAltArrows() bool {
	return xtermLike
}

// HasCtrlArrows returns true when the terminal is expected to emit
// ctrl-arrow key sequences (ESC [1;5A/B/C/D for ctrl-up/down/right/left).
// These are only generated by xterm-class emulators.
func HasCtrlArrows() bool {
	return xtermLike
}

// HasShiftArrows returns true when the terminal is expected to emit
// shift-arrow and shift-home/end key sequences
// (ESC [1;2A/B/C/D/H/F for shift-up/down/right/left/home/end).
// These are only generated by xterm-class emulators.
func HasShiftArrows() bool {
	return xtermLike
}

// Has256Colors returns true when the terminal supports 256-color mode
// (i.e. $TERM contains "256color" or is "xterm-kitty").
func Has256Colors() bool {
	term := env.Str("TERM")
	return strings.Contains(term, "256color") || term == "xterm-kitty"
}

// Init initializes the terminal for full-screen canvas use
func Init() {
	initTerminal()
	if safeReset {
		Reset()   // \033c (RIS): only safe on xterm-class terminals outside multiplexers
		EchoOff() // \033[12h (SRM): ditto
	}
	Clear()
	ShowCursor(false)
	SetLineWrap(false)
}

// Close restores the terminal and clears the screen.
// Use CloseKeepContent to keep the canvas content visible.
func Close() {
	SetLineWrap(true)
	ShowCursor(true)
	Clear()
	Home()
}

// CloseKeepContent restores the terminal but leaves the canvas content visible
func CloseKeepContent() {
	SetLineWrap(true)
	ShowCursor(true)
	Home()
}

// EchoOff disables terminal echo
func EchoOff() {
	if echoOffHelper() {
		fmt.Print(echoOff)
	}
}

// SetLineWrap enables or disables line wrapping
func SetLineWrap(enable bool) {
	if enable {
		fmt.Print(enableLineWrap)
	} else {
		fmt.Print(disableLineWrap)
	}
}

// ShowCursor shows or hides the terminal cursor
func ShowCursor(enable bool) {
	showCursorHelper(enable)
	if enable {
		fmt.Print(showCursor)
	} else {
		fmt.Print(hideCursor)
	}
}

// GetBackgroundColor queries the terminal for its background color.
// Returns normalized RGB values in [0.0, 1.0], or an error.
// The terminal response format is "rgb:RRRR/GGGG/BBBB" where each component
// is a 16-bit hex value (0000–ffff).
func GetBackgroundColor(tty *TTY) (float64, float64, float64, error) {
	// First try the escape code used by e.g. alacritty
	if err := tty.WriteString("\033]11;?\a"); err != nil {
		return 0, 0, 0, err
	}
	result, err := tty.ReadString()
	if err != nil {
		return 0, 0, 0, err
	}
	if !strings.Contains(result, "rgb:") {
		// Then try the escape code used by e.g. gnome-terminal
		if err := tty.WriteString("\033]10;?\a\033]11;?\a"); err != nil {
			return 0, 0, 0, err
		}
		result, err = tty.ReadString()
		if err != nil {
			return 0, 0, 0, err
		}
	}
	if _, after, ok := strings.Cut(result, "rgb:"); ok {
		// Trim anything after the three hex components (e.g. trailing BEL, ST, newline)
		rgb := strings.FieldsFunc(after, func(r rune) bool {
			return r < 0x20 || r == '\033' || r == '\a' || r == '\\'
		})
		if len(rgb) > 0 {
			parts := strings.SplitN(rgb[0], "/", 3)
			if len(parts) == 3 {
				rv, err1 := strconv.ParseUint(strings.TrimSpace(parts[0]), 16, 16)
				gv, err2 := strconv.ParseUint(strings.TrimSpace(parts[1]), 16, 16)
				bv, err3 := strconv.ParseUint(strings.TrimSpace(parts[2]), 16, 16)
				if err1 == nil && err2 == nil && err3 == nil {
					return float64(rv) / 65535.0, float64(gv) / 65535.0, float64(bv) / 65535.0, nil
				}
			}
		}
	}

	return 0, 0, 0, fmt.Errorf("could not read rgb value from terminal emulator, got: %q", result)
}

// BeginSyncUpdate sends the terminal's begin synchronized update escape sequence
func BeginSyncUpdate() {
	fmt.Print(beginSyncUpdate)
}

// EndSyncUpdate sends the terminal's end synchronized update escape sequence
func EndSyncUpdate() {
	fmt.Print(endSyncUpdate)
}
