// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package host

import (
	"sync"

	"gvisor.googlesource.com/gvisor/pkg/abi/linux"
	"gvisor.googlesource.com/gvisor/pkg/sentry/arch"
	"gvisor.googlesource.com/gvisor/pkg/sentry/context"
	"gvisor.googlesource.com/gvisor/pkg/sentry/fs"
	"gvisor.googlesource.com/gvisor/pkg/sentry/kernel"
	"gvisor.googlesource.com/gvisor/pkg/sentry/unimpl"
	"gvisor.googlesource.com/gvisor/pkg/sentry/usermem"
	"gvisor.googlesource.com/gvisor/pkg/syserror"
)

// TTYFileOperations implements fs.FileOperations for a host file descriptor
// that wraps a TTY FD.
//
// +stateify savable
type TTYFileOperations struct {
	fileOperations

	// mu protects the fields below.
	mu sync.Mutex `state:"nosave"`

	// FGProcessGroup is the foreground process group this TTY.  Will be
	// nil if not set or if this file has been released.
	fgProcessGroup *kernel.ProcessGroup
}

// newTTYFile returns a new fs.File that wraps a TTY FD.
func newTTYFile(ctx context.Context, dirent *fs.Dirent, flags fs.FileFlags, iops *inodeOperations) *fs.File {
	return fs.NewFile(ctx, dirent, flags, &TTYFileOperations{
		fileOperations: fileOperations{iops: iops},
	})
}

// ForegroundProcessGroup returns the foreground process for the TTY. This will
// be nil if the foreground process has not been set or if the file has been
// released.
func (t *TTYFileOperations) ForegroundProcessGroup() *kernel.ProcessGroup {
	t.mu.Lock()
	defer t.mu.Unlock()
	return t.fgProcessGroup
}

// Release implements fs.FileOperations.Release.
func (t *TTYFileOperations) Release() {
	t.mu.Lock()
	t.fgProcessGroup = nil
	t.mu.Unlock()

	t.fileOperations.Release()
}

// Ioctl implements fs.FileOperations.Ioctl.
func (t *TTYFileOperations) Ioctl(ctx context.Context, io usermem.IO, args arch.SyscallArguments) (uintptr, error) {
	// Ignore arg[0].  This is the real FD:
	fd := t.fileOperations.iops.fileState.FD()
	ioctl := args[1].Uint64()
	switch ioctl {
	case linux.TCGETS:
		termios, err := ioctlGetTermios(fd)
		if err != nil {
			return 0, err
		}
		_, err = usermem.CopyObjectOut(ctx, io, args[2].Pointer(), termios, usermem.IOOpts{
			AddressSpaceActive: true,
		})
		return 0, err

	case linux.TCSETS, linux.TCSETSW, linux.TCSETSF:
		var termios linux.Termios
		if _, err := usermem.CopyObjectIn(ctx, io, args[2].Pointer(), &termios, usermem.IOOpts{
			AddressSpaceActive: true,
		}); err != nil {
			return 0, err
		}
		err := ioctlSetTermios(fd, ioctl, &termios)
		return 0, err

	case linux.TIOCGPGRP:
		// Args: pid_t *argp
		// When successful, equivalent to *argp = tcgetpgrp(fd).
		// Get the process group ID of the foreground process group on
		// this terminal.

		t.mu.Lock()
		defer t.mu.Unlock()

		if t.fgProcessGroup == nil {
			// No process group has been set yet. Let's just lie
			// and tell it the process group from the current task.
			// The app is probably going to set it to something
			// else very soon anyways.
			t.fgProcessGroup = kernel.TaskFromContext(ctx).ThreadGroup().ProcessGroup()
		}

		// Map the ProcessGroup into a ProcessGroupID in the task's PID
		// namespace.
		pgID := kernel.TaskFromContext(ctx).ThreadGroup().PIDNamespace().IDOfProcessGroup(t.fgProcessGroup)
		_, err := usermem.CopyObjectOut(ctx, io, args[2].Pointer(), &pgID, usermem.IOOpts{
			AddressSpaceActive: true,
		})
		return 0, err

	case linux.TIOCSPGRP:
		// Args: const pid_t *argp
		// Equivalent to tcsetpgrp(fd, *argp).
		// Set the foreground process group ID of this terminal.

		var pgID kernel.ProcessGroupID
		if _, err := usermem.CopyObjectIn(ctx, io, args[2].Pointer(), &pgID, usermem.IOOpts{
			AddressSpaceActive: true,
		}); err != nil {
			return 0, err
		}

		// pgID must be non-negative.
		if pgID < 0 {
			return 0, syserror.EINVAL
		}

		// Process group with pgID must exist in this PID namespace.
		task := kernel.TaskFromContext(ctx)
		pidns := task.PIDNamespace()
		pg := pidns.ProcessGroupWithID(pgID)
		if pg == nil {
			return 0, syserror.ESRCH
		}

		// Process group must be in same session as calling task's
		// process group.
		curSession := task.ThreadGroup().ProcessGroup().Session()
		curSessionID := pidns.IDOfSession(curSession)
		if pidns.IDOfSession(pg.Session()) != curSessionID {
			return 0, syserror.EPERM
		}

		t.mu.Lock()
		t.fgProcessGroup = pg
		t.mu.Unlock()
		return 0, nil

	case linux.TIOCGWINSZ:
		// Args: struct winsize *argp
		// Get window size.
		winsize, err := ioctlGetWinsize(fd)
		if err != nil {
			return 0, err
		}
		_, err = usermem.CopyObjectOut(ctx, io, args[2].Pointer(), winsize, usermem.IOOpts{
			AddressSpaceActive: true,
		})
		return 0, err

	case linux.TIOCSWINSZ:
		// Args: const struct winsize *argp
		// Set window size.
		var winsize linux.Winsize
		if _, err := usermem.CopyObjectIn(ctx, io, args[2].Pointer(), &winsize, usermem.IOOpts{
			AddressSpaceActive: true,
		}); err != nil {
			return 0, err
		}
		err := ioctlSetWinsize(fd, &winsize)
		return 0, err

	// Unimplemented commands.
	case linux.TIOCSETD,
		linux.TIOCSBRK,
		linux.TIOCCBRK,
		linux.TCSBRK,
		linux.TCSBRKP,
		linux.TIOCSTI,
		linux.TIOCCONS,
		linux.FIONBIO,
		linux.TIOCEXCL,
		linux.TIOCNXCL,
		linux.TIOCGEXCL,
		linux.TIOCNOTTY,
		linux.TIOCSCTTY,
		linux.TIOCGSID,
		linux.TIOCGETD,
		linux.TIOCVHANGUP,
		linux.TIOCGDEV,
		linux.TIOCMGET,
		linux.TIOCMSET,
		linux.TIOCMBIC,
		linux.TIOCMBIS,
		linux.TIOCGICOUNT,
		linux.TCFLSH,
		linux.TIOCSSERIAL,
		linux.TIOCGPTPEER:

		unimpl.EmitUnimplementedEvent(ctx)
		fallthrough
	default:
		return 0, syserror.ENOTTY
	}
}