// +build !windows

package termbox

import "unicode/utf8"
import "bytes"
import "syscall"
import "unsafe"
import "strings"
import "strconv"
import "os"
import "io"

// private API

const (
	t_enter_ca = iota
	t_exit_ca
	t_show_cursor
	t_hide_cursor
	t_clear_screen
	t_sgr0
	t_underline
	t_bold
	t_blink
	t_reverse
	t_enter_keypad
	t_exit_keypad
	t_enter_mouse
	t_exit_mouse
	t_max_funcs
)

const (
	coord_invalid = -2
	attr_invalid  = Attribute(0xFFFF)
)

type input_event struct {
	data []byte
	err  error
}

var (
	// term specific sequences
	keys  []string
	funcs []string

	// termbox inner state
	orig_tios      syscall_Termios
	back_buffer    cellbuf
	front_buffer   cellbuf
	termw          int
	termh          int
	input_mode     = InputEsc
	output_mode    = OutputNormal
	out            *os.File
	in             int
	lastfg         = attr_invalid
	lastbg         = attr_invalid
	lastx          = coord_invalid
	lasty          = coord_invalid
	cursor_x       = cursor_hidden
	cursor_y       = cursor_hidden
	foreground     = ColorDefault
	background     = ColorDefault
	inbuf          = make([]byte, 0, 64)
	outbuf         bytes.Buffer
	sigwinch       = make(chan os.Signal, 1)
	sigio          = make(chan os.Signal, 1)
	quit           = make(chan int)
	input_comm     = make(chan input_event)
	interrupt_comm = make(chan struct{})
	intbuf         = make([]byte, 0, 16)
)

func write_cursor(x, y int) {
	outbuf.WriteString("\033[")
	outbuf.Write(strconv.AppendUint(intbuf, uint64(y+1), 10))
	outbuf.WriteString(";")
	outbuf.Write(strconv.AppendUint(intbuf, uint64(x+1), 10))
	outbuf.WriteString("H")
}

func write_sgr_fg(a Attribute) {
	switch output_mode {
	case Output256, Output216, OutputGrayscale:
		outbuf.WriteString("\033[38;5;")
		outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
		outbuf.WriteString("m")
	default:
		outbuf.WriteString("\033[3")
		outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
		outbuf.WriteString("m")
	}
}

func write_sgr_bg(a Attribute) {
	switch output_mode {
	case Output256, Output216, OutputGrayscale:
		outbuf.WriteString("\033[48;5;")
		outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
		outbuf.WriteString("m")
	default:
		outbuf.WriteString("\033[4")
		outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10))
		outbuf.WriteString("m")
	}
}

func write_sgr(fg, bg Attribute) {
	switch output_mode {
	case Output256, Output216, OutputGrayscale:
		outbuf.WriteString("\033[38;5;")
		outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10))
		outbuf.WriteString("m")
		outbuf.WriteString("\033[48;5;")
		outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10))
		outbuf.WriteString("m")
	default:
		outbuf.WriteString("\033[3")
		outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10))
		outbuf.WriteString(";4")
		outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10))
		outbuf.WriteString("m")
	}
}

type winsize struct {
	rows    uint16
	cols    uint16
	xpixels uint16
	ypixels uint16
}

func get_term_size(fd uintptr) (int, int) {
	var sz winsize
	_, _, _ = syscall.Syscall(syscall.SYS_IOCTL,
		fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz)))
	return int(sz.cols), int(sz.rows)
}

func send_attr(fg, bg Attribute) {
	if fg == lastfg && bg == lastbg {
		return
	}

	outbuf.WriteString(funcs[t_sgr0])

	var fgcol, bgcol Attribute

	switch output_mode {
	case Output256:
		fgcol = fg & 0x1FF
		bgcol = bg & 0x1FF
	case Output216:
		fgcol = fg & 0xFF
		bgcol = bg & 0xFF
		if fgcol > 216 {
			fgcol = ColorDefault
		}
		if bgcol > 216 {
			bgcol = ColorDefault
		}
		if fgcol != ColorDefault {
			fgcol += 0x10
		}
		if bgcol != ColorDefault {
			bgcol += 0x10
		}
	case OutputGrayscale:
		fgcol = fg & 0x1F
		bgcol = bg & 0x1F
		if fgcol > 24 {
			fgcol = ColorDefault
		}
		if bgcol > 24 {
			bgcol = ColorDefault
		}
		if fgcol != ColorDefault {
			fgcol += 0xe8
		}
		if bgcol != ColorDefault {
			bgcol += 0xe8
		}
	default:
		fgcol = fg & 0x0F
		bgcol = bg & 0x0F
	}

	if fgcol != ColorDefault {
		if bgcol != ColorDefault {
			write_sgr(fgcol, bgcol)
		} else {
			write_sgr_fg(fgcol)
		}
	} else if bgcol != ColorDefault {
		write_sgr_bg(bgcol)
	}

	if fg&AttrBold != 0 {
		outbuf.WriteString(funcs[t_bold])
	}
	if bg&AttrBold != 0 {
		outbuf.WriteString(funcs[t_blink])
	}
	if fg&AttrUnderline != 0 {
		outbuf.WriteString(funcs[t_underline])
	}
	if fg&AttrReverse|bg&AttrReverse != 0 {
		outbuf.WriteString(funcs[t_reverse])
	}

	lastfg, lastbg = fg, bg
}

