// Copyright 2025 The TCell Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use file except in compliance with the License.
// You may obtain a copy of the license at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package terminfo

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
)

var (
	// ErrTermNotFound indicates that a suitable terminal entry could
	// not be found.  This can result from either not having TERM set,
	// or from the TERM failing to support certain minimal functionality,
	// in particular absolute cursor addressability (the cup capability)
	// is required.  For example, legacy "adm3" lacks this capability,
	// whereas the slightly newer "adm3a" supports it.  This failure
	// occurs most often with "dumb".
	ErrTermNotFound = errors.New("terminal entry not found")
)

// Terminfo represents a terminfo entry.  Note that we use friendly names
// in Go, but when we write out JSON, we use the same names as terminfo.
// The name, aliases and smous, rmous fields do not come from terminfo directly.
type Terminfo struct {
	Name        string
	Aliases     []string
	Columns     int    // cols
	Lines       int    // lines
	Colors      int    // colors
	Clear       string // clear
	EnterCA     string // smcup
	ExitCA      string // rmcup
	ShowCursor  string // cnorm
	HideCursor  string // civis
	AttrOff     string // sgr0
	Underline   string // smul
	Bold        string // bold
	Blink       string // blink
	Reverse     string // rev
	Dim         string // dim
	Italic      string // sitm
	EnterKeypad string // smkx
	ExitKeypad  string // rmkx
	SetFg       string // setaf
	SetBg       string // setab
	ResetFgBg   string // op
	SetCursor   string // cup
	PadChar     string // pad
	Mouse       string // kmous
	AltChars    string // acsc
	EnterAcs    string // smacs
	ExitAcs     string // rmacs
	EnableAcs   string // enacs

	// These are non-standard extensions to terminfo.  This includes
	// true color support, and some additional keys.  Its kind of bizarre
	// that shifted variants of left and right exist, but not up and down.
	// Terminal support for these are going to vary amongst XTerm
	// emulations, so don't depend too much on them in your application.

	StrikeThrough     string // smxx
	SetFgBg           string // setfgbg
	SetFgBgRGB        string // setfgbgrgb
	SetFgRGB          string // setfrgb
	SetBgRGB          string // setbrgb
	InsertChar        string // string to insert a character (ich1)
	AutoMargin        bool   // true if writing to last cell in line advances
	TrueColor         bool   // true if the terminal supports direct color
	DisableAutoMargin string // smam
	EnableAutoMargin  string // rmam
	XTermLike         bool   // (XT) has XTerm extensions
}

type stack []any

func (st stack) Push(v any) stack {
	if b, ok := v.(bool); ok {
		if b {
			return append(st, 1)
		} else {
			return append(st, 0)
		}
	}
	return append(st, v)
}

func (st stack) PopString() (string, stack) {
	if len(st) > 0 {
		e := st[len(st)-1]
		var s string
		switch v := e.(type) {
		case int:
			s = strconv.Itoa(v)
		case string:
			s = v
		}
		return s, st[:len(st)-1]
	}
	return "", st

}
func (st stack) PopInt() (int, stack) {
	if len(st) > 0 {
		e := st[len(st)-1]
		var i int
		switch v := e.(type) {
		case int:
			i = v
		case string:
			i, _ = strconv.Atoi(v)
		}
		return i, st[:len(st)-1]
	}
	return 0, st
}

// static vars
var svars [26]string

type paramsBuffer struct {
	out bytes.Buffer
	buf bytes.Buffer
}

// Start initializes the params buffer with the initial string data.
// It also locks the paramsBuffer.  The caller must call End() when
// finished.
func (pb *paramsBuffer) Start(s string) {
	pb.out.Reset()
	pb.buf.Reset()
	pb.buf.WriteString(s)
}

// End returns the final output from TParam, but it also releases the lock.
func (pb *paramsBuffer) End() string {
	s := pb.out.String()
	return s
}

// NextCh returns the next input character to the expander.
func (pb *paramsBuffer) NextCh() (byte, error) {
	return pb.buf.ReadByte()
}

