package possumC

/*
// I think we trigger static linking on Linux because we don't want to have to specify the path to
// the library at runtime. I can't seem to get CGO on macOS to do the same. I can't get a reliable
// Windows system to get to the point I can actually build this, so I don't know how it works there.
// Also cgo directives seem to use the old build constraint syntax. Why the fuck isn't this stuff
// documented?
#cgo !windows,!darwin LDFLAGS: -l:libpossum.a -lm
#cgo darwin windows LDFLAGS: -lpossum
#include "possum.h"
*/
import "C"
import (
	"errors"
	"fmt"
	"io/fs"
	"math"
	"runtime"
	"time"
	"unsafe"

	"github.com/anacrolix/generics"
)

var NoSuchKey = Error{
	pec: C.NoSuchKey,
}

type Error struct {
	pec             C.PossumError
	displayGoesHere string
}

const cErrorEnumNoSuchKey = C.NoSuchKey

func (me Error) Is(err error) bool {
	if err == NoSuchKey || err == fs.ErrNotExist {
		return me.pec == cErrorEnumNoSuchKey
	}
	return false
}

func (me Error) Error() string {
	return fmt.Sprintf("possum error code %v", me.pec)
}

func mapError(err C.PossumError) error {
	if err == C.NoError {
		return nil
	}
	return Error{pec: err}
}

type Stat C.PossumStat

func (me Stat) LastUsed() time.Time {
	ts := me.last_used
	return time.Unix(int64(ts.secs), int64(ts.nanos))
}

func (me Stat) Size() int64 {
	return int64(me.size)
}

func (me *Stat) AsCPtr() *C.PossumStat {
	return (*C.PossumStat)(unsafe.Pointer(me))
}

type Handle = C.PossumHandle

func NewHandle(dir string) *Handle {
	cDir := C.CString(dir)
	defer C.free(unsafe.Pointer(cDir))
	handle := C.possum_new(cDir)
	return handle
}

func DropHandle(handle *Handle) {
	C.possum_drop(handle)
}

func SingleDelete(handle *Handle, key string) (opt generics.Option[Stat], err error) {
	pe := C.possum_single_delete(handle, BufFromString(key), opt.Value.AsCPtr())
	switch pe {
	case C.NoError:
		opt.Ok = true
	case C.NoSuchKey:
	default:
		err = mapError(pe)
	}
	return
}

func SingleStat(handle *Handle, key string) (opt generics.Option[Stat]) {
	opt.Ok = bool(C.possum_single_stat(
		handle,
		BufFromString(key),
		opt.Value.AsCPtr(),
	))
	return
}

func WriteSingleBuf(handle *Handle, key string, buf []byte) (written uint, err error) {
	written = uint(C.possum_single_write_buf(
		handle,
		BufFromString(key),
		BufFromBytes(buf),
	))
	if written == math.MaxUint {
		err = errors.New("unknown possum error")
	}
	return
}

func goListItems(items *C.PossumItem, itemsLen C.size_t) (goItems []Item) {
	itemsSlice := unsafe.Slice(items, uint(itemsLen))
	goItems = make([]Item, itemsLen)
	for i, from := range itemsSlice {
		to := &goItems[i]
		to.Key = C.GoStringN(
			(*C.char)(from.key.ptr),
			C.int(from.key.size),
		)
		C.free(unsafe.Pointer(from.key.ptr))
		to.Stat = Stat(from.stat)
	}
	C.free(unsafe.Pointer(items))
	return
}

func HandleListItems(handle *Handle, prefix string) (items []Item, err error) {
	var cItems *C.PossumItem
	var itemsLen C.size_t
	err = mapError(C.possum_list_items(
		handle,
		BufFromString(prefix),
		&cItems, &itemsLen))
	if err != nil {
		return
	}
	items = goListItems(cItems, itemsLen)
	return
}

