/*
ZXC - High-performance lossless compression

Copyright (c) 2025-2026 Bertrand Lebonnois and contributors.
SPDX-License-Identifier: BSD-3-Clause
*/

package zxc

/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "zxc_seekable.h"
#include "zxc_stream.h"
#include "zxc_error.h"

// Forward declaration of the Go-side trampoline (generated by //export).
extern int64_t zxcGoSeekableReadAt(void *ctx, void *dst, size_t len, uint64_t offset);

// Opens a seekable archive through a stack-built zxc_reader_t. The ctx is a
// uintptr-encoded runtime/cgo.Handle for the Go-side reader, and read_at is
// the Go-exported trampoline above.
static zxc_seekable* zxcGoOpenReader(uintptr_t ctx, uint64_t size) {
    zxc_reader_t r;
    r.read_at = zxcGoSeekableReadAt;
    r.ctx     = (void *)ctx;
    r.size    = size;
    return zxc_seekable_open_reader(&r);
}
*/
import "C"

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

// ============================================================================
// Seekable API (random-access decompression, single-threaded)
// ============================================================================

// Seekable is a handle to a seekable ZXC archive.
//
// Build one with [Open] (file-backed) or [OpenBytes] (in-memory). Always
// defer a call to [Seekable.Close] to release the native resources.
//
// A Seekable handle is single-threaded: callers must not invoke its methods
// from more than one goroutine concurrently.
type Seekable struct {
	ptr     *C.zxc_seekable
	file    *C.FILE        // set when opened via Open
	cbuf    unsafe.Pointer // C-owned copy of source bytes (OpenBytes)
	dbuf    unsafe.Pointer // C-owned copy of the dictionary content (SetDict)
	rhandle cgo.Handle     // valid when opened via OpenReader; zero otherwise
	hasRH   bool           // true when rhandle is set (cgo.Handle 0 is itself valid)
}

// Open opens a seekable archive from a file path.
//
// The file is opened in binary read mode through the C runtime and remains
// open for the lifetime of the returned handle. Close releases both the
// archive handle and the underlying FILE*.
func Open(path string) (*Seekable, error) {
	cpath := C.CString(path)
	defer C.free(unsafe.Pointer(cpath))
	mode := C.CString("rb")
	defer C.free(unsafe.Pointer(mode))

	f, err := C.fopen(cpath, mode)
	if f == nil {
		if err != nil {
			return nil, err
		}
		return nil, ErrIO
	}

	ptr := C.zxc_seekable_open_file(f)
	if ptr == nil {
		C.fclose(f)
		return nil, ErrInvalidData
	}
	return &Seekable{ptr: ptr, file: f}, nil
}

// OpenBytes opens a seekable archive from an in-memory buffer.
//
// The library requires the source buffer to remain valid for the lifetime
// of the handle. To satisfy CGO pointer rules and avoid keeping a Go slice
// pinned, OpenBytes copies the buffer into C-allocated memory. The copy is
// freed by [Seekable.Close].
func OpenBytes(data []byte) (*Seekable, error) {
	if len(data) == 0 {
		return nil, ErrSrcTooSmall
	}

	cbuf := C.malloc(C.size_t(len(data)))
	if cbuf == nil {
		return nil, ErrMemory
	}
	C.memcpy(cbuf, unsafe.Pointer(&data[0]), C.size_t(len(data)))

	ptr := C.zxc_seekable_open(cbuf, C.size_t(len(data)))
	if ptr == nil {
		C.free(cbuf)
		return nil, ErrInvalidData
	}
	return &Seekable{ptr: ptr, cbuf: cbuf}, nil
}