// PutCh "emits" (rather schedules for output) a single byte character.
func (pb *paramsBuffer) PutCh(ch byte) {
	pb.out.WriteByte(ch)
}

// PutString schedules a string for output.
func (pb *paramsBuffer) PutString(s string) {
	pb.out.WriteString(s)
}

// TParm takes a terminfo parameterized string, such as setaf or cup, and
// evaluates the string, and returns the result with the parameter
// applied.
func (t *Terminfo) TParm(s string, p ...any) string {
	var stk stack
	var a string
	var ai, bi int
	var dvars [26]string
	var params [9]any
	var pb = &paramsBuffer{}

	pb.Start(s)

	// make sure we always have 9 parameters -- makes it easier
	// later to skip checks
	for i := 0; i < len(params) && i < len(p); i++ {
		params[i] = p[i]
	}

	const (
		emit = iota
		toEnd
		toElse
	)

	skip := emit

	for {

		ch, err := pb.NextCh()
		if err != nil {
			break
		}

		if ch != '%' {
			if skip == emit {
				pb.PutCh(ch)
			}
			continue
		}

		ch, err = pb.NextCh()
		if err != nil {
			// XXX Error
			break
		}
		if skip == toEnd {
			if ch == ';' {
				skip = emit
			}
			continue
		} else if skip == toElse {
			if ch == 'e' || ch == ';' {
				skip = emit
			}
			continue
		}

		switch ch {
		case '%': // quoted %
			pb.PutCh(ch)

		case 'i': // increment both parameters (ANSI cup support)
			if i, ok := params[0].(int); ok {
				params[0] = i + 1
			}
			if i, ok := params[1].(int); ok {
				params[1] = i + 1
			}

		case 's':
			// NB: 's', 'c', and 'd' below are special cased for
			// efficiency.  They could be handled by the richer
			// format support below, less efficiently.
			a, stk = stk.PopString()
			pb.PutString(a)

		case 'c':
			// Integer as special character.
			ai, stk = stk.PopInt()
			pb.PutCh(byte(ai))

		case 'd':
			ai, stk = stk.PopInt()
			pb.PutString(strconv.Itoa(ai))

		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'x', 'X', 'o', ':':
			// This is pretty suboptimal, but this is rarely used.
			// None of the mainstream terminals use any of this,
			// and it would surprise me if this code is ever
			// executed outside test cases.
			f := "%"
			if ch == ':' {
				ch, _ = pb.NextCh()
			}
			f += string(ch)
			for ch == '+' || ch == '-' || ch == '#' || ch == ' ' {
				ch, _ = pb.NextCh()
				f += string(ch)
			}
			for (ch >= '0' && ch <= '9') || ch == '.' {
				ch, _ = pb.NextCh()
				f += string(ch)
			}
			switch ch {
			case 'd', 'x', 'X', 'o':
				ai, stk = stk.PopInt()
				pb.PutString(fmt.Sprintf(f, ai))
			case 's':
				a, stk = stk.PopString()
				pb.PutString(fmt.Sprintf(f, a))
			case 'c':
				ai, stk = stk.PopInt()
				pb.PutString(fmt.Sprintf(f, ai))
			}

		case 'p': // push parameter
			ch, _ = pb.NextCh()
			ai = int(ch - '1')
			if ai >= 0 && ai < len(params) {
				stk = stk.Push(params[ai])
			} else {
				stk = stk.Push(0)
			}

		case 'P': // pop & store variable
			ch, _ = pb.NextCh()
			if ch >= 'A' && ch <= 'Z' {
				svars[int(ch-'A')], stk = stk.PopString()
			} else if ch >= 'a' && ch <= 'z' {
				dvars[int(ch-'a')], stk = stk.PopString()
			}

		case 'g': // recall & push variable
			ch, _ = pb.NextCh()
			if ch >= 'A' && ch <= 'Z' {
				stk = stk.Push(svars[int(ch-'A')])
			} else if ch >= 'a' && ch <= 'z' {
				stk = stk.Push(dvars[int(ch-'a')])
			}

		case '\'': // push(char) - the integer value of it
			ch, _ = pb.NextCh()
			_, _ = pb.NextCh() // must be ' but we don't check
			stk = stk.Push(int(ch))

		case '{': // push(int)
			ai = 0
			ch, _ = pb.NextCh()
			for ch >= '0' && ch <= '9' {
				ai *= 10
				ai += int(ch - '0')
				ch, _ = pb.NextCh()
			}
			// ch must be '}' but no verification
			stk = stk.Push(ai)

		case 'l': // push(strlen(pop))
			a, stk = stk.PopString()
			stk = stk.Push(len(a))

		case '+':
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai + bi)

		case '-':
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai - bi)

		case '*':
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai * bi)

		case '/':
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			if bi != 0 {
				stk = stk.Push(ai / bi)
			} else {
				stk = stk.Push(0)
			}

		case 'm': // push(pop mod pop)
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			if bi != 0 {
				stk = stk.Push(ai % bi)
			} else {
				stk = stk.Push(0)
			}

		case '&': // AND
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai & bi)

		case '|': // OR
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai | bi)

		case '^': // XOR
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai ^ bi)

		case '~': // bit complement
			ai, stk = stk.PopInt()
			stk = stk.Push(ai ^ -1)

		case '!': // logical NOT
			ai, stk = stk.PopInt()
			stk = stk.Push(ai == 0)

		case '=': // numeric compare
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai == bi)

		case '>': // greater than, numeric
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai > bi)

		case '<': // less than, numeric
			bi, stk = stk.PopInt()
			ai, stk = stk.PopInt()
			stk = stk.Push(ai < bi)

		case '?': // start conditional

		case ';':
			skip = emit

		case 't':
			ai, stk = stk.PopInt()
			if ai == 0 {
				skip = toElse
			}

		case 'e':
			skip = toEnd

		default:
			pb.PutString("%" + string(ch))
		}
	}

	return pb.End()
}