func SingleReadAt(handle *Handle, key string, buf []byte, offset uint64) (n int, err error) {
	var pinner runtime.Pinner
	defer pinner.Unpin()
	pBuf := BufFromBytes(buf)
	pinner.Pin(pBuf.ptr)
	err = mapError(C.possum_single_read_at(
		handle,
		BufFromString(key),
		&pBuf,
		C.uint64_t(offset),
	))
	n = int(pBuf.size)
	return
}

type Reader = *C.PossumReader

func NewReader(handle *Handle) (r Reader, err error) {
	err = mapError(C.possum_reader_new(handle, &r))
	return
}

func BufFromString(s string) C.PossumBuf {
	return C.PossumBuf{
		(*C.char)(unsafe.Pointer(unsafe.StringData(s))),
		C.size_t(len(s)),
	}
}

func BufFromBytes(b []byte) C.PossumBuf {
	return C.PossumBuf{
		(*C.char)(unsafe.Pointer(unsafe.SliceData(b))),
		C.size_t(len(b)),
	}
}

func ReaderAdd(r Reader, key string) (v Value, err error) {
	err = mapError(C.possum_reader_add(r, BufFromString(key), &v))
	return
}

func ReaderBegin(r Reader) error {
	return mapError(C.possum_reader_begin(r))
}

func ReaderEnd(r Reader) {
	C.possum_reader_end(r)
}

func ReaderListItems(r Reader, prefix string) (items []Item, err error) {
	var cItems *C.PossumItem
	var itemsLen C.size_t
	err = mapError(C.possum_reader_list_items(r, BufFromString(prefix), &cItems, &itemsLen))
	if err != nil {
		return
	}
	items = goListItems(cItems, itemsLen)
	return
}

type Value = *C.PossumValue

func ValueReadAt(v Value, buf []byte, offset int64) (n int, err error) {
	pBuf := BufFromBytes(buf)
	var pin runtime.Pinner
	defer pin.Unpin()
	pin.Pin(pBuf.ptr)
	err = mapError(C.possum_value_read_at(v, &pBuf, C.uint64_t(offset)))
	n = int(pBuf.size)
	return
}

func ValueStat(v Value) (ret Stat) {
	C.possum_value_stat(v, ret.AsCPtr())
	return
}

type Item struct {
	Key string
	Stat
}

type Limits struct {
	MaxValueLengthSum   generics.Option[uint64]
	DisableHolePunching bool
}

func CleanupSnapshots(h *Handle) error {
	return mapError(C.possum_cleanup_snapshots(h))
}

func SetInstanceLimits(h *Handle, limits Limits) error {
	var cLimits C.PossumLimits
	cLimits.max_value_length_sum = C.uint64_t(limits.MaxValueLengthSum.UnwrapOr(math.MaxUint64))
	cLimits.disable_hole_punching = C.bool(limits.DisableHolePunching)
	return mapError(C.possum_set_instance_limits(h, &cLimits))
}

type Writer = *C.PossumWriter

func NewWriter(h *Handle) Writer {
	return C.possum_new_writer(h)
}

type ValueWriter = *C.PossumValueWriter

func StartNewValue(w Writer) (vw ValueWriter, err error) {
	err = mapError(C.possum_start_new_value(w, &vw))
	return
}

func ValueWriterFd(vw ValueWriter) int {
	return int(C.possum_value_writer_fd(vw))
}

func StageWrite(w Writer, key []byte, vw ValueWriter) error {
	return mapError(C.possum_writer_stage(w, BufFromBytes(key), vw))
}

func CommitWriter(w Writer) error {
	return mapError(C.possum_writer_commit(w))
}

func WriterRename(w Writer, v Value, newKey []byte) {
	C.possum_writer_rename(w, v, BufFromBytes(newKey))
}

func HandleMovePrefix(h *Handle, from, to []byte) error {
	return mapError(C.possum_handle_move_prefix(h, BufFromBytes(from), BufFromBytes(to)))
}

func HandleDeletePrefix(h *Handle, prefix []byte) error {
	return mapError(C.possum_handle_delete_prefix(h, BufFromBytes(prefix)))
}
