// Package blobio provides an SQL interface to incremental BLOB I/O.
package blobio

import (
	"errors"
	"io"

	"github.com/ncruces/go-sqlite3"
	"github.com/ncruces/go-sqlite3/internal/util"
)

// Register registers the SQL functions:
//
//	readblob(schema, table, column, rowid, offset, n/writer)
//
// Reads n bytes of a blob, starting at offset.
//
//	writeblob(schema, table, column, rowid, offset, data/reader)
//
// Writes data into a blob, at the given offset.
//
//	openblob(schema, table, column, rowid, write, callback, args...)
//
// Opens blobs for reading or writing.
// The callback is invoked for each open blob,
// and must be bound to an [OpenCallback],
// using [sqlite3.BindPointer] or [sqlite3.Pointer].
// The optional args will be passed to the callback,
// along with the [sqlite3.Blob] handle.
// The [sqlite3.Blob] handle is only valid during
// the execution of the callback. Callers cannot
// read or write to the handle after the callback
// exits.
//
// https://sqlite.org/c3ref/blob.html
func Register(db *sqlite3.Conn) error {
	return errors.Join(
		db.CreateFunction("readblob", 6, 0, readblob),
		db.CreateFunction("writeblob", 6, 0, writeblob),
		db.CreateFunction("openblob", -1, 0, openblob))
}

// OpenCallback is the type for the openblob callback.
type OpenCallback func(*sqlite3.Blob, ...sqlite3.Value) error

func readblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
	_ = arg[5] // bounds check

	blob, err := getAuxBlob(ctx, arg, false)
	if err != nil {
		ctx.ResultError(err)
		return // notest
	}

	_, err = blob.Seek(arg[4].Int64(), io.SeekStart)
	if err != nil {
		ctx.ResultError(err)
		return // notest
	}

	if p, ok := arg[5].Pointer().(io.Writer); ok {
		var n int64
		n, err = blob.WriteTo(p)
		ctx.ResultInt64(n)
	} else {
		n := arg[5].Int64()
		if n <= 0 {
			return
		}
		buf := make([]byte, n)
		_, err = io.ReadFull(blob, buf)
		ctx.ResultBlob(buf)
	}
	if err != nil {
		ctx.ResultError(err)
		return // notest
	}

	setAuxBlob(ctx, blob, false)
}

func writeblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
	_ = arg[5] // bounds check

	blob, err := getAuxBlob(ctx, arg, true)
	if err != nil {
		ctx.ResultError(err)
		return // notest
	}

	_, err = blob.Seek(arg[4].Int64(), io.SeekStart)
	if err != nil {
		ctx.ResultError(err)
		return // notest
	}

	if p, ok := arg[5].Pointer().(io.Reader); ok {
		var n int64
		n, err = blob.ReadFrom(p)
		ctx.ResultInt64(n)
	} else {
		_, err = blob.Write(arg[5].RawBlob())
	}
	if err != nil {
		ctx.ResultError(err)
		return // notest
	}

	setAuxBlob(ctx, blob, false)
}

func openblob(ctx sqlite3.Context, arg ...sqlite3.Value) {
	if len(arg) < 6 {
		ctx.ResultError(util.ErrorString("openblob: wrong number of arguments"))
		return
	}

	blob, err := getAuxBlob(ctx, arg, arg[4].Bool())
	if err != nil {
		ctx.ResultError(err)
		return // notest
	}

	fn := arg[5].Pointer().(OpenCallback)
	err = fn(blob, arg[6:]...)
	if err != nil {
		ctx.ResultError(err)
		return // notest
	}

	setAuxBlob(ctx, blob, true)
}

func getAuxBlob(ctx sqlite3.Context, arg []sqlite3.Value, write bool) (*sqlite3.Blob, error) {
	row := arg[3].Int64()

	if blob, ok := ctx.GetAuxData(0).(*sqlite3.Blob); ok {
		if err := blob.Reopen(row); errors.Is(err, sqlite3.MISUSE) {
			// Blob was closed (db, table, column or write changed).
		} else {
			return blob, err
		}
	}

	db := arg[0].Text()
	table := arg[1].Text()
	column := arg[2].Text()
	return ctx.Conn().OpenBlob(db, table, column, row, write)
}

func setAuxBlob(ctx sqlite3.Context, blob *sqlite3.Blob, open bool) {
	// This ensures the blob is closed if db, table, column or write change.
	ctx.SetAuxData(0, blob) // db
	ctx.SetAuxData(1, blob) // table
	ctx.SetAuxData(2, blob) // column
	if open {
		ctx.SetAuxData(4, blob) // write
	}
}