// TPuts emits the string to the writer, but expands inline padding
// indications (of the form $<[delay]> where [delay] is msec) to
// a suitable time (unless the terminfo string indicates this isn't needed
// by specifying npc - no padding).  All Terminfo based strings should be
// emitted using this function.
func (t *Terminfo) TPuts(w io.Writer, s string) {
	for {
		beg := strings.Index(s, "$<")
		if beg < 0 {
			// Most strings don't need padding, which is good news!
			_, _ = io.WriteString(w, s)
			return
		}
		_, _ = io.WriteString(w, s[:beg])
		s = s[beg+2:]
		end := strings.Index(s, ">")
		if end < 0 {
			// unterminated.. just emit bytes unadulterated
			_, _ = io.WriteString(w, "$<"+s)
			return
		}
		val := s[:end]
		s = s[end+1:]
		padus := 0
		unit := time.Millisecond
		dot := false
	loop:
		for i := range val {
			switch val[i] {
			case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
				padus *= 10
				padus += int(val[i] - '0')
				if dot {
					unit /= 10
				}
			case '.':
				if !dot {
					dot = true
				} else {
					break loop
				}
			default:
				break loop
			}
		}

		// Curses historically uses padding to achieve "fine grained"
		// delays. We have much better clocks these days, and so we
		// do not rely on padding but simply sleep a bit.
		if len(t.PadChar) > 0 {
			time.Sleep(unit * time.Duration(padus))
		}
	}
}

// TGoto returns a string suitable for addressing the cursor at the given
// row and column.  The origin 0, 0 is in the upper left corner of the screen.
func (t *Terminfo) TGoto(col, row int) string {
	return t.TParm(t.SetCursor, row, col)
}

