/*
 * SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
 * SPDX-License-Identifier: Apache-2.0
 */

package z

import (
	
	
	
	
	
	
	
)

const (
	defaultCapacity = 64
	defaultTag      = "buffer"
)

// Buffer is equivalent of bytes.Buffer without the ability to read. It is NOT thread-safe.
//
// In UseCalloc mode, z.Calloc is used to allocate memory, which depending upon how the code is
// compiled could use jemalloc for allocations.
//
// In UseMmap mode, Buffer  uses file mmap to allocate memory. This allows us to store big data
// structures without using physical memory.
//
// MaxSize can be set to limit the memory usage.
type Buffer struct {
	padding       uint64     // number of starting bytes used for padding
	offset        uint64     // used length of the buffer
	buf           []byte     // backing slice for the buffer
	bufType       BufferType // type of the underlying buffer
	curSz         int        // capacity of the buffer
	maxSz         int        // causes a panic if the buffer grows beyond this size
	mmapFile      *MmapFile  // optional mmap backing for the buffer
	autoMmapAfter int        // Calloc falls back to an mmaped tmpfile after crossing this size
	autoMmapDir   string     // directory for autoMmap to create a tempfile in
	persistent    bool       // when enabled, Release will not delete the underlying mmap file
	tag           string     // used for jemalloc stats
}

func ( int,  string) *Buffer {
	if  < defaultCapacity {
		 = defaultCapacity
	}
	if  == "" {
		 = defaultTag
	}
	return &Buffer{
		buf:     Calloc(, ),
		bufType: UseCalloc,
		curSz:   ,
		offset:  8,
		padding: 8,
		tag:     ,
	}
}

// It is the caller's responsibility to set offset after this, because Buffer
// doesn't remember what it was.
func ( string,  int) (*Buffer, error) {
	,  := os.OpenFile(, os.O_RDWR|os.O_CREATE, 0666)
	if  != nil {
		return nil, 
	}
	,  := newBufferFile(, )
	if  != nil {
		return nil, 
	}
	.persistent = true
	return , nil
}

func ( string,  int) (*Buffer, error) {
	if  == "" {
		 = tmpDir
	}
	,  := os.CreateTemp(, "buffer")
	if  != nil {
		return nil, 
	}
	return newBufferFile(, )
}

func newBufferFile( *os.File,  int) (*Buffer, error) {
	if  < defaultCapacity {
		 = defaultCapacity
	}
	,  := OpenMmapFileUsing(, , true)
	if  != nil &&  != NewFile {
		return nil, 
	}
	 := &Buffer{
		buf:      .Data,
		bufType:  UseMmap,
		curSz:    len(.Data),
		mmapFile: ,
		offset:   8,
		padding:  8,
	}
	return , nil
}

func ( []byte) *Buffer {
	return &Buffer{
		offset:  uint64(len()),
		buf:     ,
		bufType: UseInvalid,
	}
}

func ( *Buffer) ( int,  string) *Buffer {
	if .bufType != UseCalloc {
		panic("can only autoMmap with UseCalloc")
	}
	.autoMmapAfter = 
	if  == "" {
		.autoMmapDir = tmpDir
	} else {
		.autoMmapDir = 
	}
	return 
}

func ( *Buffer) ( int) *Buffer {
	.maxSz = 
	return 
}

func ( *Buffer) () bool {
	return int(.offset) == .StartOffset()
}

// LenWithPadding would return the number of bytes written to the buffer so far
// plus the padding at the start of the buffer.
func ( *Buffer) () int {
	return int(atomic.LoadUint64(&.offset))
}

// LenNoPadding would return the number of bytes written to the buffer so far
// (without the padding).
func ( *Buffer) () int {
	return int(atomic.LoadUint64(&.offset) - .padding)
}

// Bytes would return all the written bytes as a slice.
func ( *Buffer) () []byte {
	 := atomic.LoadUint64(&.offset)
	return .buf[.padding:]
}

