// Copyright 2024 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.

//go:build !(js && wasm)
// +build !js !wasm

package tcell

import (
	"bytes"
	"encoding/base64"
	"errors"
	"io"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
	"unicode/utf8"

	"golang.org/x/term"
	"golang.org/x/text/transform"

	"github.com/gdamore/tcell/v2/terminfo"
)

// NewTerminfoScreen returns a Screen that uses the stock TTY interface
// and POSIX terminal control, combined with a terminfo description taken from
// the $TERM environment variable.  It returns an error if the terminal
// is not supported for any reason.
//
// For terminals that do not support dynamic resize events, the $LINES
// $COLUMNS environment variables can be set to the actual window size,
// otherwise defaults taken from the terminal database are used.
func NewTerminfoScreen() (Screen, error) {
	return NewTerminfoScreenFromTty(nil)
}

// LookupTerminfo attempts to find a definition for the named $TERM falling
// back to attempting to parse the output from infocmp.
func LookupTerminfo(name string) (ti *terminfo.Terminfo, e error) {
	ti, e = terminfo.LookupTerminfo(name)
	if e != nil {
		ti, e = loadDynamicTerminfo(name)
		if e != nil {
			return nil, e
		}
		terminfo.AddTerminfo(ti)
	}

	return
}

// NewTerminfoScreenFromTtyTerminfo returns a Screen using a custom Tty
// implementation  and custom terminfo specification.
// If the passed in tty is nil, then a reasonable default (typically /dev/tty)
// is presumed, at least on UNIX hosts. (Windows hosts will typically fail this
// call altogether.)
// If passed terminfo is nil, then TERM environment variable is queried for
// terminal specification.
func NewTerminfoScreenFromTtyTerminfo(tty Tty, ti *terminfo.Terminfo) (s Screen, e error) {
	if ti == nil {
		ti, e = LookupTerminfo(os.Getenv("TERM"))
		if e != nil {
			return
		}
	}

	t := &tScreen{ti: ti, tty: tty}

	t.keyexist = make(map[Key]bool)
	t.keycodes = make(map[string]*tKeyCode)
	if len(ti.Mouse) > 0 {
		t.mouse = []byte(ti.Mouse)
	}
	t.prepareKeys()
	t.buildAcsMap()
	t.resizeQ = make(chan bool, 1)
	t.fallback = make(map[rune]string)
	for k, v := range RuneFallbacks {
		t.fallback[k] = v
	}

	return &baseScreen{screenImpl: t}, nil
}

// NewTerminfoScreenFromTty returns a Screen using a custom Tty implementation.
// If the passed in tty is nil, then a reasonable default (typically /dev/tty)
// is presumed, at least on UNIX hosts. (Windows hosts will typically fail this
// call altogether.)
func NewTerminfoScreenFromTty(tty Tty) (Screen, error) {
	return NewTerminfoScreenFromTtyTerminfo(tty, nil)
}

// tKeyCode represents a combination of a key code and modifiers.
type tKeyCode struct {
	key Key
	mod ModMask
}

// tScreen represents a screen backed by a terminfo implementation.
type tScreen struct {
	ti           *terminfo.Terminfo
	tty          Tty
	h            int
	w            int
	fini         bool
	cells        CellBuffer
	buffering    bool // true if we are collecting writes to buf instead of sending directly to out
	buf          bytes.Buffer
	curstyle     Style
	style        Style
	resizeQ      chan bool
	quit         chan struct{}
	keyexist     map[Key]bool
	keycodes     map[string]*tKeyCode
	keychan      chan []byte
	keytimer     *time.Timer
	keyexpire    time.Time
	cx           int
	cy           int
	mouse        []byte
	clear        bool
	cursorx      int
	cursory      int
	acs          map[rune]string
	charset      string
	encoder      transform.Transformer
	decoder      transform.Transformer
	fallback     map[rune]string
	colors       map[Color]Color
	palette      []Color
	truecolor    bool
	escaped      bool
	buttondn     bool
	finiOnce     sync.Once
	enablePaste  string
	disablePaste string
	enterUrl     string
	exitUrl      string
	setWinSize   string
	enableFocus  string
	disableFocus string
	doubleUnder  string
	curlyUnder   string
	dottedUnder  string
	dashedUnder  string
	underColor   string
	underRGB     string
	underFg      string
	cursorStyles map[CursorStyle]string
	cursorStyle  CursorStyle
	cursorColor  Color
	cursorRGB    string
	cursorFg     string
	saved        *term.State
	stopQ        chan struct{}
	eventQ       chan Event
	running      bool
	wg           sync.WaitGroup
	mouseFlags   MouseFlags
	pasteEnabled bool
	focusEnabled bool
	setTitle     string
	saveTitle    string
	restoreTitle string
	title        string
	setClipboard string

	sync.Mutex
}

func (t *tScreen) Init() error {
	if e := t.initialize(); e != nil {
		return e
	}

	t.keychan = make(chan []byte, 10)
	t.keytimer = time.NewTimer(time.Millisecond * 50)
	t.charset = "UTF-8"

	t.charset = getCharset()
	if enc := GetEncoding(t.charset); enc != nil {
		t.encoder = enc.NewEncoder()
		t.decoder = enc.NewDecoder()
	} else {
		return ErrNoCharset
	}
	ti := t.ti

	// environment overrides
	w := ti.Columns
	h := ti.Lines
	if i, _ := strconv.Atoi(os.Getenv("LINES")); i != 0 {
		h = i
	}
	if i, _ := strconv.Atoi(os.Getenv("COLUMNS")); i != 0 {
		w = i
	}
	if t.ti.SetFgBgRGB != "" || t.ti.SetFgRGB != "" || t.ti.SetBgRGB != "" {
		t.truecolor = true
	}
	// A user who wants to have his themes honored can
	// set this environment variable.
	if os.Getenv("TCELL_TRUECOLOR") == "disable" {
		t.truecolor = false
	}
	nColors := t.nColors()
	if nColors > 256 {
		nColors = 256 // clip to reasonable limits
	}
	t.colors = make(map[Color]Color, nColors)
	t.palette = make([]Color, nColors)
	for i := 0; i < nColors; i++ {
		t.palette[i] = Color(i) | ColorValid
		// identity map for our builtin colors
		t.colors[Color(i)|ColorValid] = Color(i) | ColorValid
	}

	t.quit = make(chan struct{})
	t.eventQ = make(chan Event, 10)

	t.Lock()
	t.cx = -1
	t.cy = -1
	t.style = StyleDefault
	t.cells.Resize(w, h)
	t.cursorx = -1
	t.cursory = -1
	t.resize()
	t.Unlock()

	if err := t.engage(); err != nil {
		return err
	}

	return nil
}

func (t *tScreen) prepareKeyMod(key Key, mod ModMask, val string) {
	if val != "" {
		// Do not override codes that already exist
		if _, exist := t.keycodes[val]; !exist {
			t.keyexist[key] = true
			t.keycodes[val] = &tKeyCode{key: key, mod: mod}
		}
	}
}

func (t *tScreen) prepareKeyModReplace(key Key, replace Key, mod ModMask, val string) {
	if val != "" {
		// Do not override codes that already exist
		if old, exist := t.keycodes[val]; !exist || old.key == replace {
			t.keyexist[key] = true
			t.keycodes[val] = &tKeyCode{key: key, mod: mod}
		}
	}
}