// OpenReader opens a seekable archive backed by a user-supplied
// [io.ReaderAt]. Use this to plug any storage that supports positional
// reads behind the seekable API: an *os.File, an *bytes.Reader, an HTTP
// range-request client, an S3 object, a custom VFS.
//
// size must be the total compressed-archive size in bytes (use
// f.Stat().Size() for an *os.File). The library invokes ReadAt three
// times during the call (header, footer, seek table) and once per block
// during subsequent DecompressRange calls.
//
// The caller's reader must remain valid until [Seekable.Close] is called.
// ReadAt may be invoked from any goroutine but never concurrently in the
// current single-threaded API; a future multi-threaded entry point will
// require ReadAt to be safe for concurrent use.
func OpenReader(r io.ReaderAt, size int64) (*Seekable, error) {
	if r == nil {
		return nil, ErrNullInput
	}
	if size <= 0 {
		return nil, ErrSrcTooSmall
	}
	h := cgo.NewHandle(r)
	ptr := C.zxcGoOpenReader(C.uintptr_t(h), C.uint64_t(size))
	if ptr == nil {
		h.Delete()
		return nil, ErrInvalidData
	}
	return &Seekable{ptr: ptr, rhandle: h, hasRH: true}, nil
}

// The C->Go read trampoline (zxcGoSeekableReadAt) lives in
// zxc_seekable_export.go; see the comment there for why it is kept out of this
// file's preamble. The forward declaration above (in the cgo preamble) is what
// lets zxcGoOpenReader take its address.

// Close releases the native resources associated with the handle. Safe to
// call multiple times; subsequent calls are no-ops.
func (s *Seekable) Close() error {
	if s == nil || s.ptr == nil {
		return nil
	}
	// Free the C handle first so any in-flight read_at callbacks complete
	// before we release the underlying Go reader handle.
	C.zxc_seekable_free(s.ptr)
	s.ptr = nil

	if s.file != nil {
		C.fclose(s.file)
		s.file = nil
	}
	if s.cbuf != nil {
		C.free(s.cbuf)
		s.cbuf = nil
	}
	if s.dbuf != nil {
		C.free(s.dbuf)
		s.dbuf = nil
	}
	if s.hasRH {
		s.rhandle.Delete()
		s.hasRH = false
	}
	return nil
}

// NumBlocks returns the total number of data blocks in the archive
// (excluding the EOF marker block). Returns 0 if the handle has been
// closed.
func (s *Seekable) NumBlocks() uint32 {
	if s == nil || s.ptr == nil {
		return 0
	}
	return uint32(C.zxc_seekable_get_num_blocks(s.ptr))
}

// DecompressedSize returns the total decompressed size of the archive in
// bytes. Returns 0 if the handle has been closed.
func (s *Seekable) DecompressedSize() uint64 {
	if s == nil || s.ptr == nil {
		return 0
	}
	return uint64(C.zxc_seekable_get_decompressed_size(s.ptr))
}

// BlockCompressedSize returns the on-disk size of a specific block (block
// header + payload + optional per-block checksum). The second return value
// is false if blockIdx is out of range or the handle has been closed.
func (s *Seekable) BlockCompressedSize(blockIdx uint32) (uint32, bool) {
	if s == nil || s.ptr == nil || blockIdx >= s.NumBlocks() {
		return 0, false
	}
	return uint32(C.zxc_seekable_get_block_comp_size(s.ptr, C.uint32_t(blockIdx))), true
}

// BlockDecompressedSize returns the decompressed size of a specific block.
// The second return value is false if blockIdx is out of range or the
// handle has been closed.
func (s *Seekable) BlockDecompressedSize(blockIdx uint32) (uint32, bool) {
	if s == nil || s.ptr == nil || blockIdx >= s.NumBlocks() {
		return 0, false
	}
	return uint32(C.zxc_seekable_get_block_decomp_size(s.ptr, C.uint32_t(blockIdx))), true
}

