// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package bloom

import (
	"crypto/rand"
	"encoding/binary"
	"errors"
	"fmt"
	"math/bits"
	"sync"
)

const (
	minHashes  = 1
	maxHashes  = 16 // Supports a false positive probability of 2^-16 when using optimal size values
	minEntries = 1

	bitsPerByte    = 8
	bytesPerUint64 = 8
	hashRotation   = 17
)

var (
	errInvalidNumHashes = errors.New("invalid num hashes")
	errTooFewHashes     = errors.New("too few hashes")
	errTooManyHashes    = errors.New("too many hashes")
	errTooFewEntries    = errors.New("too few entries")
)

type Filter struct {
	// numBits is always equal to [bitsPerByte * len(entries)]
	numBits uint64

	lock      sync.RWMutex
	hashSeeds []uint64
	entries   []byte
	count     int
}

// New creates a new Filter with the specified number of hashes and bytes for
// entries. The returned bloom filter is safe for concurrent usage.
func New(numHashes, numEntries int) (*Filter, error) {
	if numEntries < minEntries {
		return nil, errTooFewEntries
	}

	hashSeeds, err := newHashSeeds(numHashes)
	if err != nil {
		return nil, err
	}

	return &Filter{
		numBits:   uint64(numEntries * bitsPerByte),
		hashSeeds: hashSeeds,
		entries:   make([]byte, numEntries),
		count:     0,
	}, nil
}

func (f *Filter) Add(hash uint64) {
	f.lock.Lock()
	defer f.lock.Unlock()

	_ = 1 % f.numBits // hint to the compiler that numBits is not 0
	for _, seed := range f.hashSeeds {
		hash = bits.RotateLeft64(hash, hashRotation) ^ seed
		index := hash % f.numBits
		byteIndex := index / bitsPerByte
		bitIndex := index % bitsPerByte
		f.entries[byteIndex] |= 1 << bitIndex
	}
	f.count++
}

// Count returns the number of elements that have been added to the bloom
// filter.
func (f *Filter) Count() int {
	f.lock.RLock()
	defer f.lock.RUnlock()

	return f.count
}

func (f *Filter) Contains(hash uint64) bool {
	f.lock.RLock()
	defer f.lock.RUnlock()

	return contains(f.hashSeeds, f.entries, hash)
}

func (f *Filter) Marshal() []byte {
	f.lock.RLock()
	defer f.lock.RUnlock()

	return marshal(f.hashSeeds, f.entries)
}

func newHashSeeds(count int) ([]uint64, error) {
	switch {
	case count < minHashes:
		return nil, fmt.Errorf("%w: %d < %d", errTooFewHashes, count, minHashes)
	case count > maxHashes:
		return nil, fmt.Errorf("%w: %d > %d", errTooManyHashes, count, maxHashes)
	}

	bytes := make([]byte, count*bytesPerUint64)
	if _, err := rand.Reader.Read(bytes); err != nil {
		return nil, err
	}

	seeds := make([]uint64, count)
	for i := range seeds {
		seeds[i] = binary.BigEndian.Uint64(bytes[i*bytesPerUint64:])
	}
	return seeds, nil
}

func contains(hashSeeds []uint64, entries []byte, hash uint64) bool {
	var (
		numBits          = bitsPerByte * uint64(len(entries))
		_                = 1 % numBits // hint to the compiler that numBits is not 0
		accumulator byte = 1
	)
	for seedIndex := 0; seedIndex < len(hashSeeds) && accumulator != 0; seedIndex++ {
		hash = bits.RotateLeft64(hash, hashRotation) ^ hashSeeds[seedIndex]
		index := hash % numBits
		byteIndex := index / bitsPerByte
		bitIndex := index % bitsPerByte
		accumulator &= entries[byteIndex] >> bitIndex
	}
	return accumulator != 0
}

func marshal(hashSeeds []uint64, entries []byte) []byte {
	numHashes := len(hashSeeds)
	entriesOffset := 1 + numHashes*bytesPerUint64

	bytes := make([]byte, entriesOffset+len(entries))
	bytes[0] = byte(numHashes)
	for i, seed := range hashSeeds {
		binary.BigEndian.PutUint64(bytes[1+i*bytesPerUint64:], seed)
	}
	copy(bytes[entriesOffset:], entries)
	return bytes
}