func (t *tScreen) prepareKeyModXTerm(key Key, val string) {

	if strings.HasPrefix(val, "\x1b[") && strings.HasSuffix(val, "~") {

		// Drop the trailing ~
		val = val[:len(val)-1]

		// These suffixes are calculated assuming Xterm style modifier suffixes.
		// Please see https://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf for
		// more information (specifically "PC-Style Function Keys").
		t.prepareKeyModReplace(key, key+12, ModShift, val+";2~")
		t.prepareKeyModReplace(key, key+48, ModAlt, val+";3~")
		t.prepareKeyModReplace(key, key+60, ModAlt|ModShift, val+";4~")
		t.prepareKeyModReplace(key, key+24, ModCtrl, val+";5~")
		t.prepareKeyModReplace(key, key+36, ModCtrl|ModShift, val+";6~")
		t.prepareKeyMod(key, ModAlt|ModCtrl, val+";7~")
		t.prepareKeyMod(key, ModShift|ModAlt|ModCtrl, val+";8~")
		t.prepareKeyMod(key, ModMeta, val+";9~")
		t.prepareKeyMod(key, ModMeta|ModShift, val+";10~")
		t.prepareKeyMod(key, ModMeta|ModAlt, val+";11~")
		t.prepareKeyMod(key, ModMeta|ModAlt|ModShift, val+";12~")
		t.prepareKeyMod(key, ModMeta|ModCtrl, val+";13~")
		t.prepareKeyMod(key, ModMeta|ModCtrl|ModShift, val+";14~")
		t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt, val+";15~")
		t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt|ModShift, val+";16~")
	} else if strings.HasPrefix(val, "\x1bO") && len(val) == 3 {
		val = val[2:]
		t.prepareKeyModReplace(key, key+12, ModShift, "\x1b[1;2"+val)
		t.prepareKeyModReplace(key, key+48, ModAlt, "\x1b[1;3"+val)
		t.prepareKeyModReplace(key, key+24, ModCtrl, "\x1b[1;5"+val)
		t.prepareKeyModReplace(key, key+36, ModCtrl|ModShift, "\x1b[1;6"+val)
		t.prepareKeyModReplace(key, key+60, ModAlt|ModShift, "\x1b[1;4"+val)
		t.prepareKeyMod(key, ModAlt|ModCtrl, "\x1b[1;7"+val)
		t.prepareKeyMod(key, ModShift|ModAlt|ModCtrl, "\x1b[1;8"+val)
		t.prepareKeyMod(key, ModMeta, "\x1b[1;9"+val)
		t.prepareKeyMod(key, ModMeta|ModShift, "\x1b[1;10"+val)
		t.prepareKeyMod(key, ModMeta|ModAlt, "\x1b[1;11"+val)
		t.prepareKeyMod(key, ModMeta|ModAlt|ModShift, "\x1b[1;12"+val)
		t.prepareKeyMod(key, ModMeta|ModCtrl, "\x1b[1;13"+val)
		t.prepareKeyMod(key, ModMeta|ModCtrl|ModShift, "\x1b[1;14"+val)
		t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt, "\x1b[1;15"+val)
		t.prepareKeyMod(key, ModMeta|ModCtrl|ModAlt|ModShift, "\x1b[1;16"+val)
	}
}

func (t *tScreen) prepareXtermModifiers() {
	if t.ti.Modifiers != terminfo.ModifiersXTerm {
		return
	}
	t.prepareKeyModXTerm(KeyRight, t.ti.KeyRight)
	t.prepareKeyModXTerm(KeyLeft, t.ti.KeyLeft)
	t.prepareKeyModXTerm(KeyUp, t.ti.KeyUp)
	t.prepareKeyModXTerm(KeyDown, t.ti.KeyDown)
	t.prepareKeyModXTerm(KeyInsert, t.ti.KeyInsert)
	t.prepareKeyModXTerm(KeyDelete, t.ti.KeyDelete)
	t.prepareKeyModXTerm(KeyPgUp, t.ti.KeyPgUp)
	t.prepareKeyModXTerm(KeyPgDn, t.ti.KeyPgDn)
	t.prepareKeyModXTerm(KeyHome, t.ti.KeyHome)
	t.prepareKeyModXTerm(KeyEnd, t.ti.KeyEnd)
	t.prepareKeyModXTerm(KeyF1, t.ti.KeyF1)
	t.prepareKeyModXTerm(KeyF2, t.ti.KeyF2)
	t.prepareKeyModXTerm(KeyF3, t.ti.KeyF3)
	t.prepareKeyModXTerm(KeyF4, t.ti.KeyF4)
	t.prepareKeyModXTerm(KeyF5, t.ti.KeyF5)
	t.prepareKeyModXTerm(KeyF6, t.ti.KeyF6)
	t.prepareKeyModXTerm(KeyF7, t.ti.KeyF7)
	t.prepareKeyModXTerm(KeyF8, t.ti.KeyF8)
	t.prepareKeyModXTerm(KeyF9, t.ti.KeyF9)
	t.prepareKeyModXTerm(KeyF10, t.ti.KeyF10)
	t.prepareKeyModXTerm(KeyF11, t.ti.KeyF11)
	t.prepareKeyModXTerm(KeyF12, t.ti.KeyF12)
}

func (t *tScreen) prepareBracketedPaste() {
	// Another workaround for lack of reporting in terminfo.
	// We assume if the terminal has a mouse entry, that it
	// offers bracketed paste.  But we allow specific overrides
	// via our terminal database.
	if t.ti.EnablePaste != "" {
		t.enablePaste = t.ti.EnablePaste
		t.disablePaste = t.ti.DisablePaste
		t.prepareKey(keyPasteStart, t.ti.PasteStart)
		t.prepareKey(keyPasteEnd, t.ti.PasteEnd)
	} else if t.ti.Mouse != "" || t.ti.XTermLike {
		t.enablePaste = "\x1b[?2004h"
		t.disablePaste = "\x1b[?2004l"
		t.prepareKey(keyPasteStart, "\x1b[200~")
		t.prepareKey(keyPasteEnd, "\x1b[201~")
	}
}

func (t *tScreen) prepareUnderlines() {
	if t.ti.DoubleUnderline != "" {
		t.doubleUnder = t.ti.DoubleUnderline
	} else if t.ti.XTermLike {
		t.doubleUnder = "\x1b[4:2m"
	}
	if t.ti.CurlyUnderline != "" {
		t.curlyUnder = t.ti.CurlyUnderline
	} else if t.ti.XTermLike {
		t.curlyUnder = "\x1b[4:3m"
	}
	if t.ti.DottedUnderline != "" {
		t.dottedUnder = t.ti.DottedUnderline
	} else if t.ti.XTermLike {
		t.dottedUnder = "\x1b[4:4m"
	}
	if t.ti.DashedUnderline != "" {
		t.dashedUnder = t.ti.DashedUnderline
	} else if t.ti.XTermLike {
		t.dashedUnder = "\x1b[4:5m"
	}

	// Underline colors.  We're not going to rely upon terminfo for this
	// Essentially all terminals that support the curly underlines are
	// expected to also support coloring them too - which reflects actual
	// practice since these were introduced at about the same time.
	if t.ti.UnderlineColor != "" {
		t.underColor = t.ti.UnderlineColor
	} else if t.curlyUnder != "" {
		t.underColor = "\x1b[58:5:%p1%dm"
	}
	if t.ti.UnderlineColorRGB != "" {
		// An interesting wart here is that in order to facilitate
		// using just a single parameter, the Setulc parameter takes
		// the 24-bit color as an integer rather than separate bytes.
		// This matches the "new" style direct color approach that
		// ncurses took, even though everyone else went another way.
		t.underRGB = t.ti.UnderlineColorRGB
	} else if t.underColor != "" {
		t.underRGB = "\x1b[58:2::%p1%d:%p2%d:%p3%dm"
	}
	if t.ti.UnderlineColorReset != "" {
		t.underFg = t.ti.UnderlineColorReset
	} else if t.curlyUnder != "" {
		t.underFg = "\x1b[59m"
	}
}

func (t *tScreen) prepareExtendedOSC() {
	// Linux is a special beast - because it has a mouse entry, but does
	// not swallow these OSC commands properly.
	if strings.Contains(t.ti.Name, "linux") {
		return
	}
	// More stuff for limits in terminfo.  This time we are applying
	// the most common OSC (operating system commands).  Generally
	// terminals that don't understand these will ignore them.
	// Again, we condition this based on mouse capabilities.
	if t.ti.EnterUrl != "" {
		t.enterUrl = t.ti.EnterUrl
		t.exitUrl = t.ti.ExitUrl
	} else if t.ti.Mouse != "" || t.ti.XTermLike {
		t.enterUrl = "\x1b]8;%p2%s;%p1%s\x1b\\"
		t.exitUrl = "\x1b]8;;\x1b\\"
	}

	if t.ti.SetWindowSize != "" {
		t.setWinSize = t.ti.SetWindowSize
	} else if t.ti.Mouse != "" || t.ti.XTermLike {
		t.setWinSize = "\x1b[8;%p1%p2%d;%dt"
	}

	if t.ti.EnableFocusReporting != "" {
		t.enableFocus = t.ti.EnableFocusReporting
	} else if t.ti.Mouse != "" || t.ti.XTermLike {
		t.enableFocus = "\x1b[?1004h"
	}
	if t.ti.DisableFocusReporting != "" {
		t.disableFocus = t.ti.DisableFocusReporting
	} else if t.ti.Mouse != "" || t.ti.XTermLike {
		t.disableFocus = "\x1b[?1004l"
	}

	if t.ti.SetWindowTitle != "" {
		t.setTitle = t.ti.SetWindowTitle
	} else if t.ti.XTermLike {
		t.saveTitle = "\x1b[22;2t"
		t.restoreTitle = "\x1b[23;2t"
		// this also tries to request that UTF-8 is allowed in the title
		t.setTitle = "\x1b[>2t\x1b]2;%p1%s\x1b\\"
	}

	if t.setClipboard == "" && t.ti.XTermLike {
		// this string takes a base64 string and sends it to the clipboard.
		// it will also be able to retrieve the clipboard using "?" as the
		// sent string, when we support that.
		t.setClipboard = "\x1b]52;c;%p1%s\x1b\\"
	}
}

