//  Copyright 2019 Google Inc. All Rights Reserved.
//
//  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 packages

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"sync"
	"syscall"
	"unsafe"

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

func ioctl(fd, req, arg uintptr) (err error) {
	_, _, e1 := unix.Syscall(unix.SYS_IOCTL, fd, req, arg)
	if e1 != 0 {
		err = syscall.Errno(e1)
	}
	return
}

// This is used for anytime we need to parse YUM output.
// See https://bugzilla.redhat.com/show_bug.cgi?id=584525#c21
// TODO: We should probably look into a thin python shim we can
// interact with that the utilizes the yum libraries.
func runWithPty(cmd *exec.Cmd) ([]byte, []byte, error) {
	// Much of this logic was taken from, without the CGO stuff:
	// https://golang.org/src/os/signal/signal_cgo_test.go

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

	// grantpt doesn't appear to be required anymore.

	// unlockpt
	var i int
	if err := ioctl(pty.Fd(), unix.TIOCSPTLCK, uintptr(unsafe.Pointer(&i))); err != nil {
		return nil, nil, fmt.Errorf("error from ioctl TIOCSPTLCK: %v", err)
	}

	// ptsname
	var u uint32
	if err := ioctl(pty.Fd(), unix.TIOCGPTN, uintptr(unsafe.Pointer(&u))); err != nil {
		return nil, nil, fmt.Errorf("error from ioctl TIOCGPTN: %v", err)
	}
	path := filepath.Join("/dev/pts", strconv.Itoa(int(u)))

	tty, err := os.OpenFile(path, os.O_RDWR|syscall.O_NOCTTY, 0)
	if err != nil {
		return nil, nil, err
	}
	defer tty.Close()

	if err := unix.IoctlSetWinsize(int(pty.Fd()), syscall.TIOCSWINSZ, &unix.Winsize{Row: 1, Col: 500}); err != nil {
		return nil, nil, fmt.Errorf("error from IoctlSetWinsize: %v", err)
	}

	var stderr bytes.Buffer
	cmd.Stdin = tty
	cmd.Stdout = tty
	cmd.Stderr = &stderr
	cmd.SysProcAttr = &syscall.SysProcAttr{
		Setctty: true,
		Setsid:  true,
		Ctty:    0, // Stdin of the child process
	}

	var stdout bytes.Buffer
	var retErr error
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		input := bufio.NewReader(pty)
		for {
			b, err := input.ReadBytes('\n')
			if err != nil {
				if perr, ok := err.(*os.PathError); ok {
					err = perr.Err
				}
				if err != io.EOF && err != syscall.EIO {
					retErr = err
				}
				return
			}

			if _, err := stdout.Write(b); err != nil {
				retErr = err
				return
			}
		}
	}()

	cmdErr := cmd.Run()

	if err := tty.Close(); err != nil {
		return stdout.Bytes(), stderr.Bytes(), err

	}
	wg.Wait()

	if cmdErr != nil {
		// Yum returns non-zero exit values on non-error conditions.
		// Errors which are *not* in this category indicate failure.
		if _, ok := cmdErr.(*exec.ExitError); !ok {
			return stdout.Bytes(), stderr.Bytes(), cmdErr

		}
	}

	// Exit code 0 means no updates, 1 probably means there are but we just didn't install them.
	if cmdErr == nil {
		return nil, nil, nil
	}
	return stdout.Bytes(), stderr.Bytes(), retErr
}