// DecompressRange decompresses length bytes starting at offset (in the
// original uncompressed byte stream) into dst.
//
// Only the blocks overlapping the requested range are read and
// decompressed. dst must be at least length bytes long. Returns the number
// of bytes written, which equals length on success.
func (s *Seekable) DecompressRange(dst []byte, offset uint64, length int) (int, error) {
	if s == nil || s.ptr == nil {
		return 0, ErrNullInput
	}
	if length < 0 {
		return 0, ErrInvalidData
	}
	if len(dst) < length {
		return 0, ErrDstTooSmall
	}
	var dptr unsafe.Pointer
	if len(dst) > 0 {
		dptr = unsafe.Pointer(&dst[0])
	}
	res := C.zxc_seekable_decompress_range(
		s.ptr,
		dptr,
		C.size_t(len(dst)),
		C.uint64_t(offset),
		C.size_t(length),
	)
	if res < 0 {
		return 0, errorFromCode(res)
	}
	return int(res), nil
}

// SetDictionary attaches a [Dictionary] (content + shared table) in one call.
func (s *Seekable) SetDictionary(d *Dictionary) error {
	return s.SetDict(d.Content(), d.Huf())
}

// SetDict attaches a pre-trained dictionary to the seekable handle so that
// subsequent [Seekable.DecompressRange] calls can decode blocks that were
// compressed with that dictionary. It must be called before decoding any
// range from a dictionary-compressed archive.
//
// The dictionary content must match the one used at compression time; its ID
// is validated against the archive header. When the archive was compressed
// with a shared literal Huffman table, pass the same table as hufLengths
// ([HufTableSize] bytes, see [DictHuf]); pass nil otherwise. Both buffers
// are copied internally by the library. Passing an empty dict is an error.
func (s *Seekable) SetDict(dict, hufLengths []byte) error {
	if s == nil || s.ptr == nil {
		return ErrNullInput
	}
	if len(dict) == 0 {
		return ErrSrcTooSmall
	}

	// Copy the buffers into C-owned memory for the duration of the call (the
	// library makes its own internal copies; the cgo pointer-passing rules
	// just forbid handing it Go memory containing Go pointers via the handle).
	buf := C.malloc(C.size_t(len(dict) + len(hufLengths)))
	if buf == nil {
		return ErrMemory
	}
	C.memcpy(buf, unsafe.Pointer(&dict[0]), C.size_t(len(dict)))
	var huf unsafe.Pointer
	if len(hufLengths) > 0 {
		huf = unsafe.Add(buf, len(dict))
		C.memcpy(huf, unsafe.Pointer(&hufLengths[0]), C.size_t(len(hufLengths)))
	}

	rc := C.zxc_seekable_set_dict(s.ptr, buf, C.size_t(len(dict)), huf)
	if rc < 0 {
		C.free(buf)
		return errorFromCode(C.int64_t(rc))
	}

	// Replace any previously-set dictionary buffer.
	if s.dbuf != nil {
		C.free(s.dbuf)
	}
	s.dbuf = buf
	return nil
}

// ============================================================================
// Low-level seek table helpers
// ============================================================================

// SeekTableSize returns the encoded byte size of a seek table covering
// numBlocks data blocks. Use this to size a destination buffer for
// [WriteSeekTable].
func SeekTableSize(numBlocks uint32) int {
	return int(C.zxc_seek_table_size(C.uint32_t(numBlocks)))
}

// WriteSeekTable writes a seek table (header + entries) into dst.
// compSizes is the slice of per-block on-disk compressed sizes, in order.
//
// Most callers do not need this directly: the file and streaming APIs
// already emit a seek table when [WithSeekable] is enabled.
func WriteSeekTable(dst []byte, compSizes []uint32) (int, error) {
	if len(dst) == 0 {
		return 0, ErrDstTooSmall
	}
	if len(compSizes) == 0 {
		return 0, ErrInvalidData
	}
	res := C.zxc_write_seek_table(
		(*C.uint8_t)(unsafe.Pointer(&dst[0])),
		C.size_t(len(dst)),
		(*C.uint32_t)(unsafe.Pointer(&compSizes[0])),
		C.uint32_t(len(compSizes)),
	)
	if res < 0 {
		return 0, errorFromCode(res)
	}
	return int(res), nil
}