func (t *tScreen) prepareCursorStyles() {
	// Another workaround for lack of reporting in terminfo.
	// We assume if the terminal has a mouse entry, that it
	// offers bracketed paste.  But we allow specific overrides
	// via our terminal database.
	if t.ti.CursorDefault != "" {
		t.cursorStyles = map[CursorStyle]string{
			CursorStyleDefault:           t.ti.CursorDefault,
			CursorStyleBlinkingBlock:     t.ti.CursorBlinkingBlock,
			CursorStyleSteadyBlock:       t.ti.CursorSteadyBlock,
			CursorStyleBlinkingUnderline: t.ti.CursorBlinkingUnderline,
			CursorStyleSteadyUnderline:   t.ti.CursorSteadyUnderline,
			CursorStyleBlinkingBar:       t.ti.CursorBlinkingBar,
			CursorStyleSteadyBar:         t.ti.CursorSteadyBar,
		}
	} else if t.ti.Mouse != "" || t.ti.XTermLike {
		t.cursorStyles = map[CursorStyle]string{
			CursorStyleDefault:           "\x1b[0 q",
			CursorStyleBlinkingBlock:     "\x1b[1 q",
			CursorStyleSteadyBlock:       "\x1b[2 q",
			CursorStyleBlinkingUnderline: "\x1b[3 q",
			CursorStyleSteadyUnderline:   "\x1b[4 q",
			CursorStyleBlinkingBar:       "\x1b[5 q",
			CursorStyleSteadyBar:         "\x1b[6 q",
		}
	}
	if t.ti.CursorColorRGB != "" {
		// if it was X11 style with just a single %p1%s, then convert
		t.cursorRGB = t.ti.CursorColorRGB
	}
	if t.ti.CursorColorReset != "" {
		t.cursorFg = t.ti.CursorColorReset
	}
	if t.cursorRGB == "" {
		t.cursorRGB = "\x1b]12;%p1%s\007"
		t.cursorFg = "\x1b]112\007"
	}

	// convert XTERM style color names to RGB color code.  We have no way to do palette colors
	t.cursorRGB = strings.Replace(t.cursorRGB, "%p1%s", "#%p1%02x%p2%02x%p3%02x", 1)
}

func (t *tScreen) prepareKey(key Key, val string) {
	t.prepareKeyMod(key, ModNone, val)
}

func (t *tScreen) prepareKeys() {
	ti := t.ti
	if strings.HasPrefix(ti.Name, "xterm") {
		// assume its some form of XTerm clone
		t.ti.XTermLike = true
		ti.XTermLike = true
	}
	t.prepareKey(KeyBackspace, ti.KeyBackspace)
	t.prepareKey(KeyF1, ti.KeyF1)
	t.prepareKey(KeyF2, ti.KeyF2)
	t.prepareKey(KeyF3, ti.KeyF3)
	t.prepareKey(KeyF4, ti.KeyF4)
	t.prepareKey(KeyF5, ti.KeyF5)
	t.prepareKey(KeyF6, ti.KeyF6)
	t.prepareKey(KeyF7, ti.KeyF7)
	t.prepareKey(KeyF8, ti.KeyF8)
	t.prepareKey(KeyF9, ti.KeyF9)
	t.prepareKey(KeyF10, ti.KeyF10)
	t.prepareKey(KeyF11, ti.KeyF11)
	t.prepareKey(KeyF12, ti.KeyF12)
	t.prepareKey(KeyF13, ti.KeyF13)
	t.prepareKey(KeyF14, ti.KeyF14)
	t.prepareKey(KeyF15, ti.KeyF15)
	t.prepareKey(KeyF16, ti.KeyF16)
	t.prepareKey(KeyF17, ti.KeyF17)
	t.prepareKey(KeyF18, ti.KeyF18)
	t.prepareKey(KeyF19, ti.KeyF19)
	t.prepareKey(KeyF20, ti.KeyF20)
	t.prepareKey(KeyF21, ti.KeyF21)
	t.prepareKey(KeyF22, ti.KeyF22)
	t.prepareKey(KeyF23, ti.KeyF23)
	t.prepareKey(KeyF24, ti.KeyF24)
	t.prepareKey(KeyF25, ti.KeyF25)
	t.prepareKey(KeyF26, ti.KeyF26)
	t.prepareKey(KeyF27, ti.KeyF27)
	t.prepareKey(KeyF28, ti.KeyF28)
	t.prepareKey(KeyF29, ti.KeyF29)
	t.prepareKey(KeyF30, ti.KeyF30)
	t.prepareKey(KeyF31, ti.KeyF31)
	t.prepareKey(KeyF32, ti.KeyF32)
	t.prepareKey(KeyF33, ti.KeyF33)
	t.prepareKey(KeyF34, ti.KeyF34)
	t.prepareKey(KeyF35, ti.KeyF35)
	t.prepareKey(KeyF36, ti.KeyF36)
	t.prepareKey(KeyF37, ti.KeyF37)
	t.prepareKey(KeyF38, ti.KeyF38)
	t.prepareKey(KeyF39, ti.KeyF39)
	t.prepareKey(KeyF40, ti.KeyF40)
	t.prepareKey(KeyF41, ti.KeyF41)
	t.prepareKey(KeyF42, ti.KeyF42)
	t.prepareKey(KeyF43, ti.KeyF43)
	t.prepareKey(KeyF44, ti.KeyF44)
	t.prepareKey(KeyF45, ti.KeyF45)
	t.prepareKey(KeyF46, ti.KeyF46)
	t.prepareKey(KeyF47, ti.KeyF47)
	t.prepareKey(KeyF48, ti.KeyF48)
	t.prepareKey(KeyF49, ti.KeyF49)
	t.prepareKey(KeyF50, ti.KeyF50)
	t.prepareKey(KeyF51, ti.KeyF51)
	t.prepareKey(KeyF52, ti.KeyF52)
	t.prepareKey(KeyF53, ti.KeyF53)
	t.prepareKey(KeyF54, ti.KeyF54)
	t.prepareKey(KeyF55, ti.KeyF55)
	t.prepareKey(KeyF56, ti.KeyF56)
	t.prepareKey(KeyF57, ti.KeyF57)
	t.prepareKey(KeyF58, ti.KeyF58)
	t.prepareKey(KeyF59, ti.KeyF59)
	t.prepareKey(KeyF60, ti.KeyF60)
	t.prepareKey(KeyF61, ti.KeyF61)
	t.prepareKey(KeyF62, ti.KeyF62)
	t.prepareKey(KeyF63, ti.KeyF63)
	t.prepareKey(KeyF64, ti.KeyF64)
	t.prepareKey(KeyInsert, ti.KeyInsert)
	t.prepareKey(KeyDelete, ti.KeyDelete)
	t.prepareKey(KeyHome, ti.KeyHome)
	t.prepareKey(KeyEnd, ti.KeyEnd)
	t.prepareKey(KeyUp, ti.KeyUp)
	t.prepareKey(KeyDown, ti.KeyDown)
	t.prepareKey(KeyLeft, ti.KeyLeft)
	t.prepareKey(KeyRight, ti.KeyRight)
	t.prepareKey(KeyPgUp, ti.KeyPgUp)
	t.prepareKey(KeyPgDn, ti.KeyPgDn)
	t.prepareKey(KeyHelp, ti.KeyHelp)
	t.prepareKey(KeyPrint, ti.KeyPrint)
	t.prepareKey(KeyCancel, ti.KeyCancel)
	t.prepareKey(KeyExit, ti.KeyExit)
	t.prepareKey(KeyBacktab, ti.KeyBacktab)

	t.prepareKeyMod(KeyRight, ModShift, ti.KeyShfRight)
	t.prepareKeyMod(KeyLeft, ModShift, ti.KeyShfLeft)
	t.prepareKeyMod(KeyUp, ModShift, ti.KeyShfUp)
	t.prepareKeyMod(KeyDown, ModShift, ti.KeyShfDown)
	t.prepareKeyMod(KeyHome, ModShift, ti.KeyShfHome)
	t.prepareKeyMod(KeyEnd, ModShift, ti.KeyShfEnd)
	t.prepareKeyMod(KeyPgUp, ModShift, ti.KeyShfPgUp)
	t.prepareKeyMod(KeyPgDn, ModShift, ti.KeyShfPgDn)

	t.prepareKeyMod(KeyRight, ModCtrl, ti.KeyCtrlRight)
	t.prepareKeyMod(KeyLeft, ModCtrl, ti.KeyCtrlLeft)
	t.prepareKeyMod(KeyUp, ModCtrl, ti.KeyCtrlUp)
	t.prepareKeyMod(KeyDown, ModCtrl, ti.KeyCtrlDown)
	t.prepareKeyMod(KeyHome, ModCtrl, ti.KeyCtrlHome)
	t.prepareKeyMod(KeyEnd, ModCtrl, ti.KeyCtrlEnd)

	// Sadly, xterm handling of keycodes is somewhat erratic.  In
	// particular, different codes are sent depending on application
	// mode is in use or not, and the entries for many of these are
	// simply absent from terminfo on many systems.  So we insert
	// a number of escape sequences if they are not already used, in
	// order to have the widest correct usage.  Note that prepareKey
	// will not inject codes if the escape sequence is already known.
	// We also only do this for terminals that have the application
	// mode present.

	// Cursor mode
	if ti.EnterKeypad != "" {
		t.prepareKey(KeyUp, "\x1b[A")
		t.prepareKey(KeyDown, "\x1b[B")
		t.prepareKey(KeyRight, "\x1b[C")
		t.prepareKey(KeyLeft, "\x1b[D")
		t.prepareKey(KeyEnd, "\x1b[F")
		t.prepareKey(KeyHome, "\x1b[H")
		t.prepareKey(KeyDelete, "\x1b[3~")
		t.prepareKey(KeyHome, "\x1b[1~")
		t.prepareKey(KeyEnd, "\x1b[4~")
		t.prepareKey(KeyPgUp, "\x1b[5~")
		t.prepareKey(KeyPgDn, "\x1b[6~")

		// Application mode
		t.prepareKey(KeyUp, "\x1bOA")
		t.prepareKey(KeyDown, "\x1bOB")
		t.prepareKey(KeyRight, "\x1bOC")
		t.prepareKey(KeyLeft, "\x1bOD")
		t.prepareKey(KeyHome, "\x1bOH")
	}

	t.prepareKey(keyPasteStart, ti.PasteStart)
	t.prepareKey(keyPasteEnd, ti.PasteEnd)
	t.prepareXtermModifiers()
	t.prepareBracketedPaste()
	t.prepareCursorStyles()
	t.prepareUnderlines()
	t.prepareExtendedOSC()

outer:
	// Add key mappings for control keys.
	for i := 0; i < ' '; i++ {
		// Do not insert direct key codes for ambiguous keys.
		// For example, ESC is used for lots of other keys, so
		// when parsing this we don't want to fast path handling
		// of it, but instead wait a bit before parsing it as in
		// isolation.
		for esc := range t.keycodes {
			if []byte(esc)[0] == byte(i) {
				continue outer
			}
		}

		t.keyexist[Key(i)] = true

		mod := ModCtrl
		switch Key(i) {
		case KeyBS, KeyTAB, KeyESC, KeyCR:
			// directly type-able- no control sequence
			mod = ModNone
		}
		t.keycodes[string(rune(i))] = &tKeyCode{key: Key(i), mod: mod}
	}
}

