package gpgme

// #include <string.h>
// #include <gpgme.h>
// #include <errno.h>
// #include "go_gpgme.h"
import "C"

import (
	"io"
	"os"
	"runtime"
	"runtime/cgo"
	"unsafe"
)

const (
	SeekSet = C.SEEK_SET
	SeekCur = C.SEEK_CUR
	SeekEnd = C.SEEK_END
)

var dataCallbacks = C.struct_gpgme_data_cbs{
	read:  C.gpgme_data_read_cb_t(C.gogpgme_readfunc),
	write: C.gpgme_data_write_cb_t(C.gogpgme_writefunc),
	seek:  C.gpgme_data_seek_cb_t(C.gogpgme_seekfunc),
}

//export gogpgme_readfunc
func gogpgme_readfunc(handle, buffer unsafe.Pointer, size C.size_t) C.ssize_t {
	h := *(*cgo.Handle)(handle)
	d := h.Value().(*Data)
	n, err := d.r.Read(unsafe.Slice((*byte)(buffer), size))
	if err != nil && err != io.EOF {
		d.err = err
		C.gpgme_err_set_errno(C.EIO)
		return -1
	}
	return C.ssize_t(n)
}

//export gogpgme_writefunc
func gogpgme_writefunc(handle, buffer unsafe.Pointer, size C.size_t) C.ssize_t {
	h := *(*cgo.Handle)(handle)
	d := h.Value().(*Data)
	n, err := d.w.Write(unsafe.Slice((*byte)(buffer), size))
	if err != nil && err != io.EOF {
		d.err = err
		C.gpgme_err_set_errno(C.EIO)
		return -1
	}
	return C.ssize_t(n)
}

//export gogpgme_seekfunc
func gogpgme_seekfunc(handle unsafe.Pointer, offset C.gpgme_off_t, whence C.int) C.gpgme_off_t {
	h := *(*cgo.Handle)(handle)
	d := h.Value().(*Data)
	n, err := d.s.Seek(int64(offset), int(whence))
	if err != nil {
		d.err = err
		C.gpgme_err_set_errno(C.EIO)
		return -1
	}
	return C.gpgme_off_t(n)
}

// The Data buffer used to communicate with GPGME
type Data struct {
	dh  C.gpgme_data_t // WARNING: Call runtime.KeepAlive(d) after ANY passing of d.dh to C
	r   io.Reader
	w   io.Writer
	s   io.Seeker
	cbc cgo.Handle // WARNING: Call runtime.KeepAlive(d) after ANY use of d.cbc in C (typically via d.dh)
	err error
}

func newData() *Data {
	d := &Data{}
	runtime.SetFinalizer(d, (*Data).Close)
	return d
}

// NewData returns a new memory based data buffer
func NewData() (*Data, error) {
	d := newData()
	return d, handleError(C.gpgme_data_new(&d.dh))
}

// NewDataFile returns a new file based data buffer
func NewDataFile(f *os.File) (*Data, error) {
	d := newData()
	d.r = f
	return d, handleError(C.gpgme_data_new_from_fd(&d.dh, C.int(f.Fd())))
}

// NewDataBytes returns a new memory based data buffer that contains `b` bytes
func NewDataBytes(b []byte) (*Data, error) {
	d := newData()
	var cb *C.char
	if len(b) != 0 {
		cb = (*C.char)(unsafe.Pointer(&b[0]))
	}
	return d, handleError(C.gpgme_data_new_from_mem(&d.dh, cb, C.size_t(len(b)), 1))
}

// NewDataReader returns a new callback based data buffer
func NewDataReader(r io.Reader) (*Data, error) {
	d := newData()
	d.r = r
	if s, ok := r.(io.Seeker); ok {
		d.s = s
	}
	d.cbc = cgo.NewHandle(d)
	return d, handleError(C.gpgme_data_new_from_cbs(&d.dh, &dataCallbacks, unsafe.Pointer(&d.cbc)))
}

// NewDataWriter returns a new callback based data buffer
func NewDataWriter(w io.Writer) (*Data, error) {
	d := newData()
	d.w = w
	if s, ok := w.(io.Seeker); ok {
		d.s = s
	}
	d.cbc = cgo.NewHandle(d)
	return d, handleError(C.gpgme_data_new_from_cbs(&d.dh, &dataCallbacks, unsafe.Pointer(&d.cbc)))
}

// NewDataReadWriter returns a new callback based data buffer
func NewDataReadWriter(rw io.ReadWriter) (*Data, error) {
	d := newData()
	d.r = rw
	d.w = rw
	if s, ok := rw.(io.Seeker); ok {
		d.s = s
	}
	d.cbc = cgo.NewHandle(d)
	return d, handleError(C.gpgme_data_new_from_cbs(&d.dh, &dataCallbacks, unsafe.Pointer(&d.cbc)))
}

// NewDataReadWriteSeeker returns a new callback based data buffer
func NewDataReadWriteSeeker(rw io.ReadWriteSeeker) (*Data, error) {
	d := newData()
	d.r = rw
	d.w = rw
	d.s = rw
	d.cbc = cgo.NewHandle(d)
	return d, handleError(C.gpgme_data_new_from_cbs(&d.dh, &dataCallbacks, unsafe.Pointer(&d.cbc)))
}

// Close releases any resources associated with the data buffer
func (d *Data) Close() error {
	if d.dh == nil {
		return nil
	}
	if d.cbc > 0 {
		d.cbc.Delete()
	}
	C.gpgme_data_release(d.dh)
	runtime.KeepAlive(d)
	d.dh = nil
	return nil
}

func (d *Data) Write(p []byte) (int, error) {
	total := 0
	for total < len(p) {
		remaining := p[total:]

		var buffer *byte
		if len(remaining) > 0 {
			buffer = &remaining[0]
		}

		n, err := C.gpgme_data_write(d.dh, unsafe.Pointer(buffer), C.size_t(len(remaining)))
		runtime.KeepAlive(d)
		switch {
		case d.err != nil:
			defer func() { d.err = nil }()
			return total, d.err
		case n < 0:
			return total, err
		case n == 0: // This should never happen, but ensure we don’t loop forever
			// If we got here, we know len(p) > 0, so ErrShortWrite is appropriate.
			return total, io.ErrShortWrite
		}

		total += int(n)
	}
	return total, nil
}

func (d *Data) Read(p []byte) (int, error) {
	var buffer *byte
	if len(p) > 0 {
		buffer = &p[0]
	}

	n, err := C.gpgme_data_read(d.dh, unsafe.Pointer(buffer), C.size_t(len(p)))
	runtime.KeepAlive(d)
	switch {
	case d.err != nil:
		defer func() { d.err = nil }()

		return 0, d.err
	case n < 0:
		return 0, err
	case len(p) > 0 && n == 0:
		return 0, io.EOF
	}
	return int(n), nil
}

func (d *Data) Seek(offset int64, whence int) (int64, error) {
	n, err := C.gogpgme_data_seek(d.dh, C.gpgme_off_t(offset), C.int(whence))
	runtime.KeepAlive(d)
	switch {
	case d.err != nil:
		defer func() { d.err = nil }()

		return 0, d.err
	case n < 0:
		return 0, err
	}
	return int64(n), nil
}

// Name returns the associated filename if any
func (d *Data) Name() string {
	res := C.GoString(C.gpgme_data_get_file_name(d.dh))
	runtime.KeepAlive(d)
	return res
}
