package main

import (
	"errors"
	"fmt"
	"log"
	"strconv"
	"strings"

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

type CommandHandler struct {
	shell     *Shell
	filestore Filestore
}

func NewCommandHandler(shell *Shell, filestore Filestore) *CommandHandler {
	return &CommandHandler{
		shell:     shell,
		filestore: filestore,
	}
}

func Control(c rune) rune {
	return c & 0x1f
}

func sttyCharFromDesc(c string) (rune, error) {
	if len(c) > 2 || len(c) < 1 {
		return 'i', errors.New(
			"stty char description has unsupported length: " +
				strconv.Itoa(len(c)),
		)
	}

	if rune(c[0]) == '^' {
		return Control(rune(c[1])), nil
	}

	return rune(c[0]), nil
}

const fakeUname = "Linux 5.16.19-76051619-generic"

type CommandExecutor interface {
	Execute(shell *Shell, args []string) error
}

type SttyCommand struct{}

func (c *SttyCommand) Execute(shell *Shell, args []string) error {
	// Tramp uses:
	// -inlcr: don't translate newline to carriage return
	// -onlcr: don't translate newline to carriage return-newline
	// -echo: echo input characters
	// kill CHAR: CHAR will erase the current line
	// erase CHAR: CHAR will erase the last character typed
	// icanon: enable special charactersq

	fd := int(shell.GetPty().Fd())
	const ioctlReadTermios = unix.TCGETS  // Linux
	const ioctlWriteTermios = unix.TCSETS // Linux

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

	newState := *termios

	var skipNext bool
	for i, arg := range args {
		if skipNext {
			skipNext = false
			continue
		}

		var disable bool
		if arg[0] == '-' {
			disable = true
			arg = arg[1:]
		}

		switch arg {
		case "onlcr":
			if disable {
				newState.Iflag &^= unix.ONLCR
				continue
			}

			newState.Iflag |= unix.ONLCR
		case "inlcr":
			if disable {
				newState.Iflag &^= unix.INLCR
				continue
			}

			newState.Iflag |= unix.INLCR
		case "echo":
			if disable {
				newState.Lflag &^= unix.ECHO
				continue
			}

			newState.Lflag |= unix.ECHO
		case "kill":
			skipNext = true

			char, err := sttyCharFromDesc(args[i+1])
			if err != nil {
				log.Printf(
					"Warning: Not applying unsupported character description for stty kill: %s",
					args[i+1],
				)
				continue
			}

			newState.Cc[unix.VKILL] = uint8(char)
		case "erase":
			skipNext = true

			char, err := sttyCharFromDesc(args[i+1])
			if err != nil {
				log.Printf(
					"Warning: Not applying unsupported character description for stty erase: %s",
					args[i+1],
				)
				continue
			}

			newState.Cc[unix.VERASE] = uint8(char)
		case "icanon":
			if disable {
				newState.Lflag &^= unix.ICANON
				continue
			}

			newState.Lflag |= unix.ICANON

		case "tab0":
			newState.Oflag = (newState.Oflag & ^uint32(unix.TABDLY)) | unix.TAB0
		case "iutf8":
			if disable {
				newState.Iflag &^= unix.IUTF8
				continue
			}

			newState.Iflag |= unix.IUTF8

		default:
			log.Printf(
				"Warning, tramp requested unexpected stty option %s",
				arg,
			)
		}
	}

	if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newState); err != nil {
		return err
	}

	return nil
}

type EchoCommand struct{}

func (c *EchoCommand) Execute(shell *Shell, args []string) error {
	if len(args) > 1 {
		if "\"`uname -sr`\"" == args[0]+" "+args[1] {
			log.Println("Tramp requested uname, returning a fake one: " + fakeUname)
			_, err := shell.WriteOutput(
				[]byte(
					fmt.Sprintf(
						"\"%s\"\n%s\n",
						fakeUname,
						"tramp_exit_status 0",
					),
				),
			)
			return err
		}

		if "\"`tty`\"" == args[0] {
			log.Println("Telling tramp that it's dealing with a tty")
			_, err := shell.WriteOutputString("\"/dev/pts/0\"\n" + trampSuccess)

			return err
		}
	}

	if len(args) > 0 {
		if args[0] == "~root" {
			log.Println("Telling tramp root's home directory")
			_, err := shell.WriteOutputString("/root")
			return err
		}
	}

	_, err := shell.WriteOutput([]byte(strings.Join(args, " ") + "\n"))
	return err
}