func (t *tScreen) Fini() {
	t.finiOnce.Do(t.finish)
}

func (t *tScreen) finish() {
	close(t.quit)
	t.finalize()
}

func (t *tScreen) SetStyle(style Style) {
	t.Lock()
	if !t.fini {
		t.style = style
	}
	t.Unlock()
}

func (t *tScreen) encodeRune(r rune, buf []byte) []byte {

	nb := make([]byte, 6)
	ob := make([]byte, 6)
	num := utf8.EncodeRune(ob, r)
	ob = ob[:num]
	dst := 0
	var err error
	if enc := t.encoder; enc != nil {
		enc.Reset()
		dst, _, err = enc.Transform(nb, ob, true)
	}
	if err != nil || dst == 0 || nb[0] == '\x1a' {
		// Combining characters are elided
		if len(buf) == 0 {
			if acs, ok := t.acs[r]; ok {
				buf = append(buf, []byte(acs)...)
			} else if fb, ok := t.fallback[r]; ok {
				buf = append(buf, []byte(fb)...)
			} else {
				buf = append(buf, '?')
			}
		}
	} else {
		buf = append(buf, nb[:dst]...)
	}

	return buf
}

func (t *tScreen) sendFgBg(fg Color, bg Color, attr AttrMask) AttrMask {
	ti := t.ti
	if ti.Colors == 0 {
		// foreground vs background, we calculate luminance
		// and possibly do a reverse video
		if !fg.Valid() {
			return attr
		}
		v, ok := t.colors[fg]
		if !ok {
			v = FindColor(fg, []Color{ColorBlack, ColorWhite})
			t.colors[fg] = v
		}
		switch v {
		case ColorWhite:
			return attr
		case ColorBlack:
			return attr ^ AttrReverse
		}
	}

	if fg == ColorReset || bg == ColorReset {
		t.TPuts(ti.ResetFgBg)
	}
	if t.truecolor {
		if ti.SetFgBgRGB != "" && fg.IsRGB() && bg.IsRGB() {
			r1, g1, b1 := fg.RGB()
			r2, g2, b2 := bg.RGB()
			t.TPuts(ti.TParm(ti.SetFgBgRGB,
				int(r1), int(g1), int(b1),
				int(r2), int(g2), int(b2)))
			return attr
		}

		if fg.IsRGB() && ti.SetFgRGB != "" {
			r, g, b := fg.RGB()
			t.TPuts(ti.TParm(ti.SetFgRGB, int(r), int(g), int(b)))
			fg = ColorDefault
		}

		if bg.IsRGB() && ti.SetBgRGB != "" {
			r, g, b := bg.RGB()
			t.TPuts(ti.TParm(ti.SetBgRGB,
				int(r), int(g), int(b)))
			bg = ColorDefault
		}
	}

	if fg.Valid() {
		if v, ok := t.colors[fg]; ok {
			fg = v
		} else {
			v = FindColor(fg, t.palette)
			t.colors[fg] = v
			fg = v
		}
	}

	if bg.Valid() {
		if v, ok := t.colors[bg]; ok {
			bg = v
		} else {
			v = FindColor(bg, t.palette)
			t.colors[bg] = v
			bg = v
		}
	}

	if fg.Valid() && bg.Valid() && ti.SetFgBg != "" {
		t.TPuts(ti.TParm(ti.SetFgBg, int(fg&0xff), int(bg&0xff)))
	} else {
		if fg.Valid() && ti.SetFg != "" {
			t.TPuts(ti.TParm(ti.SetFg, int(fg&0xff)))
		}
		if bg.Valid() && ti.SetBg != "" {
			t.TPuts(ti.TParm(ti.SetBg, int(bg&0xff)))
		}
	}
	return attr
}

