package xxhashbench

import (
	"fmt"
	"hash/crc32"
	"hash/fnv"
	"testing"

	OneOfOne "github.com/OneOfOne/xxhash"
	"github.com/cespare/xxhash/v2"
	"github.com/spaolacci/murmur3"
)

// TODO: The main benchmarks live in the xxhash package now, so the only purpose
// of this is to compare different hash functions. Consider deleting xxhashbench
// or replacing it with a more minimal comparison.

var sink uint64

var benchmarks = []struct {
	name         string
	directBytes  func([]byte) uint64
	directString func(string) uint64
	digestBytes  func([]byte) uint64
	digestString func(string) uint64
}{
	{
		name:         "xxhash",
		directBytes:  xxhash.Sum64,
		directString: xxhash.Sum64String,
		digestBytes: func(b []byte) uint64 {
			h := xxhash.New()
			h.Write(b)
			return h.Sum64()
		},
		digestString: func(s string) uint64 {
			h := xxhash.New()
			h.WriteString(s)
			return h.Sum64()
		},
	},
	{
		name:         "OneOfOne",
		directBytes:  OneOfOne.Checksum64,
		directString: OneOfOne.ChecksumString64,
		digestBytes: func(b []byte) uint64 {
			h := OneOfOne.New64()
			h.Write(b)
			return h.Sum64()
		},
		digestString: func(s string) uint64 {
			h := OneOfOne.New64()
			h.WriteString(s)
			return h.Sum64()
		},
	},
	{
		name:        "murmur3",
		directBytes: murmur3.Sum64,
		directString: func(s string) uint64 {
			return murmur3.Sum64([]byte(s))
		},
		digestBytes: func(b []byte) uint64 {
			h := murmur3.New64()
			h.Write(b)
			return h.Sum64()
		},
		digestString: func(s string) uint64 {
			h := murmur3.New64()
			h.Write([]byte(s))
			return h.Sum64()
		},
	},
	{
		name: "CRC-32",
		directBytes: func(b []byte) uint64 {
			return uint64(crc32.ChecksumIEEE(b))
		},
		directString: func(s string) uint64 {
			return uint64(crc32.ChecksumIEEE([]byte(s)))
		},
		digestBytes: func(b []byte) uint64 {
			h := crc32.NewIEEE()
			h.Write(b)
			return uint64(h.Sum32())
		},
		digestString: func(s string) uint64 {
			h := crc32.NewIEEE()
			h.Write([]byte(s))
			return uint64(h.Sum32())
		},
	},
	{
		name: "FNV-1a",
		digestBytes: func(b []byte) uint64 {
			h := fnv.New64()
			h.Write(b)
			return h.Sum64()
		},
		digestString: func(s string) uint64 {
			h := fnv.New64a()
			h.Write([]byte(s))
			return h.Sum64()
		},
	},
}

func BenchmarkHashes(b *testing.B) {
	for _, bb := range benchmarks {
		for _, benchSize := range []struct {
			name string
			n    int
		}{
			{"5B", 5},
			{"100B", 100},
			{"4KB", 4e3},
			{"10MB", 10e6},
		} {
			input := make([]byte, benchSize.n)
			for i := range input {
				input[i] = byte(i)
			}
			inputString := string(input)
			if bb.directBytes != nil {
				name := fmt.Sprintf("%s,direct,bytes,n=%s", bb.name, benchSize.name)
				b.Run(name, func(b *testing.B) {
					benchmarkHashBytes(b, input, bb.directBytes)
				})
			}
			if bb.directString != nil {
				name := fmt.Sprintf("%s,direct,string,n=%s", bb.name, benchSize.name)
				b.Run(name, func(b *testing.B) {
					benchmarkHashString(b, inputString, bb.directString)
				})
			}
			if bb.digestBytes != nil {
				name := fmt.Sprintf("%s,digest,bytes,n=%s", bb.name, benchSize.name)
				b.Run(name, func(b *testing.B) {
					benchmarkHashBytes(b, input, bb.digestBytes)
				})
			}
			if bb.digestString != nil {
				name := fmt.Sprintf("%s,digest,string,n=%s", bb.name, benchSize.name)
				b.Run(name, func(b *testing.B) {
					benchmarkHashString(b, inputString, bb.digestString)
				})
			}
		}
	}
}

func benchmarkHashBytes(b *testing.B, input []byte, fn func([]byte) uint64) {
	b.SetBytes(int64(len(input)))
	for i := 0; i < b.N; i++ {
		sink = fn(input)
	}
}

func benchmarkHashString(b *testing.B, input string, fn func(string) uint64) {
	b.SetBytes(int64(len(input)))
	for i := 0; i < b.N; i++ {
		sink = fn(input)
	}
}
