package hcsshim

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"syscall"
	"time"

	"github.com/Sirupsen/logrus"
)

type ProcessError struct {
	Process   *process
	Operation string
	ExtraInfo string
	Err       error
}

type process struct {
	handle         hcsProcess
	processID      int
	container      *container
	cachedPipes    *cachedPipes
	callbackNumber uintptr
}

type cachedPipes struct {
	stdIn  syscall.Handle
	stdOut syscall.Handle
	stdErr syscall.Handle
}

type processModifyRequest struct {
	Operation   string
	ConsoleSize *consoleSize `json:",omitempty"`
	CloseHandle *closeHandle `json:",omitempty"`
}

type consoleSize struct {
	Height uint16
	Width  uint16
}

type closeHandle struct {
	Handle string
}

type processStatus struct {
	ProcessId      uint32
	Exited         bool
	ExitCode       uint32
	LastWaitResult int32
}

const (
	stdIn  string = "StdIn"
	stdOut string = "StdOut"
	stdErr string = "StdErr"
)

const (
	modifyConsoleSize string = "ConsoleSize"
	modifyCloseHandle string = "CloseHandle"
)

// Pid returns the process ID of the process within the container.
func (process *process) Pid() int {
	return process.processID
}

// Kill signals the process to terminate but does not wait for it to finish terminating.
func (process *process) Kill() error {
	operation := "Kill"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	var resultp *uint16
	err := hcsTerminateProcess(process.handle, &resultp)
	err = processHcsResult(err, resultp)
	if err == ErrVmcomputeOperationPending {
		return ErrVmcomputeOperationPending
	} else if err != nil {
		return makeProcessError(process, operation, "", err)
	}

	logrus.Debugf(title+" succeeded processid=%d", process.processID)
	return nil
}

// Wait waits for the process to exit.
func (process *process) Wait() error {
	operation := "Wait"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	if hcsCallbacksSupported {
		err := waitForNotification(process.callbackNumber, hcsNotificationProcessExited, nil)
		if err != nil {
			return makeProcessError(process, operation, "", err)
		}
	} else {
		_, err := process.waitTimeoutInternal(syscall.INFINITE)
		if err != nil {
			return makeProcessError(process, operation, "", err)
		}
	}

	logrus.Debugf(title+" succeeded processid=%d", process.processID)
	return nil
}

// WaitTimeout waits for the process to exit or the duration to elapse. It returns
// false if timeout occurs.
func (process *process) WaitTimeout(timeout time.Duration) error {
	operation := "WaitTimeout"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	if hcsCallbacksSupported {
		err := waitForNotification(process.callbackNumber, hcsNotificationProcessExited, &timeout)
		if err != nil {
			return makeProcessError(process, operation, "", err)
		}
	} else {
		finished, err := waitTimeoutHelper(process, timeout)
		if !finished {
			return ErrTimeout
		} else if err != nil {
			return makeProcessError(process, operation, "", err)
		}
	}

	logrus.Debugf(title+" succeeded processid=%d", process.processID)
	return nil
}

func (process *process) hcsWait(timeout uint32) (bool, error) {
	var (
		resultp   *uint16
		exitEvent syscall.Handle
	)
	err := hcsCreateProcessWait(process.handle, &exitEvent, &resultp)
	err = processHcsResult(err, resultp)
	if err != nil {
		return false, err
	}
	defer syscall.CloseHandle(exitEvent)

	return waitForSingleObject(exitEvent, timeout)
}

func (process *process) waitTimeoutInternal(timeout uint32) (bool, error) {
	return waitTimeoutInternalHelper(process, timeout)
}

// ExitCode returns the exit code of the process. The process must have
// already terminated.
func (process *process) ExitCode() (int, error) {
	operation := "ExitCode"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	properties, err := process.properties()
	if err != nil {
		return 0, makeProcessError(process, operation, "", err)
	}

	if properties.Exited == false {
		return 0, ErrInvalidProcessState
	}

	logrus.Debugf(title+" succeeded processid=%d exitCode=%d", process.processID, properties.ExitCode)
	return int(properties.ExitCode), nil
}

// ResizeConsole resizes the console of the process.
func (process *process) ResizeConsole(width, height uint16) error {
	operation := "ResizeConsole"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	modifyRequest := processModifyRequest{
		Operation: modifyConsoleSize,
		ConsoleSize: &consoleSize{
			Height: height,
			Width:  width,
		},
	}

	modifyRequestb, err := json.Marshal(modifyRequest)
	if err != nil {
		return err
	}

	modifyRequestStr := string(modifyRequestb)

	var resultp *uint16
	err = hcsModifyProcess(process.handle, modifyRequestStr, &resultp)
	err = processHcsResult(err, resultp)
	if err != nil {
		return makeProcessError(process, operation, "", err)
	}

	logrus.Debugf(title+" succeeded processid=%d", process.processID)
	return nil
}

func (process *process) properties() (*processStatus, error) {
	operation := "properties"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	var (
		resultp     *uint16
		propertiesp *uint16
	)
	err := hcsGetProcessProperties(process.handle, &propertiesp, &resultp)
	err = processHcsResult(err, resultp)
	if err != nil {
		return nil, makeProcessError(process, operation, "", err)
	}

	if propertiesp == nil {
		return nil, errors.New("Unexpected result from hcsGetProcessProperties, properties should never be nil")
	}
	propertiesRaw := convertAndFreeCoTaskMemBytes(propertiesp)

	properties := &processStatus{}
	if err := json.Unmarshal(propertiesRaw, properties); err != nil {
		return nil, err
	}

	logrus.Debugf(title+" succeeded processid=%d, properties=%s", process.processID, propertiesRaw)
	return properties, nil
}