func (t *tScreen) drawCell(x, y int) int {

	ti := t.ti

	mainc, combc, style, width := t.cells.GetContent(x, y)
	if !t.cells.Dirty(x, y) {
		return width
	}

	if y == t.h-1 && x == t.w-1 && t.ti.AutoMargin && ti.DisableAutoMargin == "" && ti.InsertChar != "" {
		// our solution is somewhat goofy.
		// we write to the second to the last cell what we want in the last cell, then we
		// insert a character at that 2nd to last position to shift the last column into
		// place, then we rewrite that 2nd to last cell.  Old terminals suck.
		t.TPuts(ti.TGoto(x-1, y))
		defer func() {
			t.TPuts(ti.TGoto(x-1, y))
			t.TPuts(ti.InsertChar)
			t.cy = y
			t.cx = x - 1
			t.cells.SetDirty(x-1, y, true)
			_ = t.drawCell(x-1, y)
			t.TPuts(t.ti.TGoto(0, 0))
			t.cy = 0
			t.cx = 0
		}()
	} else if t.cy != y || t.cx != x {
		t.TPuts(ti.TGoto(x, y))
		t.cx = x
		t.cy = y
	}

	if style == StyleDefault {
		style = t.style
	}
	if style != t.curstyle {
		fg, bg, attrs := style.fg, style.bg, style.attrs

		t.TPuts(ti.AttrOff)

		attrs = t.sendFgBg(fg, bg, attrs)
		if attrs&AttrBold != 0 {
			t.TPuts(ti.Bold)
		}
		if us, uc := style.ulStyle, style.ulColor; us != UnderlineStyleNone {
			if t.underColor != "" || t.underRGB != "" {
				if uc == ColorReset {
					t.TPuts(t.underFg)
				} else if uc.IsRGB() {
					if t.underRGB != "" {
						r, g, b := uc.RGB()
						t.TPuts(ti.TParm(t.underRGB, int(r), int(g), int(b)))
					} else {
						if v, ok := t.colors[uc]; ok {
							uc = v
						} else {
							v = FindColor(uc, t.palette)
							t.colors[uc] = v
							uc = v
						}
						t.TPuts(ti.TParm(t.underColor, int(uc&0xff)))
					}
				} else if uc.Valid() {
					t.TPuts(ti.TParm(t.underColor, int(uc&0xff)))
				}
			}
			t.TPuts(ti.Underline) // to ensure everyone gets at least a basic underline
			switch us {
			case UnderlineStyleDouble:
				t.TPuts(t.doubleUnder)
			case UnderlineStyleCurly:
				t.TPuts(t.curlyUnder)
			case UnderlineStyleDotted:
				t.TPuts(t.dottedUnder)
			case UnderlineStyleDashed:
				t.TPuts(t.dashedUnder)
			}
		}
		if attrs&AttrReverse != 0 {
			t.TPuts(ti.Reverse)
		}
		if attrs&AttrBlink != 0 {
			t.TPuts(ti.Blink)
		}
		if attrs&AttrDim != 0 {
			t.TPuts(ti.Dim)
		}
		if attrs&AttrItalic != 0 {
			t.TPuts(ti.Italic)
		}
		if attrs&AttrStrikeThrough != 0 {
			t.TPuts(ti.StrikeThrough)
		}

		// URL string can be long, so don't send it unless we really need to
		if t.enterUrl != "" && t.curstyle.url != style.url {
			if style.url != "" {
				t.TPuts(ti.TParm(t.enterUrl, style.url, style.urlId))
			} else {
				t.TPuts(t.exitUrl)
			}
		}

		t.curstyle = style
	}

	// now emit runes - taking care to not overrun width with a
	// wide character, and to ensure that we emit exactly one regular
	// character followed up by any residual combing characters

	if width < 1 {
		width = 1
	}

	var str string

	buf := make([]byte, 0, 6)

	buf = t.encodeRune(mainc, buf)
	for _, r := range combc {
		buf = t.encodeRune(r, buf)
	}

	str = string(buf)
	if width > 1 && str == "?" {
		// No FullWidth character support
		str = "? "
		t.cx = -1
	}

	if x > t.w-width {
		// too wide to fit; emit a single space instead
		width = 1
		str = " "
	}
	t.writeString(str)
	t.cx += width
	t.cells.SetDirty(x, y, false)
	if width > 1 {
		t.cx = -1
	}

	return width
}

func (t *tScreen) ShowCursor(x, y int) {
	t.Lock()
	t.cursorx = x
	t.cursory = y
	t.Unlock()
}

func (t *tScreen) SetCursor(cs CursorStyle, cc Color) {
	t.Lock()
	t.cursorStyle = cs
	t.cursorColor = cc
	t.Unlock()
}

func (t *tScreen) HideCursor() {
	t.ShowCursor(-1, -1)
}

func (t *tScreen) showCursor() {

	x, y := t.cursorx, t.cursory
	w, h := t.cells.Size()
	if x < 0 || y < 0 || x >= w || y >= h {
		t.hideCursor()
		return
	}
	t.TPuts(t.ti.TGoto(x, y))
	t.TPuts(t.ti.ShowCursor)
	if t.cursorStyles != nil {
		if esc, ok := t.cursorStyles[t.cursorStyle]; ok {
			t.TPuts(esc)
		}
	}
	if t.cursorRGB != "" {
		if t.cursorColor == ColorReset {
			t.TPuts(t.cursorFg)
		} else if t.cursorColor.Valid() {
			r, g, b := t.cursorColor.RGB()
			t.TPuts(t.ti.TParm(t.cursorRGB, int(r), int(g), int(b)))
		}
	}
	t.cx = x
	t.cy = y
}

// writeString sends a string to the terminal. The string is sent as-is and
// this function does not expand inline padding indications (of the form
// $<[delay]> where [delay] is msec). In order to have these expanded, use
// TPuts. If the screen is "buffering", the string is collected in a buffer,
// with the intention that the entire buffer be sent to the terminal in one
// write operation at some point later.
func (t *tScreen) writeString(s string) {
	if t.buffering {
		_, _ = io.WriteString(&t.buf, s)
	} else {
		_, _ = io.WriteString(t.tty, s)
	}
}

func (t *tScreen) TPuts(s string) {
	if t.buffering {
		t.ti.TPuts(&t.buf, s)
	} else {
		t.ti.TPuts(t.tty, s)
	}
}

func (t *tScreen) Show() {
	t.Lock()
	if !t.fini {
		t.resize()
		t.draw()
	}
	t.Unlock()
}

func (t *tScreen) clearScreen() {
	t.TPuts(t.ti.AttrOff)
	t.TPuts(t.exitUrl)
	_ = t.sendFgBg(t.style.fg, t.style.bg, AttrNone)
	t.TPuts(t.ti.Clear)
	t.clear = false
}

func (t *tScreen) hideCursor() {
	// does not update cursor position
	if t.ti.HideCursor != "" {
		t.TPuts(t.ti.HideCursor)
	} else {
		// No way to hide cursor, stick it
		// at bottom right of screen
		t.cx, t.cy = t.cells.Size()
		t.TPuts(t.ti.TGoto(t.cx, t.cy))
	}
}

func (t *tScreen) draw() {
	// clobber cursor position, because we're going to change it all
	t.cx = -1
	t.cy = -1
	// make no style assumptions
	t.curstyle = styleInvalid

	t.buf.Reset()
	t.buffering = true
	defer func() {
		t.buffering = false
	}()

	// hide the cursor while we move stuff around
	t.hideCursor()

	if t.clear {
		t.clearScreen()
	}

	for y := 0; y < t.h; y++ {
		for x := 0; x < t.w; x++ {
			width := t.drawCell(x, y)
			if width > 1 {
				if x+1 < t.w {
					// this is necessary so that if we ever
					// go back to drawing that cell, we
					// actually will *draw* it.
					t.cells.SetDirty(x+1, y, true)
				}
			}
			x += width - 1
		}
	}

	// restore the cursor
	t.showCursor()

	_, _ = t.buf.WriteTo(t.tty)
}

func (t *tScreen) EnableMouse(flags ...MouseFlags) {
	var f MouseFlags
	flagsPresent := false
	for _, flag := range flags {
		f |= flag
		flagsPresent = true
	}
	if !flagsPresent {
		f = MouseMotionEvents | MouseDragEvents | MouseButtonEvents
	}

	t.Lock()
	t.mouseFlags = f
	t.enableMouse(f)
	t.Unlock()
}

func (t *tScreen) enableMouse(f MouseFlags) {
	// Rather than using terminfo to find mouse escape sequences, we rely on the fact that
	// pretty much *every* terminal that supports mouse tracking follows the
	// XTerm standards (the modern ones).
	if len(t.mouse) != 0 {
		// start by disabling all tracking.
		t.TPuts("\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l")
		if f&MouseButtonEvents != 0 {
			t.TPuts("\x1b[?1000h")
		}
		if f&MouseDragEvents != 0 {
			t.TPuts("\x1b[?1002h")
		}
		if f&MouseMotionEvents != 0 {
			t.TPuts("\x1b[?1003h")
		}
		if f&(MouseButtonEvents|MouseDragEvents|MouseMotionEvents) != 0 {
			t.TPuts("\x1b[?1006h")
		}
	}

}

func (t *tScreen) DisableMouse() {
	t.Lock()
	t.mouseFlags = 0
	t.enableMouse(0)
	t.Unlock()
}

func (t *tScreen) EnablePaste() {
	t.Lock()
	t.pasteEnabled = true
	t.enablePasting(true)
	t.Unlock()
}