func send_char(x, y int, ch rune) {
	var buf [8]byte
	n := utf8.EncodeRune(buf[:], ch)
	if x-1 != lastx || y != lasty {
		write_cursor(x, y)
	}
	lastx, lasty = x, y
	outbuf.Write(buf[:n])
}

func flush() error {
	_, err := io.Copy(out, &outbuf)
	outbuf.Reset()
	if err != nil {
		return err
	}
	return nil
}

func send_clear() error {
	send_attr(foreground, background)
	outbuf.WriteString(funcs[t_clear_screen])
	if !is_cursor_hidden(cursor_x, cursor_y) {
		write_cursor(cursor_x, cursor_y)
	}

	// we need to invalidate cursor position too and these two vars are
	// used only for simple cursor positioning optimization, cursor
	// actually may be in the correct place, but we simply discard
	// optimization once and it gives us simple solution for the case when
	// cursor moved
	lastx = coord_invalid
	lasty = coord_invalid

	return flush()
}

func update_size_maybe() error {
	w, h := get_term_size(out.Fd())
	if w != termw || h != termh {
		termw, termh = w, h
		back_buffer.resize(termw, termh)
		front_buffer.resize(termw, termh)
		front_buffer.clear()
		return send_clear()
	}
	return nil
}

func tcsetattr(fd uintptr, termios *syscall_Termios) error {
	r, _, e := syscall.Syscall(syscall.SYS_IOCTL,
		fd, uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(termios)))
	if r != 0 {
		return os.NewSyscallError("SYS_IOCTL", e)
	}
	return nil
}

func tcgetattr(fd uintptr, termios *syscall_Termios) error {
	r, _, e := syscall.Syscall(syscall.SYS_IOCTL,
		fd, uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(termios)))
	if r != 0 {
		return os.NewSyscallError("SYS_IOCTL", e)
	}
	return nil
}

func parse_escape_sequence(event *Event, buf []byte) (int, bool) {
	bufstr := string(buf)
	// mouse
	if len(bufstr) >= 6 && strings.HasPrefix(bufstr, "\033[M") {
		switch buf[3] & 3 {
		case 0:
			event.Key = MouseLeft
		case 1:
			event.Key = MouseMiddle
		case 2:
			event.Key = MouseRight
		case 3:
			return 6, false
		}
		event.Type = EventMouse // KeyEvent by default
		// wheel up outputs MouseLeft
		if buf[3] == 0x60 || buf[3] == 0x70 {
			event.Key = MouseMiddle
		}
		// the coord is 1,1 for upper left
		event.MouseX = int(buf[4]) - 1 - 32
		event.MouseY = int(buf[5]) - 1 - 32
		return 6, true
	}

	for i, key := range keys {
		if strings.HasPrefix(bufstr, key) {
			event.Ch = 0
			event.Key = Key(0xFFFF - i)
			return len(key), true
		}
	}
	return 0, true
}

func extract_raw_event(data []byte, event *Event) bool {
	if len(inbuf) == 0 {
		return false
	}

	n := len(data)
	if n == 0 {
		return false
	}

	n = copy(data, inbuf)
	copy(inbuf, inbuf[n:])
	inbuf = inbuf[:len(inbuf)-n]

	event.N = n
	event.Type = EventRaw
	return true
}

func extract_event(inbuf []byte, event *Event) bool {
	if len(inbuf) == 0 {
		event.N = 0
		return false
	}

	if inbuf[0] == '\033' {
		// possible escape sequence
		n, ok := parse_escape_sequence(event, inbuf)
		if n != 0 {
			event.N = n
			return ok
		}

		// it's not escape sequence, then it's Alt or Esc, check input_mode
		switch {
		case input_mode&InputEsc != 0:
			// if we're in escape mode, fill Esc event, pop buffer, return success
			event.Ch = 0
			event.Key = KeyEsc
			event.Mod = 0
			event.N = 1
			return true
		case input_mode&InputAlt != 0:
			// if we're in alt mode, set Alt modifier to event and redo parsing
			event.Mod = ModAlt
			ok := extract_event(inbuf[1:], event)
			if ok {
				event.N++
			} else {
				event.N = 0
			}
			return ok
		default:
			panic("unreachable")
		}
	}

	// if we're here, this is not an escape sequence and not an alt sequence
	// so, it's a FUNCTIONAL KEY or a UNICODE character

	// first of all check if it's a functional key
	if Key(inbuf[0]) <= KeySpace || Key(inbuf[0]) == KeyBackspace2 {
		// fill event, pop buffer, return success
		event.Ch = 0
		event.Key = Key(inbuf[0])
		event.N = 1
		return true
	}

	// the only possible option is utf8 rune
	if r, n := utf8.DecodeRune(inbuf); r != utf8.RuneError {
		event.Ch = r
		event.Key = 0
		event.N = n
		return true
	}

	return false
}

func fcntl(fd int, cmd int, arg int) (val int, err error) {
	r, _, e := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), uintptr(cmd),
		uintptr(arg))
	val = int(r)
	if e != 0 {
		err = e
	}
	return
}