func AssignmentsToMap(args []string) map[string]string {
	var assignments = make(map[string]string)
	for _, arg := range args {
		split := strings.Split(arg, "=")
		if len(split) > 1 {
			assignments[split[0]] = split[1]
		}
	}

	return assignments
}

func ApplyEnvIfPresent(shell *Shell, comm *Command) {
	var args []string
	args = append(args, comm.Name)
	args = append(args, comm.Arguments...)

	assignments := AssignmentsToMap(args)
	if ps1, ok := assignments["PS1"]; ok {
		shell.SetPrompt(ps1)
	}
}

type TestCommand struct {
	filestore Filestore
}

func (c *TestCommand) Execute(shell *Shell, args []string) error {
	if len(args) > 0 && args[0] == "0" {
		_, err := shell.WriteOutputString(trampSuccess)
		return err
	}

	if len(args) > 1 && args[0] == "-d" {
		log.Printf("Checking for existance of directory %s", args[1])
		exists, err := c.filestore.Exists(args[1])
		if err != nil {
			return err
		}

		if exists {
			_, err := shell.WriteOutputString(trampSuccess)
			return err
		}

		_, err = shell.WriteOutputString(trampFailure)
		return err
	}

	if len(args) > 1 && args[0] == "-r" {
		_, err := shell.WriteOutputString(trampSuccess)
		return err
	}

	return nil
}

const fakePath = "/bin:/usr/bin"
const fakePipeBuf = "4096"
const trampSuccess = "tramp_exit_status 0\n"
const trampFailure = "tramp_exit_status 1\n"

type GetConfCommand struct{}

func (c *GetConfCommand) Execute(shell *Shell, args []string) error {
	if len(args) > 0 {
		switch args[0] {
		case "PATH":
			shell.WriteOutputString(fakePath + "\n" + trampSuccess)
		case "PIPE_BUF":
			shell.WriteOutputString(fakePipeBuf + "\n" + trampSuccess)
		}
	}
	return nil
}

func (h *CommandHandler) Handle(comm *Command) error {
	if strings.Contains(comm.Name, "=") {
		ApplyEnvIfPresent(h.shell, comm)
		return nil
	}

	switch comm.Name {
	case "exec":
		ApplyEnvIfPresent(h.shell, comm)
	case "(cd":
		h.shell.WriteOutput([]byte("tramp_exit_status 0\n"))
	case "echo":
		(&EchoCommand{}).Execute(h.shell, comm.Arguments)
	case "(echo":
		if strings.Join(comm.Arguments, " ") == "foo ; echo bar)" {
			log.Println("Handling tramp's foobar test")
			h.shell.WriteOutputString("foo\nbar\n")
		}
	case "stty":
		return (&SttyCommand{}).Execute(h.shell, comm.Arguments)
	case "set":
		log.Println("Ignoring \"set\" command")
	case "locale":
		if len(comm.Arguments) > 0 {
			if comm.Arguments[0] == "-a" {
				locales := []string{"C", "C.UTF-8", "POSIX", "en_US.utf8"}
				log.Println("Tramp requested locale, returning fake values: ", locales)
				h.shell.WriteOutputString(
					strings.Join(locales, "\n") + "\n",
				)
				return nil
			}
		}

		log.Println("Ignoring \"locale\" command with unsupported parameters: ", comm.Arguments)
	case "getconf":
		return (&GetConfCommand{}).Execute(h.shell, comm.Arguments)
	case "test":
		return (&TestCommand{filestore: h.filestore}).Execute(h.shell, comm.Arguments)
	default:
		log.Printf("Error: Received unexpected command %s", comm.Name)
	}

	return nil
}