func (t *tScreen) DisablePaste() {
	t.Lock()
	t.pasteEnabled = false
	t.enablePasting(false)
	t.Unlock()
}

func (t *tScreen) enablePasting(on bool) {
	var s string
	if on {
		s = t.enablePaste
	} else {
		s = t.disablePaste
	}
	if s != "" {
		t.TPuts(s)
	}
}

func (t *tScreen) EnableFocus() {
	t.Lock()
	t.focusEnabled = true
	t.enableFocusReporting()
	t.Unlock()
}

func (t *tScreen) DisableFocus() {
	t.Lock()
	t.focusEnabled = false
	t.disableFocusReporting()
	t.Unlock()
}

func (t *tScreen) enableFocusReporting() {
	if t.enableFocus != "" {
		t.TPuts(t.enableFocus)
	}
}

func (t *tScreen) disableFocusReporting() {
	if t.disableFocus != "" {
		t.TPuts(t.disableFocus)
	}
}

func (t *tScreen) Size() (int, int) {
	t.Lock()
	w, h := t.w, t.h
	t.Unlock()
	return w, h
}

func (t *tScreen) resize() {
	ws, err := t.tty.WindowSize()
	if err != nil {
		return
	}
	if ws.Width == t.w && ws.Height == t.h {
		return
	}
	t.cx = -1
	t.cy = -1

	t.cells.Resize(ws.Width, ws.Height)
	t.cells.Invalidate()
	t.h = ws.Height
	t.w = ws.Width
	ev := &EventResize{t: time.Now(), ws: ws}
	select {
	case t.eventQ <- ev:
	default:
	}
}

func (t *tScreen) Colors() int {
	// this doesn't change, no need for lock
	if t.truecolor {
		return 1 << 24
	}
	return t.ti.Colors
}

// nColors returns the size of the built-in palette.
// This is distinct from Colors(), as it will generally
// always be a small number. (<= 256)
func (t *tScreen) nColors() int {
	return t.ti.Colors
}

// vtACSNames is a map of bytes defined by terminfo that are used in
// the terminals Alternate Character Set to represent other glyphs.
// For example, the upper left corner of the box drawing set can be
// displayed by printing "l" while in the alternate character set.
// It's not quite that simple, since the "l" is the terminfo name,
// and it may be necessary to use a different character based on
// the terminal implementation (or the terminal may lack support for
// this altogether).  See buildAcsMap below for detail.
var vtACSNames = map[byte]rune{
	'+': RuneRArrow,
	',': RuneLArrow,
	'-': RuneUArrow,
	'.': RuneDArrow,
	'0': RuneBlock,
	'`': RuneDiamond,
	'a': RuneCkBoard,
	'b': '␉', // VT100, Not defined by terminfo
	'c': '␌', // VT100, Not defined by terminfo
	'd': '␋', // VT100, Not defined by terminfo
	'e': '␊', // VT100, Not defined by terminfo
	'f': RuneDegree,
	'g': RunePlMinus,
	'h': RuneBoard,
	'i': RuneLantern,
	'j': RuneLRCorner,
	'k': RuneURCorner,
	'l': RuneULCorner,
	'm': RuneLLCorner,
	'n': RunePlus,
	'o': RuneS1,
	'p': RuneS3,
	'q': RuneHLine,
	'r': RuneS7,
	's': RuneS9,
	't': RuneLTee,
	'u': RuneRTee,
	'v': RuneBTee,
	'w': RuneTTee,
	'x': RuneVLine,
	'y': RuneLEqual,
	'z': RuneGEqual,
	'{': RunePi,
	'|': RuneNEqual,
	'}': RuneSterling,
	'~': RuneBullet,
}

// buildAcsMap builds a map of characters that we translate from Unicode to
// alternate character encodings.  To do this, we use the standard VT100 ACS
// maps.  This is only done if the terminal lacks support for Unicode; we
// always prefer to emit Unicode glyphs when we are able.
func (t *tScreen) buildAcsMap() {
	acsstr := t.ti.AltChars
	t.acs = make(map[rune]string)
	for len(acsstr) > 2 {
		srcv := acsstr[0]
		dstv := string(acsstr[1])
		if r, ok := vtACSNames[srcv]; ok {
			t.acs[r] = t.ti.EnterAcs + dstv + t.ti.ExitAcs
		}
		acsstr = acsstr[2:]
	}
}

func (t *tScreen) clip(x, y int) (int, int) {
	w, h := t.cells.Size()
	if x < 0 {
		x = 0
	}
	if y < 0 {
		y = 0
	}
	if x > w-1 {
		x = w - 1
	}
	if y > h-1 {
		y = h - 1
	}
	return x, y
}

// buildMouseEvent returns an event based on the supplied coordinates and button
// state. Note that the screen's mouse button state is updated based on the
// input to this function (i.e. it mutates the receiver).
func (t *tScreen) buildMouseEvent(x, y, btn int) *EventMouse {

	// XTerm mouse events only report at most one button at a time,
	// which may include a wheel button.  Wheel motion events are
	// reported as single impulses, while other button events are reported
	// as separate press & release events.

	button := ButtonNone
	mod := ModNone

	// Mouse wheel has bit 6 set, no release events.  It should be noted
	// that wheel events are sometimes misdelivered as mouse button events
	// during a click-drag, so we debounce these, considering them to be
	// button press events unless we see an intervening release event.
	switch btn & 0x43 {
	case 0:
		button = Button1
	case 1:
		button = Button3 // Note we prefer to treat right as button 2
	case 2:
		button = Button2 // And the middle button as button 3
	case 3:
		button = ButtonNone
	case 0x40:
		button = WheelUp
	case 0x41:
		button = WheelDown
	case 0x42:
		button = WheelLeft
	case 0x43:
		button = WheelRight
	}

	if btn&0x4 != 0 {
		mod |= ModShift
	}
	if btn&0x8 != 0 {
		mod |= ModAlt
	}
	if btn&0x10 != 0 {
		mod |= ModCtrl
	}

	// Some terminals will report mouse coordinates outside the
	// screen, especially with click-drag events.  Clip the coordinates
	// to the screen in that case.
	x, y = t.clip(x, y)

	return NewEventMouse(x, y, button, mod)
}

// parseSgrMouse attempts to locate an SGR mouse record at the start of the
// buffer.  It returns true, true if it found one, and the associated bytes
// be removed from the buffer.  It returns true, false if the buffer might
// contain such an event, but more bytes are necessary (partial match), and
// false, false if the content is definitely *not* an SGR mouse record.
func (t *tScreen) parseSgrMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) {

	b := buf.Bytes()

	var x, y, btn, state int
	dig := false
	neg := false
	motion := false
	scroll := false
	i := 0
	val := 0

	for i = range b {
		switch b[i] {
		case '\x1b':
			if state != 0 {
				return false, false
			}
			state = 1

		case '\x9b':
			if state != 0 {
				return false, false
			}
			state = 2

		case '[':
			if state != 1 {
				return false, false
			}
			state = 2

		case '<':
			if state != 2 {
				return false, false
			}
			val = 0
			dig = false
			neg = false
			state = 3

		case '-':
			if state != 3 && state != 4 && state != 5 {
				return false, false
			}
			if dig || neg {
				return false, false
			}
			neg = true // stay in state

		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
			if state != 3 && state != 4 && state != 5 {
				return false, false
			}
			val *= 10
			val += int(b[i] - '0')
			dig = true // stay in state

		case ';':
			if neg {
				val = -val
			}
			switch state {
			case 3:
				btn, val = val, 0
				neg, dig, state = false, false, 4
			case 4:
				x, val = val-1, 0
				neg, dig, state = false, false, 5
			default:
				return false, false
			}

		case 'm', 'M':
			if state != 5 {
				return false, false
			}
			if neg {
				val = -val
			}
			y = val - 1

			motion = (btn & 32) != 0
			scroll = (btn & 0x42) == 0x40
			btn &^= 32
			if b[i] == 'm' {
				// mouse release, clear all buttons
				btn |= 3
				btn &^= 0x40
				t.buttondn = false
			} else if motion {
				/*
				 * Some broken terminals appear to send
				 * mouse button one motion events, instead of
				 * encoding 35 (no buttons) into these events.
				 * We resolve these by looking for a non-motion
				 * event first.
				 */
				if !t.buttondn {
					btn |= 3
					btn &^= 0x40
				}
			} else if !scroll {
				t.buttondn = true
			}
			// consume the event bytes
			for i >= 0 {
				_, _ = buf.ReadByte()
				i--
			}
			*evs = append(*evs, t.buildMouseEvent(x, y, btn))
			return true, true
		}
	}

	// incomplete & inconclusive at this point
	return true, false
}

