// Copy paste of https://github.com/markus-wa/demoinfocs-golang/blob/master/internal/bitread/bitread.go
// We need it to read Source 1 demos header and for Source 2 demos the CDemoFileHeader proto message.
package bitread

import (
	"io"
	"math"
	"sync"

	bitread "github.com/markus-wa/gobitread"
	"github.com/pkg/errors"
)

const (
	smallBuffer      = 512
	largeBuffer      = 1024 * 128
	maxVarInt32Bytes = 5
	maxVarintBytes   = 10
)

// BitReader wraps github.com/markus-wa/gobitread.BitReader and provides additional functionality specific to CS:GO demos.
type BitReader struct {
	bitread.BitReader
	buffer *[]byte
}

// ReadString reads a variable length string.
func (r *BitReader) ReadString() string {
	// Valve also uses this sooo
	const valveMaxStringLength = 4096
	return r.readStringLimited(valveMaxStringLength, false)
}

func (r *BitReader) readStringLimited(limit int, endOnNewLine bool) string {
	const minStringBufferLength = 256
	result := make([]byte, 0, minStringBufferLength)

	for i := 0; i < limit; i++ {
		b := r.ReadSingleByte()
		if b == 0 || (endOnNewLine && b == '\n') {
			break
		}

		result = append(result, b)
	}

	return string(result)
}

// ReadFloat reads a 32-bit float. Wraps ReadInt().
func (r *BitReader) ReadFloat() float32 {
	return math.Float32frombits(uint32(r.ReadInt(32)))
}

// ReadVarInt32 reads a variable size unsigned int (max 32-bit).
func (r *BitReader) ReadVarInt32() uint32 {
	var (
		res uint32
		b   uint32 = 0x80
	)

	for count := uint(0); b&0x80 != 0 && count != maxVarInt32Bytes; count++ {
		b = uint32(r.ReadSingleByte())
		res |= (b & 0x7f) << (7 * count)
	}

	return res
}

// ReadVarInt64 reads a variable size unsigned int (max 64-bit).
func (r *BitReader) ReadVarInt64() uint64 {
	var (
		res uint64
		b   uint64 = 0x80
	)

	for count := uint(0); b&0x80 != 0 && count != maxVarintBytes; count++ {
		b = uint64(r.ReadSingleByte())
		res |= (b & 0x7f) << (7 * count)
	}

	return res
}

// ReadSignedVarInt32 reads a variable size signed int (max 32-bit).
func (r *BitReader) ReadSignedVarInt32() int32 {
	res := r.ReadVarInt32()
	return int32((res >> 1) ^ -(res & 1))
}

// ReadSignedVarInt64 reads a variable size signed int (max 64-bit).
func (r *BitReader) ReadSignedVarInt64() int64 {
	res := r.ReadVarInt64()
	return int64((res >> 1) ^ -(res & 1))
}

// ReadUBitInt reads some kind of variable size uint.
// Honestly, not quite sure how it works.
func (r *BitReader) ReadUBitInt() uint {
	res := r.ReadInt(6)
	switch res & (16 | 32) {
	case 16:
		res = (res & 15) | (r.ReadInt(4) << 4)
	case 32:
		res = (res & 15) | (r.ReadInt(8) << 4)
	case 48:
		res = (res & 15) | (r.ReadInt(32-4) << 4)
	}

	return res
}

var bitReaderPool = sync.Pool{
	New: func() any {
		return new(BitReader)
	},
}

// Pool puts the BitReader into a pool for future use.
// Pooling BitReaders improves performance by minimizing the amount newly allocated readers.
func (r *BitReader) Pool() error {
	err := r.Close()
	if err != nil {
		return errors.Wrap(err, "failed to close BitReader before pooling")
	}

	if len(*r.buffer) == smallBuffer {
		smallBufferPool.Put(r.buffer)
	}

	r.buffer = nil

	bitReaderPool.Put(r)

	return nil
}

func newBitReader(underlying io.Reader, buffer *[]byte) *BitReader {
	br := bitReaderPool.Get().(*BitReader)
	br.buffer = buffer
	br.OpenWithBuffer(underlying, *buffer)

	return br
}

var smallBufferPool = sync.Pool{
	New: func() any {
		b := make([]byte, smallBuffer)
		return &b
	},
}

// NewSmallBitReader returns a BitReader with a small buffer, suitable for short streams.
func NewSmallBitReader(underlying io.Reader) *BitReader {
	return newBitReader(underlying, smallBufferPool.Get().(*[]byte))
}

// NewLargeBitReader returns a BitReader with a large buffer, suitable for long streams (main demo file).
func NewLargeBitReader(underlying io.Reader) *BitReader {
	b := make([]byte, largeBuffer)
	return newBitReader(underlying, &b)
}
