// +build windows

package lcow

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"os"
	"strconv"

	"github.com/Microsoft/hcsshim"
	"github.com/Microsoft/opengcs/service/gcsutils/remotefs"
	"github.com/containerd/continuity/driver"
)

type lcowfile struct {
	process   hcsshim.Process
	stdin     io.WriteCloser
	stdout    io.ReadCloser
	stderr    io.ReadCloser
	fs        *lcowfs
	guestPath string
}

func (l *lcowfs) Open(path string) (driver.File, error) {
	return l.OpenFile(path, os.O_RDONLY, 0)
}

func (l *lcowfs) OpenFile(path string, flag int, perm os.FileMode) (_ driver.File, err error) {
	flagStr := strconv.FormatInt(int64(flag), 10)
	permStr := strconv.FormatUint(uint64(perm), 8)

	commandLine := fmt.Sprintf("%s %s %s %s %s", remotefs.RemotefsCmd, remotefs.OpenFileCmd, path, flagStr, permStr)
	env := make(map[string]string)
	env["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:"
	processConfig := &hcsshim.ProcessConfig{
		EmulateConsole:    false,
		CreateStdInPipe:   true,
		CreateStdOutPipe:  true,
		CreateStdErrPipe:  true,
		CreateInUtilityVm: true,
		WorkingDirectory:  "/bin",
		Environment:       env,
		CommandLine:       commandLine,
	}

	process, err := l.currentSVM.config.Uvm.CreateProcess(processConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to open file %s: %s", path, err)
	}

	stdin, stdout, stderr, err := process.Stdio()
	if err != nil {
		process.Kill()
		process.Close()
		return nil, fmt.Errorf("failed to open file pipes %s: %s", path, err)
	}

	lf := &lcowfile{
		process:   process,
		stdin:     stdin,
		stdout:    stdout,
		stderr:    stderr,
		fs:        l,
		guestPath: path,
	}

	if _, err := lf.getResponse(); err != nil {
		return nil, fmt.Errorf("failed to open file %s: %s", path, err)
	}
	return lf, nil
}

func (l *lcowfile) Read(b []byte) (int, error) {
	hdr := &remotefs.FileHeader{
		Cmd:  remotefs.Read,
		Size: uint64(len(b)),
	}

	if err := remotefs.WriteFileHeader(l.stdin, hdr, nil); err != nil {
		return 0, err
	}

	buf, err := l.getResponse()
	if err != nil {
		return 0, err
	}

	n := copy(b, buf)
	return n, nil
}

func (l *lcowfile) Write(b []byte) (int, error) {
	hdr := &remotefs.FileHeader{
		Cmd:  remotefs.Write,
		Size: uint64(len(b)),
	}

	if err := remotefs.WriteFileHeader(l.stdin, hdr, b); err != nil {
		return 0, err
	}

	_, err := l.getResponse()
	if err != nil {
		return 0, err
	}

	return len(b), nil
}

func (l *lcowfile) Seek(offset int64, whence int) (int64, error) {
	seekHdr := &remotefs.SeekHeader{
		Offset: offset,
		Whence: int32(whence),
	}

	buf := &bytes.Buffer{}
	if err := binary.Write(buf, binary.BigEndian, seekHdr); err != nil {
		return 0, err
	}

	hdr := &remotefs.FileHeader{
		Cmd:  remotefs.Write,
		Size: uint64(buf.Len()),
	}
	if err := remotefs.WriteFileHeader(l.stdin, hdr, buf.Bytes()); err != nil {
		return 0, err
	}

	resBuf, err := l.getResponse()
	if err != nil {
		return 0, err
	}

	var res int64
	if err := binary.Read(bytes.NewBuffer(resBuf), binary.BigEndian, &res); err != nil {
		return 0, err
	}
	return res, nil
}

func (l *lcowfile) Close() error {
	hdr := &remotefs.FileHeader{
		Cmd:  remotefs.Close,
		Size: 0,
	}

	if err := remotefs.WriteFileHeader(l.stdin, hdr, nil); err != nil {
		return err
	}

	_, err := l.getResponse()
	return err
}

func (l *lcowfile) Readdir(n int) ([]os.FileInfo, error) {
	nStr := strconv.FormatInt(int64(n), 10)

	// Unlike the other File functions, this one can just be run without maintaining state,
	// so just do the normal runRemoteFSProcess way.
	buf := &bytes.Buffer{}
	if err := l.fs.runRemoteFSProcess(nil, buf, remotefs.ReadDirCmd, l.guestPath, nStr); err != nil {
		return nil, err
	}

	var info []remotefs.FileInfo
	if err := json.Unmarshal(buf.Bytes(), &info); err != nil {
		return nil, err
	}

	osInfo := make([]os.FileInfo, len(info))
	for i := range info {
		osInfo[i] = &info[i]
	}
	return osInfo, nil
}

func (l *lcowfile) getResponse() ([]byte, error) {
	hdr, err := remotefs.ReadFileHeader(l.stdout)
	if err != nil {
		return nil, err
	}

	if hdr.Cmd != remotefs.CmdOK {
		// Something went wrong during the openfile in the server.
		// Parse stderr and return that as an error
		eerr, err := remotefs.ReadError(l.stderr)
		if eerr != nil {
			return nil, remotefs.ExportedToError(eerr)
		}

		// Maybe the parsing went wrong?
		if err != nil {
			return nil, err
		}

		// At this point, we know something went wrong in the remotefs program, but
		// we we don't know why.
		return nil, fmt.Errorf("unknown error")
	}

	// Successful command, we might have some data to read (for Read + Seek)
	buf := make([]byte, hdr.Size, hdr.Size)
	if _, err := io.ReadFull(l.stdout, buf); err != nil {
		return nil, err
	}
	return buf, nil
}