func (t *tScreen) parseFocus(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
	state := 0
	b := buf.Bytes()
	for i := range b {
		switch state {
		case 0:
			if b[i] != '\x1b' {
				return false, false
			}
			state = 1
		case 1:
			if b[i] != '[' {
				return false, false
			}
			state = 2
		case 2:
			if b[i] != 'I' && b[i] != 'O' {
				return false, false
			}
			*evs = append(*evs, NewEventFocus(b[i] == 'I'))
			_, _ = buf.ReadByte()
			_, _ = buf.ReadByte()
			_, _ = buf.ReadByte()
			return true, true
		}
	}
	return true, false
}

func (t *tScreen) parseClipboard(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
	b := buf.Bytes()
	state := 0
	prefix := []byte("\x1b]52;c;")

	if len(prefix) >= len(b) {
		if bytes.HasPrefix(prefix, b) {
			// inconclusive so far
			return true, false
		}
		// definitely not a match
		return false, false
	}
	b = b[len(prefix):]

	for _, c := range b {
		// valid base64 digits
		if state == 0 {
			if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || (c == '+') || (c == '/') || (c == '=') {
				continue
			}
			if c == '\x1b' {
				state = 1
				continue
			}
			if c == '\a' {
				// matched with BEL instead of ST
				b = b[:len(b)-1] // drop the trailing BEL
				decoded := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
				if num, err := base64.StdEncoding.Decode(decoded, b); err == nil {
					*evs = append(*evs, NewEventClipboard(decoded[:num]))
				}
				_, _ = buf.ReadBytes('\a')
				return true, true
			}
			return false, false
		}
		if state == 1 {
			if c == '\\' {
				b = b[:len(b)-2] // drop the trailing ST (\x1b\\)
				// now decode the data
				decoded := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
				if num, err := base64.StdEncoding.Decode(decoded, b); err == nil {
					*evs = append(*evs, NewEventClipboard(decoded[:num]))
				}
				_, _ = buf.ReadBytes('\\')
				return true, true
			}
			return false, false
		}
	}
	// not enough data yet (not terminated)
	return true, false
}

// parseXtermMouse is like parseSgrMouse, but it parses a legacy
// X11 mouse record.
func (t *tScreen) parseXtermMouse(buf *bytes.Buffer, evs *[]Event) (bool, bool) {

	b := buf.Bytes()

	state := 0
	btn := 0
	x := 0
	y := 0

	for i := range b {
		switch state {
		case 0:
			switch b[i] {
			case '\x1b':
				state = 1
			case '\x9b':
				state = 2
			default:
				return false, false
			}
		case 1:
			if b[i] != '[' {
				return false, false
			}
			state = 2
		case 2:
			if b[i] != 'M' {
				return false, false
			}
			state++
		case 3:
			btn = int(b[i])
			state++
		case 4:
			x = int(b[i]) - 32 - 1
			state++
		case 5:
			y = int(b[i]) - 32 - 1
			for i >= 0 {
				_, _ = buf.ReadByte()
				i--
			}
			*evs = append(*evs, t.buildMouseEvent(x, y, btn))
			return true, true
		}
	}
	return true, false
}

func (t *tScreen) parseFunctionKey(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
	b := buf.Bytes()
	partial := false
	for e, k := range t.keycodes {
		esc := []byte(e)
		if (len(esc) == 1) && (esc[0] == '\x1b') {
			continue
		}
		if bytes.HasPrefix(b, esc) {
			// matched
			var r rune
			if len(esc) == 1 {
				r = rune(b[0])
			}
			mod := k.mod
			if t.escaped {
				mod |= ModAlt
				t.escaped = false
			}
			switch k.key {
			case keyPasteStart:
				*evs = append(*evs, NewEventPaste(true))
			case keyPasteEnd:
				*evs = append(*evs, NewEventPaste(false))
			default:
				*evs = append(*evs, NewEventKey(k.key, r, mod))
			}
			for i := 0; i < len(esc); i++ {
				_, _ = buf.ReadByte()
			}
			return true, true
		}
		if bytes.HasPrefix(esc, b) {
			partial = true
		}
	}
	return partial, false
}

func (t *tScreen) parseRune(buf *bytes.Buffer, evs *[]Event) (bool, bool) {
	b := buf.Bytes()
	if b[0] >= ' ' && b[0] <= 0x7F {
		// printable ASCII easy to deal with -- no encodings
		mod := ModNone
		if t.escaped {
			mod = ModAlt
			t.escaped = false
		}
		*evs = append(*evs, NewEventKey(KeyRune, rune(b[0]), mod))
		_, _ = buf.ReadByte()
		return true, true
	}

	if b[0] < 0x80 {
		// Low numbered values are control keys, not runes.
		return false, false
	}

	utf := make([]byte, 12)
	for l := 1; l <= len(b); l++ {
		t.decoder.Reset()
		nOut, nIn, e := t.decoder.Transform(utf, b[:l], true)
		if e == transform.ErrShortSrc {
			continue
		}
		if nOut != 0 {
			r, _ := utf8.DecodeRune(utf[:nOut])
			if r != utf8.RuneError {
				mod := ModNone
				if t.escaped {
					mod = ModAlt
					t.escaped = false
				}
				*evs = append(*evs, NewEventKey(KeyRune, r, mod))
			}
			for nIn > 0 {
				_, _ = buf.ReadByte()
				nIn--
			}
			return true, true
		}
	}
	// Looks like potential escape
	return true, false
}

func (t *tScreen) scanInput(buf *bytes.Buffer, expire bool) {
	evs := t.collectEventsFromInput(buf, expire)

	for _, ev := range evs {
		select {
		case t.eventQ <- ev:
		case <-t.quit:
			return
		}
	}
}

// Return an array of Events extracted from the supplied buffer. This is done
// while holding the screen's lock - the events can then be queued for
// application processing with the lock released.
func (t *tScreen) collectEventsFromInput(buf *bytes.Buffer, expire bool) []Event {

	res := make([]Event, 0, 20)

	t.Lock()
	defer t.Unlock()

	for {
		b := buf.Bytes()
		if len(b) == 0 {
			buf.Reset()
			return res
		}

		partials := 0

		if part, comp := t.parseRune(buf, &res); comp {
			continue
		} else if part {
			partials++
		}

		if part, comp := t.parseFunctionKey(buf, &res); comp {
			continue
		} else if part {
			partials++
		}

		if part, comp := t.parseFocus(buf, &res); comp {
			continue
		} else if part {
			partials++
		}

		// Only parse mouse records if this term claims to have
		// mouse support

		if t.ti.Mouse != "" {
			if part, comp := t.parseXtermMouse(buf, &res); comp {
				continue
			} else if part {
				partials++
			}

			if part, comp := t.parseSgrMouse(buf, &res); comp {
				continue
			} else if part {
				partials++
			}
		}

		if t.setClipboard != "" {
			if part, comp := t.parseClipboard(buf, &res); comp {
				continue
			} else if part {
				partials++
			}
		}

		if partials == 0 || expire {
			if b[0] == '\x1b' {
				if len(b) == 1 {
					res = append(res, NewEventKey(KeyEsc, 0, ModNone))
					t.escaped = false
				} else {
					t.escaped = true
				}
				_, _ = buf.ReadByte()
				continue
			}
			// Nothing was going to match, or we timed-out
			// waiting for more data -- just deliver the characters
			// to the app & let them sort it out.  Possibly we
			// should only do this for control characters like ESC.
			by, _ := buf.ReadByte()
			mod := ModNone
			if t.escaped {
				t.escaped = false
				mod = ModAlt
			}
			res = append(res, NewEventKey(KeyRune, rune(by), mod))
			continue
		}

		// well we have some partial data, wait until we get
		// some more
		break
	}

	return res
}

