// Package terminfo implements reading terminfo files in pure go.
package terminfo

//go:generate go run gen.go

import (
	"io"
	"io/ioutil"
	"path"
	"strconv"
	"strings"
)

// Error is a terminfo error.
type Error string

// Error satisfies the error interface.
func (err Error) Error() string {
	return string(err)
}

const (
	// ErrInvalidFileSize is the invalid file size error.
	ErrInvalidFileSize Error = "invalid file size"
	// ErrUnexpectedFileEnd is the unexpected file end error.
	ErrUnexpectedFileEnd Error = "unexpected file end"
	// ErrInvalidStringTable is the invalid string table error.
	ErrInvalidStringTable Error = "invalid string table"
	// ErrInvalidMagic is the invalid magic error.
	ErrInvalidMagic Error = "invalid magic"
	// ErrInvalidHeader is the invalid header error.
	ErrInvalidHeader Error = "invalid header"
	// ErrInvalidNames is the invalid names error.
	ErrInvalidNames Error = "invalid names"
	// ErrInvalidExtendedHeader is the invalid extended header error.
	ErrInvalidExtendedHeader Error = "invalid extended header"
	// ErrEmptyTermName is the empty term name error.
	ErrEmptyTermName Error = "empty term name"
	// ErrDatabaseDirectoryNotFound is the database directory not found error.
	ErrDatabaseDirectoryNotFound Error = "database directory not found"
	// ErrFileNotFound is the file not found error.
	ErrFileNotFound Error = "file not found"
	// ErrInvalidTermProgramVersion is the invalid TERM_PROGRAM_VERSION error.
	ErrInvalidTermProgramVersion Error = "invalid TERM_PROGRAM_VERSION"
)

// Terminfo describes a terminal's capabilities.
type Terminfo struct {
	// File is the original source file.
	File string
	// Names are the provided cap names.
	Names []string
	// Bools are the bool capabilities.
	Bools map[int]bool
	// BoolsM are the missing bool capabilities.
	BoolsM map[int]bool
	// Nums are the num capabilities.
	Nums map[int]int
	// NumsM are the missing num capabilities.
	NumsM map[int]bool
	// Strings are the string capabilities.
	Strings map[int][]byte
	// StringsM are the missing string capabilities.
	StringsM map[int]bool
	// ExtBools are the extended bool capabilities.
	ExtBools map[int]bool
	// ExtBoolsNames is the map of extended bool capabilities to their index.
	ExtBoolNames map[int][]byte
	// ExtNums are the extended num capabilities.
	ExtNums map[int]int
	// ExtNumsNames is the map of extended num capabilities to their index.
	ExtNumNames map[int][]byte
	// ExtStrings are the extended string capabilities.
	ExtStrings map[int][]byte
	// ExtStringsNames is the map of extended string capabilities to their index.
	ExtStringNames map[int][]byte
}

// Decode decodes the terminfo data contained in buf.
func Decode(buf []byte) (*Terminfo, error) {
	var err error
	// check max file length
	if len(buf) >= maxFileLength {
		return nil, ErrInvalidFileSize
	}
	d := &decoder{
		buf: buf,
		n:   len(buf),
	}
	// read header
	h, err := d.readInts(6, 16)
	if err != nil {
		return nil, err
	}
	var numWidth int
	// check magic
	switch {
	case h[fieldMagic] == magic:
		numWidth = 16
	case h[fieldMagic] == magicExtended:
		numWidth = 32
	default:
		return nil, ErrInvalidMagic
	}
	// check header
	if hasInvalidCaps(h) {
		return nil, ErrInvalidHeader
	}
	// check remaining length
	if d.n-d.pos < capLength(h) {
		return nil, ErrUnexpectedFileEnd
	}
	// read names
	names, err := d.readBytes(h[fieldNameSize])
	if err != nil {
		return nil, err
	}
	// check name is terminated properly
	i := findNull(names, 0)
	if i == -1 {
		return nil, ErrInvalidNames
	}
	names = names[:i]
	// read bool caps
	bools, boolsM, err := d.readBools(h[fieldBoolCount])
	if err != nil {
		return nil, err
	}
	// read num caps
	nums, numsM, err := d.readNums(h[fieldNumCount], numWidth)
	if err != nil {
		return nil, err
	}
	// read string caps
	strs, strsM, err := d.readStrings(h[fieldStringCount], h[fieldTableSize])
	if err != nil {
		return nil, err
	}
	ti := &Terminfo{
		Names:    strings.Split(string(names), "|"),
		Bools:    bools,
		BoolsM:   boolsM,
		Nums:     nums,
		NumsM:    numsM,
		Strings:  strs,
		StringsM: strsM,
	}
	// at the end of file, so no extended caps
	if d.pos >= d.n {
		return ti, nil
	}
	// decode extended header
	eh, err := d.readInts(5, 16)
	if err != nil {
		return nil, err
	}
	// check extended offset field
	if hasInvalidExtOffset(eh) {
		return nil, ErrInvalidExtendedHeader
	}
	// check extended cap lengths
	if d.n-d.pos != extCapLength(eh, numWidth) {
		return nil, ErrInvalidExtendedHeader
	}
	// read extended bool caps
	ti.ExtBools, _, err = d.readBools(eh[fieldExtBoolCount])
	if err != nil {
		return nil, err
	}
	// read extended num caps
	ti.ExtNums, _, err = d.readNums(eh[fieldExtNumCount], numWidth)
	if err != nil {
		return nil, err
	}
	// read extended string data table indexes
	extIndexes, err := d.readInts(eh[fieldExtOffsetCount], 16)
	if err != nil {
		return nil, err
	}
	// read string data table
	extData, err := d.readBytes(eh[fieldExtTableSize])
	if err != nil {
		return nil, err
	}
	// precautionary check that exactly at end of file
	if d.pos != d.n {
		return nil, ErrUnexpectedFileEnd
	}
	var last int
	// read extended string caps
	ti.ExtStrings, last, err = readStrings(extIndexes, extData, eh[fieldExtStringCount])
	if err != nil {
		return nil, err
	}
	extIndexes, extData = extIndexes[eh[fieldExtStringCount]:], extData[last:]
	// read extended bool names
	ti.ExtBoolNames, _, err = readStrings(extIndexes, extData, eh[fieldExtBoolCount])
	if err != nil {
		return nil, err
	}
	extIndexes = extIndexes[eh[fieldExtBoolCount]:]
	// read extended num names
	ti.ExtNumNames, _, err = readStrings(extIndexes, extData, eh[fieldExtNumCount])
	if err != nil {
		return nil, err
	}
	extIndexes = extIndexes[eh[fieldExtNumCount]:]
	// read extended string names
	ti.ExtStringNames, _, err = readStrings(extIndexes, extData, eh[fieldExtStringCount])
	if err != nil {
		return nil, err
	}
	// extIndexes = extIndexes[eh[fieldExtStringCount]:]
	return ti, nil
}