// Grow would grow the buffer to have at least n more bytes. In case the buffer is at capacity, it
// would reallocate twice the size of current capacity + n, to ensure n bytes can be written to the
// buffer without further allocation. In UseMmap mode, this might result in underlying file
// expansion.
func ( *Buffer) ( int) {
	if .buf == nil {
		panic("z.Buffer needs to be initialized before using")
	}
	if .maxSz > 0 && int(.offset)+ > .maxSz {
		 := fmt.Errorf(
			"z.Buffer max size exceeded: %d offset: %d grow: %d", .maxSz, .offset, )
		panic()
	}
	if int(.offset)+ < .curSz {
		return
	}

	// Calculate new capacity.
	 := .curSz + 
	// Don't allocate more than 1GB at a time.
	if  > 1<<30 {
		 = 1 << 30
	}
	// Allocate at least n, even if it exceeds the 1GB limit above.
	if  >  {
		 = 
	}
	.curSz += 

	switch .bufType {
	case UseCalloc:
		// If autoMmap gets triggered, copy the slice over to an mmaped file.
		if .autoMmapAfter > 0 && .curSz > .autoMmapAfter {
			.bufType = UseMmap
			,  := os.CreateTemp(.autoMmapDir, "")
			if  != nil {
				panic()
			}
			,  := OpenMmapFileUsing(, .curSz, true)
			if  != nil &&  != NewFile {
				panic()
			}
			assert(int(.offset) == copy(.Data, .buf[:.offset]))
			Free(.buf)
			.mmapFile = 
			.buf = .Data
			break
		}

		// Else, reallocate the slice.
		 := Calloc(.curSz, .tag)
		assert(int(.offset) == copy(, .buf[:.offset]))
		Free(.buf)
		.buf = 

	case UseMmap:
		// Truncate and remap the underlying file.
		if  := .mmapFile.Truncate(int64(.curSz));  != nil {
			 = errors.Join(,
				fmt.Errorf("while trying to truncate file: %s to size: %d", .mmapFile.Fd.Name(), .curSz))
			panic()
		}
		.buf = .mmapFile.Data

	default:
		panic("can only use Grow on UseCalloc and UseMmap buffers")
	}
}

// Allocate is a way to get a slice of size n back from the buffer. This slice can be directly
// written to. Warning: Allocate is not thread-safe. The byte slice returned MUST be used before
// further calls to Buffer.
func ( *Buffer) ( int) []byte {
	.Grow()
	 := .offset
	.offset += uint64()
	return .buf[:int(.offset)]
}

// AllocateOffset works the same way as allocate, but instead of returning a byte slice, it returns
// the offset of the allocation.
func ( *Buffer) ( int) int {
	.Grow()
	.offset += uint64()
	return int(.offset) - 
}

func ( *Buffer) ( int) {
	 := .Allocate(8)
	binary.BigEndian.PutUint64(, uint64())
}

// SliceAllocate would encode the size provided into the buffer, followed by a call to Allocate,
// hence returning the slice of size sz. This can be used to allocate a lot of small buffers into
// this big buffer.
// Note that SliceAllocate should NOT be mixed with normal calls to Write.
func ( *Buffer) ( int) []byte {
	.Grow(8 + )
	.writeLen()
	return .Allocate()
}

func ( *Buffer) () int {
	return int(.padding)
}

func ( *Buffer) ( []byte) {
	 := .SliceAllocate(len())
	assert(len() == copy(, ))
}

func ( *Buffer) ( func( []byte) error) error {
	if .IsEmpty() {
		return nil
	}

	 := .StartOffset()
	var  []byte
	for  >= 0 {
		,  = .Slice()
		if len() == 0 {
			continue
		}
		if  := ();  != nil {
			return 
		}
	}

	return nil
}

const (
	UseCalloc BufferType = iota
	UseMmap
	UseInvalid
)

type BufferType int

func ( BufferType) () string {
	switch  {
	case UseCalloc:
		return "UseCalloc"
	case UseMmap:
		return "UseMmap"
	default:
		return "UseInvalid"
	}
}

type LessFunc func(a, b []byte) bool
type sortHelper struct {
	offsets []int
	b       *Buffer
	tmp     *Buffer
	less    LessFunc
	small   []int
}

func ( *sortHelper) (,  int) {
	.tmp.Reset()
	.small = .small[:0]
	 := 
	for  >= 0 &&  <  {
		.small = append(.small, )
		_,  = .b.Slice()
	}

	// We are sorting the slices pointed to by s.small offsets, but only moving the offsets around.
	sort.Slice(.small, func(,  int) bool {
		,  := .b.Slice(.small[])
		,  := .b.Slice(.small[])
		return .less(, )
	})
	// Now we iterate over the s.small offsets and copy over the slices. The result is now in order.
	for ,  := range .small {
		_, _ = .tmp.Write(rawSlice(.b.buf[:]))
	}
	assert(- == copy(.b.buf[:], .tmp.Bytes()))
}

func assert( bool) {
	if ! {
		log.Fatalf("%+v", errors.New("Assertion failure"))
	}
}
func check( error) {
	if  != nil {
		log.Fatalf("%+v", )
	}
}
func check2( interface{},  error) {
	check()
}

