package libcontainer

import (
	"fmt"
	"os"
	"path/filepath"
	"syscall"
	"unsafe"

	"github.com/opencontainers/runc/libcontainer/label"
)

// NewConsole returns an initalized console that can be used within a container by copying bytes
// from the master side to the slave that is attached as the tty for the container's init process.
func NewConsole(uid, gid int) (Console, error) {
	master, err := os.OpenFile("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
	if err != nil {
		return nil, err
	}
	console, err := ptsname(master)
	if err != nil {
		return nil, err
	}
	if err := unlockpt(master); err != nil {
		return nil, err
	}
	if err := os.Chmod(console, 0600); err != nil {
		return nil, err
	}
	if err := os.Chown(console, uid, gid); err != nil {
		return nil, err
	}
	return &linuxConsole{
		slavePath: console,
		master:    master,
	}, nil
}

// newConsoleFromPath is an internal function returning an initialized console for use inside
// a container's MNT namespace.
func newConsoleFromPath(slavePath string) *linuxConsole {
	return &linuxConsole{
		slavePath: slavePath,
	}
}

// linuxConsole is a linux psuedo TTY for use within a container.
type linuxConsole struct {
	master    *os.File
	slavePath string
}

func (c *linuxConsole) Fd() uintptr {
	return c.master.Fd()
}

func (c *linuxConsole) Path() string {
	return c.slavePath
}

func (c *linuxConsole) Read(b []byte) (int, error) {
	return c.master.Read(b)
}

func (c *linuxConsole) Write(b []byte) (int, error) {
	return c.master.Write(b)
}

func (c *linuxConsole) Close() error {
	if m := c.master; m != nil {
		return m.Close()
	}
	return nil
}

// mount initializes the console inside the rootfs mounting with the specified mount label
// and applying the correct ownership of the console.
func (c *linuxConsole) mount(rootfs, mountLabel string) error {
	oldMask := syscall.Umask(0000)
	defer syscall.Umask(oldMask)
	if err := label.SetFileLabel(c.slavePath, mountLabel); err != nil {
		return err
	}
	dest := filepath.Join(rootfs, "/dev/console")
	f, err := os.Create(dest)
	if err != nil && !os.IsExist(err) {
		return err
	}
	if f != nil {
		f.Close()
	}
	return syscall.Mount(c.slavePath, dest, "bind", syscall.MS_BIND, "")
}

// dupStdio opens the slavePath for the console and dups the fds to the current
// processes stdio, fd 0,1,2.
func (c *linuxConsole) dupStdio() error {
	slave, err := c.open(syscall.O_RDWR)
	if err != nil {
		return err
	}
	fd := int(slave.Fd())
	for _, i := range []int{0, 1, 2} {
		if err := syscall.Dup3(fd, i, 0); err != nil {
			return err
		}
	}
	return nil
}

// open is a clone of os.OpenFile without the O_CLOEXEC used to open the pty slave.
func (c *linuxConsole) open(flag int) (*os.File, error) {
	r, e := syscall.Open(c.slavePath, flag, 0)
	if e != nil {
		return nil, &os.PathError{
			Op:   "open",
			Path: c.slavePath,
			Err:  e,
		}
	}
	return os.NewFile(uintptr(r), c.slavePath), nil
}

func ioctl(fd uintptr, flag, data uintptr) error {
	if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, flag, data); err != 0 {
		return err
	}
	return nil
}

// unlockpt unlocks the slave pseudoterminal device corresponding to the master pseudoterminal referred to by f.
// unlockpt should be called before opening the slave side of a pty.
func unlockpt(f *os.File) error {
	var u int32
	return ioctl(f.Fd(), syscall.TIOCSPTLCK, uintptr(unsafe.Pointer(&u)))
}

// ptsname retrieves the name of the first available pts for the given master.
func ptsname(f *os.File) (string, error) {
	var n int32
	if err := ioctl(f.Fd(), syscall.TIOCGPTN, uintptr(unsafe.Pointer(&n))); err != nil {
		return "", err
	}
	return fmt.Sprintf("/dev/pts/%d", n), nil
}