// Open reads the terminfo file name from the specified directory dir.
func Open(dir, name string) (*Terminfo, error) {
	var err error
	var buf []byte
	var filename string
	for _, f := range []string{
		path.Join(dir, name[0:1], name),
		path.Join(dir, strconv.FormatUint(uint64(name[0]), 16), name),
	} {
		buf, err = ioutil.ReadFile(f)
		if err == nil {
			filename = f
			break
		}
	}
	if buf == nil {
		return nil, ErrFileNotFound
	}
	// decode
	ti, err := Decode(buf)
	if err != nil {
		return nil, err
	}
	// save original file name
	ti.File = filename
	// add to cache
	termCache.Lock()
	for _, n := range ti.Names {
		termCache.db[n] = ti
	}
	termCache.Unlock()
	return ti, nil
}

// boolCaps returns all bool and extended capabilities using f to format the
// index key.
func (ti *Terminfo) boolCaps(f func(int) string, extended bool) map[string]bool {
	m := make(map[string]bool, len(ti.Bools)+len(ti.ExtBools))
	if !extended {
		for k, v := range ti.Bools {
			m[f(k)] = v
		}
	} else {
		for k, v := range ti.ExtBools {
			m[string(ti.ExtBoolNames[k])] = v
		}
	}
	return m
}

// BoolCaps returns all bool capabilities.
func (ti *Terminfo) BoolCaps() map[string]bool {
	return ti.boolCaps(BoolCapName, false)
}

// BoolCapsShort returns all bool capabilities, using the short name as the
// index.
func (ti *Terminfo) BoolCapsShort() map[string]bool {
	return ti.boolCaps(BoolCapNameShort, false)
}

// ExtBoolCaps returns all extended bool capabilities.
func (ti *Terminfo) ExtBoolCaps() map[string]bool {
	return ti.boolCaps(BoolCapName, true)
}

// ExtBoolCapsShort returns all extended bool capabilities, using the short
// name as the index.
func (ti *Terminfo) ExtBoolCapsShort() map[string]bool {
	return ti.boolCaps(BoolCapNameShort, true)
}

// numCaps returns all num and extended capabilities using f to format the
// index key.
func (ti *Terminfo) numCaps(f func(int) string, extended bool) map[string]int {
	m := make(map[string]int, len(ti.Nums)+len(ti.ExtNums))
	if !extended {
		for k, v := range ti.Nums {
			m[f(k)] = v
		}
	} else {
		for k, v := range ti.ExtNums {
			m[string(ti.ExtNumNames[k])] = v
		}
	}
	return m
}

// NumCaps returns all num capabilities.
func (ti *Terminfo) NumCaps() map[string]int {
	return ti.numCaps(NumCapName, false)
}

// NumCapsShort returns all num capabilities, using the short name as the
// index.
func (ti *Terminfo) NumCapsShort() map[string]int {
	return ti.numCaps(NumCapNameShort, false)
}

// ExtNumCaps returns all extended num capabilities.
func (ti *Terminfo) ExtNumCaps() map[string]int {
	return ti.numCaps(NumCapName, true)
}