func ( *sortHelper) (,  []byte, ,  int) {
	if len() == 0 || len() == 0 {
		return
	}
	.tmp.Reset()
	check2(.tmp.Write())
	 = .tmp.Bytes()

	var ,  []byte

	 := func() {
		assert(len() == copy(.b.buf[:], ))
		 = [len():]
		 += len()
	}
	 := func() {
		assert(len() == copy(.b.buf[:], ))
		 = [len():]
		 += len()
	}

	for  <  {
		if len() == 0 {
			assert(len() == copy(.b.buf[:], ))
			return
		}
		if len() == 0 {
			assert(len() == copy(.b.buf[:], ))
			return
		}
		 = rawSlice()
		 = rawSlice()

		// We skip the first 4 bytes in the rawSlice, because that stores the length.
		if .less([8:], [8:]) {
			()
		} else {
			()
		}
	}
}

func ( *sortHelper) (,  int) []byte {
	assert( <= )

	 :=  + (-)/2
	,  := .offsets[], .offsets[]
	if  ==  {
		// No need to sort, just return the buffer.
		return .b.buf[:]
	}

	// lo, mid would sort from [offset[lo], offset[mid]) .
	 := .(, )
	// Typically we'd use mid+1, but here mid represents an offset in the buffer. Each offset
	// contains a thousand entries. So, if we do mid+1, we'd skip over those entries.
	 := .(, )

	.merge(, , , )
	return .b.buf[:]
}

// SortSlice is like SortSliceBetween but sorting over the entire buffer.
func ( *Buffer) ( func(,  []byte) bool) {
	.SortSliceBetween(.StartOffset(), int(.offset), )
}
func ( *Buffer) (,  int,  LessFunc) {
	if  >=  {
		return
	}
	if  == 0 {
		panic("start can never be zero")
	}

	var  []int
	,  := , 0
	for  >= 0 &&  <  {
		if %1024 == 0 {
			 = append(, )
		}
		_,  = .Slice()
		++
	}
	assert(len() > 0)
	if [len()-1] !=  {
		 = append(, )
	}

	 := int(float64((-)/2) * 1.1)
	 := &sortHelper{
		offsets: ,
		b:       ,
		less:    ,
		small:   make([]int, 0, 1024),
		tmp:     NewBuffer(, .tag),
	}
	defer func() { _ = .tmp.Release() }()

	 := [0]
	for ,  := range [1:] {
		.sortSmall(, )
		 = 
	}
	.sort(0, len()-1)
}

func rawSlice( []byte) []byte {
	 := binary.BigEndian.Uint64()
	return [:8+int()]
}

// Slice would return the slice written at offset.
func ( *Buffer) ( int) ([]byte, int) {
	if  >= int(.offset) {
		return nil, -1
	}

	 := binary.BigEndian.Uint64(.buf[:])
	 :=  + 8
	 :=  + int()
	 := .buf[:]
	if  >= int(.offset) {
		 = -1
	}
	return , 
}

// SliceOffsets is an expensive function. Use sparingly.
func ( *Buffer) () []int {
	 := .StartOffset()
	var  []int
	for  >= 0 {
		 = append(, )
		_,  = .Slice()
	}
	return 
}

func ( *Buffer) ( int) []byte {
	if  > .curSz {
		panic("offset beyond current size")
	}
	return .buf[:.curSz]
}

// Write would write p bytes to the buffer.
func ( *Buffer) ( []byte) ( int,  error) {
	 = len()
	.Grow()
	assert( == copy(.buf[.offset:], ))
	.offset += uint64()
	return , nil
}

// Reset would reset the buffer to be reused.
func ( *Buffer) () {
	.offset = uint64(.StartOffset())
}

// Release would free up the memory allocated by the buffer. Once the usage of buffer is done, it is
// important to call Release, otherwise a memory leak can happen.
func ( *Buffer) () error {
	if  == nil {
		return nil
	}
	switch .bufType {
	case UseCalloc:
		Free(.buf)
	case UseMmap:
		if .mmapFile == nil {
			return nil
		}
		 := .mmapFile.Fd.Name()
		if  := .mmapFile.Close(-1);  != nil {
			return errors.Join(, fmt.Errorf("while closing file: %s", ))
		}
		if !.persistent {
			if  := os.Remove();  != nil {
				return errors.Join(, fmt.Errorf("while deleting file %s", ))
			}
		}
	}
	return nil
}