// TColor returns a string corresponding to the given foreground and background
// colors.  Either fg or bg can be set to -1 to elide.
func (t *Terminfo) TColor(fi, bi int) string {
	rv := ""
	// As a special case, we map bright colors to lower versions if the
	// color table only holds 8.  For the remaining 240 colors, the user
	// is out of luck.  Someday we could create a mapping table, but its
	// not worth it.
	if t.Colors == 8 {
		if fi > 7 && fi < 16 {
			fi -= 8
		}
		if bi > 7 && bi < 16 {
			bi -= 8
		}
	}
	if t.Colors > fi && fi >= 0 {
		rv += t.TParm(t.SetFg, fi)
	}
	if t.Colors > bi && bi >= 0 {
		rv += t.TParm(t.SetBg, bi)
	}
	return rv
}

var (
	dblock    sync.Mutex
	terminfos = make(map[string]*Terminfo)
)

// AddTerminfo can be called to register a new Terminfo entry.
func AddTerminfo(t *Terminfo) {
	dblock.Lock()

	terminfos[t.Name] = t
	for _, x := range t.Aliases {
		terminfos[x] = t
	}
	dblock.Unlock()
}

// LookupTerminfo attempts to find a definition for the named $TERM.
func LookupTerminfo(name string) (*Terminfo, error) {
	if name == "" {
		// else on windows: index out of bounds
		// on the name[0] reference below
		return nil, ErrTermNotFound
	}

	addtruecolor := false
	add256color := false
	switch os.Getenv("COLORTERM") {
	case "truecolor", "24bit", "24-bit":
		addtruecolor = true
	}
	dblock.Lock()
	t := terminfos[name]
	dblock.Unlock()

	// If the name ends in -truecolor, then fabricate an entry
	// from the corresponding -256color, -color, or bare terminal.
	if t != nil && t.TrueColor {
		addtruecolor = true
	} else if t == nil && strings.HasSuffix(name, "-truecolor") {

		suffixes := []string{
			"-256color",
			"-88color",
			"-color",
			"",
		}
		base := name[:len(name)-len("-truecolor")]
		for _, s := range suffixes {
			if t, _ = LookupTerminfo(base + s); t != nil {
				addtruecolor = true
				break
			}
		}
	}

	// If the name ends in -256color, maybe fabricate using the xterm 256 color sequences
	if t == nil && strings.HasSuffix(name, "-256color") {
		suffixes := []string{
			"-88color",
			"-color",
		}
		base := name[:len(name)-len("-256color")]
		for _, s := range suffixes {
			if t, _ = LookupTerminfo(base + s); t != nil {
				add256color = true
				break
			}
		}
	}

	if t == nil {
		return nil, ErrTermNotFound
	}

	switch os.Getenv("TCELL_TRUECOLOR") {
	case "":
	case "disable":
		addtruecolor = false
	default:
		addtruecolor = true
	}

	// If the user has requested 24-bit color with $COLORTERM, then
	// amend the value (unless already present).  This means we don't
	// need to have a value present.
	if addtruecolor &&
		t.SetFgBgRGB == "" &&
		t.SetFgRGB == "" &&
		t.SetBgRGB == "" {

		// Supply vanilla ISO 8613-6:1994 24-bit color sequences.
		t.SetFgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%dm"
		t.SetBgRGB = "\x1b[48;2;%p1%d;%p2%d;%p3%dm"
		t.SetFgBgRGB = "\x1b[38;2;%p1%d;%p2%d;%p3%d;" +
			"48;2;%p4%d;%p5%d;%p6%dm"
	}

	if add256color {
		t.Colors = 256
		t.SetFg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m"
		t.SetBg = "\x1b[%?%p1%{8}%<%t4%p1%d%e%p1%{16}%<%t10%p1%{8}%-%d%e48;5;%p1%d%;m"
		t.SetFgBg = "\x1b[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;;%?%p2%{8}%<%t4%p2%d%e%p2%{16}%<%t10%p2%{8}%-%d%e48;5;%p2%d%;m"
		t.ResetFgBg = "\x1b[39;49m"
	}

	return t, nil
}

func TerminfoNames() []string {
	res := make([]string, 0, len(terminfos))
	for m := range terminfos {
		res = append(res, m)
	}
	return res
}
