//go:build !windows
// +build !windows

package keyboard

import (
	"errors"
	"os"
	"os/signal"
	"runtime"
	"strings"
	"unicode/utf8"

	"golang.org/x/sys/unix"
)

type (
	input_event struct {
		data []byte
		err  error
	}
)

var (
	out *os.File
	in  int

	// term specific keys
	keys []string

	// termbox inner state
	orig_tios unix.Termios

	sigio       = make(chan os.Signal, 1)
	quitEvProd  = make(chan bool)
	quitConsole = make(chan bool)
	inbuf       = make([]byte, 0, 128)
	input_buf   = make(chan input_event)
)

func parse_escape_sequence(buf []byte) (size int, event KeyEvent) {
	bufstr := string(buf)
	for i, key := range keys {
		if strings.HasPrefix(bufstr, key) {
			event.Rune = 0
			event.Key = Key(0xFFFF - i)
			size = len(key)
			return
		}
	}

	// Might be an Alt combo in format of ESC+letter
	if buf[0] == '\033' {
		event.Key = KeyEsc
		event.Rune, size = utf8.DecodeRune(buf[1:])
		size = len(buf)
		return
	}
	return 0, event
}

func extract_event(inbuf []byte) (int, KeyEvent) {
	if len(inbuf) == 0 {
		return 0, KeyEvent{}
	}

	if inbuf[0] == '\033' {
		if len(inbuf) == 1 {
			return 1, KeyEvent{Key: KeyEsc}
		}
		// possible escape sequence
		if size, event := parse_escape_sequence(inbuf); size != 0 {
			return size, event
		} else {
			// it's not a recognized escape sequence, return error
			i := 1 // check for multiple sequences in the buffer
			for ; i < len(inbuf) && inbuf[i] != '\033'; i++ {
			}
			return i, KeyEvent{Key: KeyEsc, Err: errors.New("Unrecognized escape sequence")}
		}
	}

	// 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 {
		return 1, KeyEvent{Key: Key(inbuf[0])}
	}

	// the only possible option is utf8 rune
	if r, n := utf8.DecodeRune(inbuf); r != utf8.RuneError {
		return n, KeyEvent{Rune: r}
	}

	return 0, KeyEvent{}
}

// Wait for an event and return it. This is a blocking function call.
func inputEventsProducer() {
	for {
		select {
		case <-quitEvProd:
			return
		case ev := <-input_buf:
			if ev.err != nil {
				select {
				case <-quitEvProd:
					return
				case inputComm <- KeyEvent{Err: ev.err}:
				}
				break
			}
			inbuf = append(inbuf, ev.data...)
			for {
				size, event := extract_event(inbuf)
				if size > 0 {
					select {
					case <-quitEvProd:
						return
					case inputComm <- event:
					}
					copy(inbuf, inbuf[size:])
					inbuf = inbuf[:len(inbuf)-size]
				}
				if size == 0 || len(inbuf) == 0 {
					break
				}
			}
		}
	}
}

func initConsole() (err error) {
	out, err = os.OpenFile("/dev/tty", unix.O_WRONLY, 0)
	if err != nil {
		return
	}
	in, err = unix.Open("/dev/tty", unix.O_RDONLY, 0)
	if err != nil {
		return
	}

	err = setup_term()
	if err != nil {
		return errors.New("Error while reading terminfo data:" + err.Error())
	}

	signal.Notify(sigio, unix.SIGIO)

	if _, err = unix.FcntlInt(uintptr(in), unix.F_SETFL, unix.O_ASYNC|unix.O_NONBLOCK); err != nil {
		return
	}
	_, err = unix.FcntlInt(uintptr(in), unix.F_SETOWN, unix.Getpid())
	if runtime.GOOS != "darwin" && err != nil {
		return
	}

	if err = unix.IoctlSetTermios(int(out.Fd()), ioctl_GETATTR, &orig_tios); err != nil {
		return
	}

	tios := orig_tios
	tios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK |
		unix.ISTRIP | unix.INLCR | unix.IGNCR |
		unix.ICRNL | unix.IXON
	tios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON |
		unix.ISIG | unix.IEXTEN
	tios.Cflag &^= unix.CSIZE | unix.PARENB
	tios.Cflag |= unix.CS8
	tios.Cc[unix.VMIN] = 1
	tios.Cc[unix.VTIME] = 0

	if err = unix.IoctlSetTermios(int(out.Fd()), ioctl_SETATTR, &tios); err != nil {
		return
	}

	go func() {
		buf := make([]byte, 128)
		for {
			select {
			case <-quitConsole:
				return
			case <-sigio:
				for {
					bytesRead, err := unix.Read(in, buf)
					if err == unix.EAGAIN || err == unix.EWOULDBLOCK {
						break
					}
					if err != nil {
						bytesRead = 0
					}
					data := make([]byte, bytesRead)
					copy(data, buf)
					select {
					case <-quitConsole:
						return
					case input_buf <- input_event{data, err}:
						continue
					}
				}
			}
		}
	}()

	go inputEventsProducer()
	return
}

func releaseConsole() {
	quitConsole <- true
	quitEvProd <- true
	unix.IoctlSetTermios(int(out.Fd()), ioctl_SETATTR, &orig_tios)
	out.Close()
	unix.Close(in)
}