func (t *tScreen) mainLoop(stopQ chan struct{}) {
	defer t.wg.Done()
	buf := &bytes.Buffer{}
	for {
		select {
		case <-stopQ:
			return
		case <-t.quit:
			return
		case <-t.resizeQ:
			t.Lock()
			t.cx = -1
			t.cy = -1
			t.resize()
			t.cells.Invalidate()
			t.draw()
			t.Unlock()
			continue
		case <-t.keytimer.C:
			// If the timer fired, and the current time
			// is after the expiration of the escape sequence,
			// then we assume the escape sequence reached its
			// conclusion, and process the chunk independently.
			// This lets us detect conflicts such as a lone ESC.
			if buf.Len() > 0 {
				if time.Now().After(t.keyexpire) {
					t.scanInput(buf, true)
				}
			}
			if buf.Len() > 0 {
				if !t.keytimer.Stop() {
					select {
					case <-t.keytimer.C:
					default:
					}
				}
				t.keytimer.Reset(time.Millisecond * 50)
			}
		case chunk := <-t.keychan:
			buf.Write(chunk)
			t.keyexpire = time.Now().Add(time.Millisecond * 50)
			t.scanInput(buf, false)
			if !t.keytimer.Stop() {
				select {
				case <-t.keytimer.C:
				default:
				}
			}
			if buf.Len() > 0 {
				t.keytimer.Reset(time.Millisecond * 50)
			}
		}
	}
}

func (t *tScreen) inputLoop(stopQ chan struct{}) {

	defer t.wg.Done()
	for {
		select {
		case <-stopQ:
			return
		default:
		}
		chunk := make([]byte, 128)
		n, e := t.tty.Read(chunk)
		switch e {
		case nil:
		default:
			t.Lock()
			running := t.running
			t.Unlock()
			if running {
				select {
				case t.eventQ <- NewEventError(e):
				case <-t.quit:
				}
			}
			return
		}
		if n > 0 {
			t.keychan <- chunk[:n]
		}
	}
}

func (t *tScreen) Sync() {
	t.Lock()
	t.cx = -1
	t.cy = -1
	if !t.fini {
		t.resize()
		t.clear = true
		t.cells.Invalidate()
		t.draw()
	}
	t.Unlock()
}

func (t *tScreen) CharacterSet() string {
	return t.charset
}

func (t *tScreen) RegisterRuneFallback(orig rune, fallback string) {
	t.Lock()
	t.fallback[orig] = fallback
	t.Unlock()
}

func (t *tScreen) UnregisterRuneFallback(orig rune) {
	t.Lock()
	delete(t.fallback, orig)
	t.Unlock()
}

func (t *tScreen) CanDisplay(r rune, checkFallbacks bool) bool {

	if enc := t.encoder; enc != nil {
		nb := make([]byte, 6)
		ob := make([]byte, 6)
		num := utf8.EncodeRune(ob, r)

		enc.Reset()
		dst, _, err := enc.Transform(nb, ob[:num], true)
		if dst != 0 && err == nil && nb[0] != '\x1A' {
			return true
		}
	}
	// Terminal fallbacks always permitted, since we assume they are
	// basically nearly perfect renditions.
	if _, ok := t.acs[r]; ok {
		return true
	}
	if !checkFallbacks {
		return false
	}
	if _, ok := t.fallback[r]; ok {
		return true
	}
	return false
}

func (t *tScreen) HasMouse() bool {
	return len(t.mouse) != 0
}

func (t *tScreen) HasKey(k Key) bool {
	if k == KeyRune {
		return true
	}
	return t.keyexist[k]
}

func (t *tScreen) SetSize(w, h int) {
	if t.setWinSize != "" {
		t.TPuts(t.ti.TParm(t.setWinSize, w, h))
	}
	t.cells.Invalidate()
	t.resize()
}

func (t *tScreen) Resize(int, int, int, int) {}

func (t *tScreen) Suspend() error {
	t.disengage()
	return nil
}

func (t *tScreen) Resume() error {
	return t.engage()
}

func (t *tScreen) Tty() (Tty, bool) {
	return t.tty, true
}

// engage is used to place the terminal in raw mode and establish screen size, etc.
// Think of this is as tcell "engaging" the clutch, as it's going to be driving the
// terminal interface.
func (t *tScreen) engage() error {
	t.Lock()
	defer t.Unlock()
	if t.tty == nil {
		return ErrNoScreen
	}
	t.tty.NotifyResize(func() {
		select {
		case t.resizeQ <- true:
		default:
		}
	})
	if t.running {
		return errors.New("already engaged")
	}
	if err := t.tty.Start(); err != nil {
		return err
	}
	t.running = true
	if ws, err := t.tty.WindowSize(); err == nil && ws.Width != 0 && ws.Height != 0 {
		t.cells.Resize(ws.Width, ws.Height)
	}
	stopQ := make(chan struct{})
	t.stopQ = stopQ
	t.enableMouse(t.mouseFlags)
	t.enablePasting(t.pasteEnabled)
	if t.focusEnabled {
		t.enableFocusReporting()
	}

	ti := t.ti
	if os.Getenv("TCELL_ALTSCREEN") != "disable" {
		// Technically this may not be right, but every terminal we know about
		// (even Wyse 60) uses this to enter the alternate screen buffer, and
		// possibly save and restore the window title and/or icon.
		// (In theory there could be terminals that don't support X,Y cursor
		// positions without a setup command, but we don't support them.)
		t.TPuts(ti.EnterCA)
		if t.saveTitle != "" {
			t.TPuts(t.saveTitle)
		}
	}
	t.TPuts(ti.EnterKeypad)
	t.TPuts(ti.HideCursor)
	t.TPuts(ti.EnableAcs)
	t.TPuts(ti.DisableAutoMargin)
	t.TPuts(ti.Clear)
	if t.title != "" && t.setTitle != "" {
		t.TPuts(t.ti.TParm(t.setTitle, t.title))
	}

	t.wg.Add(2)
	go t.inputLoop(stopQ)
	go t.mainLoop(stopQ)
	return nil
}

// disengage is used to release the terminal back to support from the caller.
// Think of this as tcell disengaging the clutch, so that another application
// can take over the terminal interface.  This restores the TTY mode that was
// present when the application was first started.
func (t *tScreen) disengage() {

	t.Lock()
	if !t.running {
		t.Unlock()
		return
	}
	t.running = false
	stopQ := t.stopQ
	close(stopQ)
	_ = t.tty.Drain()
	t.Unlock()

	t.tty.NotifyResize(nil)
	// wait for everything to shut down
	t.wg.Wait()

	// shutdown the screen and disable special modes (e.g. mouse and bracketed paste)
	ti := t.ti
	t.cells.Resize(0, 0)
	t.TPuts(ti.ShowCursor)
	if t.cursorStyles != nil && t.cursorStyle != CursorStyleDefault {
		t.TPuts(t.cursorStyles[CursorStyleDefault])
	}
	if t.cursorFg != "" && t.cursorColor.Valid() {
		t.TPuts(t.cursorFg)
	}
	t.TPuts(ti.ResetFgBg)
	t.TPuts(ti.AttrOff)
	t.TPuts(ti.ExitKeypad)
	t.TPuts(ti.EnableAutoMargin)
	if os.Getenv("TCELL_ALTSCREEN") != "disable" {
		if t.restoreTitle != "" {
			t.TPuts(t.restoreTitle)
		}
		t.TPuts(ti.Clear) // only needed if ExitCA is empty
		t.TPuts(ti.ExitCA)
	}
	t.enableMouse(0)
	t.enablePasting(false)
	t.disableFocusReporting()

	_ = t.tty.Stop()
}

// Beep emits a beep to the terminal.
func (t *tScreen) Beep() error {
	t.writeString(string(byte(7)))
	return nil
}

// finalize is used to at application shutdown, and restores the terminal
// to it's initial state.  It should not be called more than once.
func (t *tScreen) finalize() {
	t.disengage()
	_ = t.tty.Close()
}

func (t *tScreen) StopQ() <-chan struct{} {
	return t.quit
}

func (t *tScreen) EventQ() chan Event {
	return t.eventQ
}

func (t *tScreen) GetCells() *CellBuffer {
	return &t.cells
}

func (t *tScreen) SetTitle(title string) {
	t.Lock()
	t.title = title
	if t.setTitle != "" && t.running {
		t.TPuts(t.ti.TParm(t.setTitle, title))
	}
	t.Unlock()
}

func (t *tScreen) SetClipboard(data []byte) {
	// Post binary data to the system clipboard.  It might be UTF-8, it might not be.
	t.Lock()
	if t.setClipboard != "" {
		encoded := base64.StdEncoding.EncodeToString(data)
		t.TPuts(t.ti.TParm(t.setClipboard, encoded))
	}
	t.Unlock()
}

func (t *tScreen) GetClipboard() {
	t.Lock()
	if t.setClipboard != "" {
		t.TPuts(t.ti.TParm(t.setClipboard, "?"))
	}
	t.Unlock()
}
