package main

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
	"sync"
	"unsafe"

	"github.com/anatol/booster/init/quirk"
	"golang.org/x/sys/unix"
)

func consoleSetFont(c *VirtualConsole) error {
	if c.FontFile == "" {
		info("setfont parameters are not specified")
		return nil
	}

	info("loading font file %s", c.FontFile)
	args := []string{c.FontFile}
	if c.FontMapFile != "" {
		args = append(args, "-m", c.FontMapFile)
	}
	if c.FontUnicodeFile != "" {
		args = append(args, "-u", c.FontUnicodeFile)
	}
	err := exec.Command("setfont", args...).Run()
	return unwrapExitError(err)
}

func loadKmap(fd uintptr, file string) error {
	blob, err := os.ReadFile(file)
	if err != nil {
		return err
	}

	curr := 0 // position of characters read from blob
	if !bytes.HasPrefix(blob, []byte("bkeymap")) {
		return fmt.Errorf("%s: is not a valid binary keymap", file)
	}
	curr = 7

	const (
		// from linux/kd.h
		KDGKBENT = 0x4B46 /* gets one entry in translation table */
		KDSKBENT = 0x4B47 /* sets one entry in translation table */

		NR_KEYS        = 128
		MAX_NR_KEYMAPS = 256
	)

	// load kmap
	keymaps := blob[curr : curr+MAX_NR_KEYMAPS]
	curr += MAX_NR_KEYMAPS

	for i, enabled := range keymaps {
		if enabled != 1 {
			continue
		}

		type kbEntry struct {
			kbTable uint8
			kbIndex uint8
			kbValue uint16
		}
		for j := 0; j < NR_KEYS; j++ {
			var ke kbEntry

			ke.kbTable = uint8(i)
			ke.kbIndex = uint8(j)
			ke.kbValue = *(*uint16)(unsafe.Pointer(&blob[curr]))
			curr += 2

			if err := ioctl(fd, KDSKBENT, uintptr(unsafe.Pointer(&ke))); err != nil {
				return err
			}
		}
	}

	return nil
}

func consoleLoadKeymap(c *VirtualConsole) error {
	if c.KeymapFile == "" {
		info("loadkey keymap is not specified")
		return nil
	}
	isUtf := c.Utf

	info("loading keymap file %s", c.KeymapFile)

	cons, err := os.OpenFile("/dev/tty0", os.O_RDWR, 0)
	if err != nil {
		return err
	}
	defer cons.Close()

	const (
		// from linux/kd.h
		KDSETMODE = 0x4B3A
		KDGETMODE = 0x4B3B

		K_RAW       = 0x00
		K_XLATE     = 0x01
		K_MEDIUMRAW = 0x02
		K_UNICODE   = 0x03
	)

	var mode int
	var ctrl string // refer 'man console_codes' for the control codes explanation
	if isUtf {
		mode = K_UNICODE
		ctrl = "\033%G"
		// stty -F ${dev} iutf8
	} else {
		mode = K_XLATE
		ctrl = "\033%@"
		// stty -F ${dev} -iutf8
	}
	// kbd_mode
	if err := unix.IoctlSetInt(int(cons.Fd()), KDSETMODE, mode); err != nil {
		return err
	}
	if _, err := cons.WriteString(ctrl); err != nil {
		return err
	}

	return loadKmap(cons.Fd(), c.KeymapFile)
}

func configureVirtualConsole() error {
	if c := config.VirtualConsole; c != nil {
		if err := consoleSetFont(c); err != nil {
			return err
		}
		if err := consoleLoadKeymap(c); err != nil {
			return err
		}
	}
	return nil
}

// readPasswordLine reads from reader until it finds \n or io.EOF.
// The slice returned does not include the \n.
// readPasswordLine also ignores any \r it finds.
// Windows uses \r as end of line. So, on Windows, readPasswordLine
// reads until it finds \r and ignores any \n it finds during processing.
func readPasswordLine(reader io.Reader) ([]byte, error) {
	var buf [1]byte
	var ret []byte

	for {
		n, err := reader.Read(buf[:])
		if n > 0 {
			switch buf[0] {
			case '\b':
				if len(ret) > 0 {
					ret = ret[:len(ret)-1]
				}
			case '\n':
				return ret, nil
			default:
				ret = append(ret, buf[0])
			}
			continue
		}
		if err != nil {
			if err == io.EOF && len(ret) > 0 {
				return ret, nil
			}
			return ret, err
		}
	}
}

var inputMutex sync.Mutex

func readPassword(prompt, postPrompt string) ([]byte, error) {
	inputMutex.Lock()
	defer inputMutex.Unlock()

	console(prompt)

	stdin := os.Stdin
	fd := int(stdin.Fd())

	termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
	if err != nil {
		return nil, err
	}

	newState := *termios
	newState.Lflag &^= unix.ECHO
	newState.Lflag |= unix.ICANON | unix.ISIG
	newState.Iflag |= unix.ICRNL
	if err := unix.IoctlSetTermios(fd, unix.TCSETS, &newState); err != nil {
		return nil, err
	}

	defer unix.IoctlSetTermios(fd, unix.TCSETS, termios)

	password, err := readPasswordLine(stdin)
	if postPrompt != "" {
		console(postPrompt)
	}
	if !quirk.TestEnabled {
		console("\n")
	}
	return password, err
}
