package remotefs

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"io"
	"io/ioutil"
	"os"
	"syscall"

	"github.com/docker/docker/pkg/archive"
)

// ReadError is an utility function that reads a serialized error from the given reader
// and deserializes it.
func ReadError(in io.Reader) (*ExportedError, error) {
	b, err := ioutil.ReadAll(in)
	if err != nil {
		return nil, err
	}

	// No error
	if len(b) == 0 {
		return nil, nil
	}

	var exportedErr ExportedError
	if err := json.Unmarshal(b, &exportedErr); err != nil {
		return nil, err
	}

	return &exportedErr, nil
}

// ExportedToError will convert a ExportedError to an error. It will try to match
// the error to any existing known error like os.ErrNotExist. Otherwise, it will just
// return an implementation of the error interface.
func ExportedToError(ee *ExportedError) error {
	if ee.Error() == os.ErrNotExist.Error() {
		return os.ErrNotExist
	} else if ee.Error() == os.ErrExist.Error() {
		return os.ErrExist
	} else if ee.Error() == os.ErrPermission.Error() {
		return os.ErrPermission
	} else if ee.Error() == io.EOF.Error() {
		return io.EOF
	}
	return ee
}

// WriteError is an utility function that serializes the error
// and writes it to the output writer.
func WriteError(err error, out io.Writer) error {
	if err == nil {
		return nil
	}
	err = fixOSError(err)

	var errno int
	switch typedError := err.(type) {
	case *os.PathError:
		if se, ok := typedError.Err.(syscall.Errno); ok {
			errno = int(se)
		}
	case *os.LinkError:
		if se, ok := typedError.Err.(syscall.Errno); ok {
			errno = int(se)
		}
	case *os.SyscallError:
		if se, ok := typedError.Err.(syscall.Errno); ok {
			errno = int(se)
		}
	}

	exportedError := &ExportedError{
		ErrString: err.Error(),
		ErrNum:    errno,
	}

	b, err1 := json.Marshal(exportedError)
	if err1 != nil {
		return err1
	}

	_, err1 = out.Write(b)
	if err1 != nil {
		return err1
	}
	return nil
}

// fixOSError converts possible platform dependent error into the portable errors in the
// Go os package if possible.
func fixOSError(err error) error {
	// The os.IsExist, os.IsNotExist, and os.IsPermissions functions are platform
	// dependent, so sending the raw error might break those functions on a different OS.
	// Go defines portable errors for these.
	if os.IsExist(err) {
		return os.ErrExist
	} else if os.IsNotExist(err) {
		return os.ErrNotExist
	} else if os.IsPermission(err) {
		return os.ErrPermission
	}
	return err
}

// ReadTarOptions reads from the specified reader and deserializes an archive.TarOptions struct.
func ReadTarOptions(r io.Reader) (*archive.TarOptions, error) {
	var size uint64
	if err := binary.Read(r, binary.BigEndian, &size); err != nil {
		return nil, err
	}

	rawJSON := make([]byte, size)
	if _, err := io.ReadFull(r, rawJSON); err != nil {
		return nil, err
	}

	var opts archive.TarOptions
	if err := json.Unmarshal(rawJSON, &opts); err != nil {
		return nil, err
	}
	return &opts, nil
}

// WriteTarOptions serializes a archive.TarOptions struct and writes it to the writer.
func WriteTarOptions(w io.Writer, opts *archive.TarOptions) error {
	optsBuf, err := json.Marshal(opts)
	if err != nil {
		return err
	}

	optsSize := uint64(len(optsBuf))
	optsSizeBuf := &bytes.Buffer{}
	if err := binary.Write(optsSizeBuf, binary.BigEndian, optsSize); err != nil {
		return err
	}

	if _, err := optsSizeBuf.WriteTo(w); err != nil {
		return err
	}

	if _, err := w.Write(optsBuf); err != nil {
		return err
	}

	return nil
}

// ReadFileHeader reads from r and returns a deserialized FileHeader
func ReadFileHeader(r io.Reader) (*FileHeader, error) {
	hdr := &FileHeader{}
	if err := binary.Read(r, binary.BigEndian, hdr); err != nil {
		return nil, err
	}
	return hdr, nil
}

// WriteFileHeader serializes a FileHeader and writes it to w, along with any extra data
func WriteFileHeader(w io.Writer, hdr *FileHeader, extraData []byte) error {
	if err := binary.Write(w, binary.BigEndian, hdr); err != nil {
		return err
	}
	if _, err := w.Write(extraData); err != nil {
		return err
	}
	return nil
}