// ExtNumCapsShort returns all extended num capabilities, using the short
// name as the index.
func (ti *Terminfo) ExtNumCapsShort() map[string]int {
	return ti.numCaps(NumCapNameShort, true)
}

// stringCaps returns all string and extended capabilities using f to format the
// index key.
func (ti *Terminfo) stringCaps(f func(int) string, extended bool) map[string][]byte {
	m := make(map[string][]byte, len(ti.Strings)+len(ti.ExtStrings))
	if !extended {
		for k, v := range ti.Strings {
			m[f(k)] = v
		}
	} else {
		for k, v := range ti.ExtStrings {
			m[string(ti.ExtStringNames[k])] = v
		}
	}
	return m
}

// StringCaps returns all string capabilities.
func (ti *Terminfo) StringCaps() map[string][]byte {
	return ti.stringCaps(StringCapName, false)
}

// StringCapsShort returns all string capabilities, using the short name as the
// index.
func (ti *Terminfo) StringCapsShort() map[string][]byte {
	return ti.stringCaps(StringCapNameShort, false)
}

// ExtStringCaps returns all extended string capabilities.
func (ti *Terminfo) ExtStringCaps() map[string][]byte {
	return ti.stringCaps(StringCapName, true)
}

// ExtStringCapsShort returns all extended string capabilities, using the short
// name as the index.
func (ti *Terminfo) ExtStringCapsShort() map[string][]byte {
	return ti.stringCaps(StringCapNameShort, true)
}

// Has determines if the bool cap i is present.
func (ti *Terminfo) Has(i int) bool {
	return ti.Bools[i]
}

// Num returns the num cap i, or -1 if not present.
func (ti *Terminfo) Num(i int) int {
	n, ok := ti.Nums[i]
	if !ok {
		return -1
	}
	return n
}

// Printf formats the string cap i, interpolating parameters v.
func (ti *Terminfo) Printf(i int, v ...interface{}) string {
	return Printf(ti.Strings[i], v...)
}

// Fprintf prints the string cap i to writer w, interpolating parameters v.
func (ti *Terminfo) Fprintf(w io.Writer, i int, v ...interface{}) {
	Fprintf(w, ti.Strings[i], v...)
}

// Color takes a foreground and background color and returns string that sets
// them for this terminal.
func (ti *Terminfo) Colorf(fg, bg int, str string) string {
	maxColors := int(ti.Nums[MaxColors])
	// map bright colors to lower versions if the color table only holds 8.
	if maxColors == 8 {
		if fg > 7 && fg < 16 {
			fg -= 8
		}
		if bg > 7 && bg < 16 {
			bg -= 8
		}
	}
	var s string
	if maxColors > fg && fg >= 0 {
		s += ti.Printf(SetAForeground, fg)
	}
	if maxColors > bg && bg >= 0 {
		s += ti.Printf(SetABackground, bg)
	}
	return s + str + ti.Printf(ExitAttributeMode)
}

// Goto 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 (ti *Terminfo) Goto(row, col int) string {
	return Printf(ti.Strings[CursorAddress], row, col)
}

// Puts emits the string to the writer, but expands inline padding indications
// (of the form $<[delay]> where [delay] is msec) to a suitable number of
// padding characters (usually null bytes) based upon the supplied baud. At
// high baud rates, more padding characters will be inserted.
/*func (ti *Terminfo) Puts(w io.Writer, s string, lines, baud int) (int, error) {
	var err error
	for {
		start := strings.Index(s, "$<")
		if start == -1 {
			// most strings don't need padding, which is good news!
			return io.WriteString(w, s)
		}
		end := strings.Index(s, ">")
		if end == -1 {
			// unterminated... just emit bytes unadulterated.
			return io.WriteString(w, "$<"+s)
		}
		var c int
		c, err = io.WriteString(w, s[:start])
		if err != nil {
			return n + c, err
		}
		n += c
		s = s[start+2:]
		val := s[:end]
		s = s[end+1:]
		var ms int
		var dot, mandatory, asterisk bool
		unit := 1000
		for _, ch := range val {
			switch {
			case ch >= '0' && ch <= '9':
				ms = (ms * 10) + int(ch-'0')
				if dot {
					unit *= 10
				}
			case ch == '.' && !dot:
				dot = true
			case ch == '*' && !asterisk:
				ms *= lines
				asterisk = true
			case ch == '/':
				mandatory = true
			default:
				break
			}
		}
		z, pad := ((baud/8)/unit)*ms, ti.Strings[PadChar]
		b := make([]byte, len(pad)*z)
		for bp := copy(b, pad); bp < len(b); bp *= 2 {
			copy(b[bp:], b[:bp])
		}
		if (!ti.Bools[XonXoff] && baud > int(ti.Nums[PaddingBaudRate])) || mandatory {
			c, err = w.Write(b)
			if err != nil {
				return n + c, err
			}
			n += c
		}
	}
	return n, nil
}*/