// Stdio returns the stdin, stdout, and stderr pipes, respectively. Closing
// these pipes does not close the underlying pipes; it should be possible to
// call this multiple times to get multiple interfaces.
func (process *process) Stdio() (io.WriteCloser, io.ReadCloser, io.ReadCloser, error) {
	operation := "Stdio"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	var stdIn, stdOut, stdErr syscall.Handle

	if process.cachedPipes == nil {
		var (
			processInfo hcsProcessInformation
			resultp     *uint16
		)
		err := hcsGetProcessInfo(process.handle, &processInfo, &resultp)
		err = processHcsResult(err, resultp)
		if err != nil {
			return nil, nil, nil, makeProcessError(process, operation, "", err)
		}

		stdIn, stdOut, stdErr = processInfo.StdInput, processInfo.StdOutput, processInfo.StdError
	} else {
		// Use cached pipes
		stdIn, stdOut, stdErr = process.cachedPipes.stdIn, process.cachedPipes.stdOut, process.cachedPipes.stdErr

		// Invalidate the cache
		process.cachedPipes = nil
	}

	pipes, err := makeOpenFiles([]syscall.Handle{stdIn, stdOut, stdErr})
	if err != nil {
		return nil, nil, nil, err
	}

	logrus.Debugf(title+" succeeded processid=%d", process.processID)
	return pipes[0], pipes[1], pipes[2], nil
}

// CloseStdin closes the write side of the stdin pipe so that the process is
// notified on the read side that there is no more data in stdin.
func (process *process) CloseStdin() error {
	operation := "CloseStdin"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	modifyRequest := processModifyRequest{
		Operation: modifyCloseHandle,
		CloseHandle: &closeHandle{
			Handle: stdIn,
		},
	}

	modifyRequestb, err := json.Marshal(modifyRequest)
	if err != nil {
		return err
	}

	modifyRequestStr := string(modifyRequestb)

	var resultp *uint16
	err = hcsModifyProcess(process.handle, modifyRequestStr, &resultp)
	err = processHcsResult(err, resultp)
	if err != nil {
		return makeProcessError(process, operation, "", err)
	}

	logrus.Debugf(title+" succeeded processid=%d", process.processID)
	return nil
}

// Close cleans up any state associated with the process but does not kill
// or wait on it.
func (process *process) Close() error {
	operation := "Close"
	title := "HCSShim::Process::" + operation
	logrus.Debugf(title+" processid=%d", process.processID)

	// Don't double free this
	if process.handle == 0 {
		return nil
	}

	if hcsCallbacksSupported {
		if err := process.unregisterCallback(); err != nil {
			return makeProcessError(process, operation, "", err)
		}
	}

	if err := hcsCloseProcess(process.handle); err != nil {
		return makeProcessError(process, operation, "", err)
	}

	process.handle = 0

	logrus.Debugf(title+" succeeded processid=%d", process.processID)
	return nil
}

// closeProcess wraps process.Close for use by a finalizer
func closeProcess(process *process) {
	process.Close()
}

func (process *process) registerCallback() error {
	context := &notifcationWatcherContext{
		channels: newChannels(),
	}

	callbackMapLock.Lock()
	callbackNumber := nextCallback
	nextCallback++
	callbackMap[callbackNumber] = context
	callbackMapLock.Unlock()

	var callbackHandle hcsCallback
	err := hcsRegisterProcessCallback(process.handle, notificationWatcherCallback, callbackNumber, &callbackHandle)
	if err != nil {
		return err
	}
	context.handle = callbackHandle
	process.callbackNumber = callbackNumber

	return nil
}

func (process *process) unregisterCallback() error {
	callbackNumber := process.callbackNumber

	callbackMapLock.RLock()
	context := callbackMap[callbackNumber]
	callbackMapLock.RUnlock()

	if context == nil {
		return nil
	}

	handle := context.handle

	if handle == 0 {
		return nil
	}

	// hcsUnregisterProcessCallback has its own syncronization
	// to wait for all callbacks to complete. We must NOT hold the callbackMapLock.
	err := hcsUnregisterProcessCallback(handle)
	if err != nil {
		return err
	}

	closeChannels(context.channels)

	callbackMapLock.Lock()
	callbackMap[callbackNumber] = nil
	callbackMapLock.Unlock()

	handle = 0

	return nil
}

func (e *ProcessError) Error() string {
	if e == nil {
		return "<nil>"
	}

	if e.Process == nil {
		return "Unexpected nil process for error: " + e.Err.Error()
	}

	s := fmt.Sprintf("process %d", e.Process.processID)

	if e.Process.container != nil {
		s += " in container " + e.Process.container.id
	}

	if e.Operation != "" {
		s += " " + e.Operation
	}

	if e.Err != nil {
		s += fmt.Sprintf(" failed in Win32: %s (0x%x)", e.Err, win32FromError(e.Err))
	}

	return s
}

func makeProcessError(process *process, operation string, extraInfo string, err error) error {
	// Don't wrap errors created in hcsshim
	if err == ErrTimeout ||
		err == ErrUnexpectedProcessAbort ||
		err == ErrUnexpectedContainerExit ||
		err == ErrHandleClose ||
		err == ErrInvalidProcessState ||
		err == ErrInvalidNotificationType ||
		err == ErrVmcomputeOperationPending {
		return err
	}

	processError := &ProcessError{Process: process, Operation: operation, ExtraInfo: extraInfo, Err: err}
	logrus.Error(processError)
	return processError
}
