package xxh3

import (
	"encoding/binary"
	"hash"
)

// Hasher implements the hash.Hash interface
type Hasher struct {
	acc  [8]u64
	blk  u64
	len  u64
	key  ptr
	buf  [_block + _stripe]byte
	seed u64
}

var (
	_ hash.Hash   = (*Hasher)(nil)
	_ hash.Hash64 = (*Hasher)(nil)
)

// New returns a new Hasher that implements the hash.Hash interface.
func New() *Hasher {
	return new(Hasher)
}

// NewSeed returns a new Hasher that implements the hash.Hash interface.
func NewSeed(seed uint64) *Hasher {
	var h Hasher
	h.Reset()
	h.seed = seed
	h.key = key

	// Only initiate once, not on reset.
	if seed != 0 {
		h.key = ptr(&[secretSize]byte{})
		initSecret(h.key, seed)
	}
	return &h
}

// Reset resets the Hash to its initial state.
func (h *Hasher) Reset() {
	h.acc = [8]u64{
		prime32_3, prime64_1, prime64_2, prime64_3,
		prime64_4, prime32_2, prime64_5, prime32_1,
	}
	h.blk = 0
	h.len = 0
}

// ResetSeed will reset the hash and set a new seed.
// This will change the original state used by Reset.
func (h *Hasher) ResetSeed(seed uint64) {
	h.Reset()
	// Set key if not set before.
	if h.seed == 0 && seed != 0 {
		h.key = ptr(&[secretSize]byte{})
	}
	// Re-init seed.
	if seed == 0 {
		h.key = nil
	} else if seed != h.seed {
		initSecret(h.key, seed)
	}
	h.seed = seed
}

// BlockSize returns the hash's underlying block size.
// The Write method will accept any amount of data, but
// it may operate more efficiently if all writes are a
// multiple of the block size.
func (h *Hasher) BlockSize() int { return _stripe }

// Size returns the number of bytes Sum will return.
func (h *Hasher) Size() int { return 8 }

// Sum appends the current hash to b and returns the resulting slice.
// It does not change the underlying hash state.
func (h *Hasher) Sum(b []byte) []byte {
	var tmp [8]byte
	binary.BigEndian.PutUint64(tmp[:], h.Sum64())
	return append(b, tmp[:]...)
}

// Write adds more data to the running hash.
// It never returns an error.
func (h *Hasher) Write(buf []byte) (int, error) {
	h.update(buf)
	return len(buf), nil
}

// WriteString adds more data to the running hash.
// It never returns an error.
func (h *Hasher) WriteString(buf string) (int, error) {
	h.updateString(buf)
	return len(buf), nil
}

func (h *Hasher) update(buf []byte) {
	// relies on the data pointer being the first word in the string header
	h.updateString(*(*string)(ptr(&buf)))
}

func (h *Hasher) updateString(buf string) {
	if h.key == nil {
		h.key = key
		h.Reset()
	}

	// On first write, if more than 1 block, process without copy.
	for h.len == 0 && len(buf) > len(h.buf) {
		if hasAVX512 {
			accumBlockAVX512(&h.acc, *(*ptr)(ptr(&buf)), h.key)
		} else if hasAVX2 {
			accumBlockAVX2(&h.acc, *(*ptr)(ptr(&buf)), h.key)
		} else if hasSSE2 {
			accumBlockSSE(&h.acc, *(*ptr)(ptr(&buf)), h.key)
		} else if hasNEON {
			accumBlockNEON(&h.acc, *(*ptr)(ptr(&buf)), h.key)
		} else {
			accumBlockScalar(&h.acc, *(*ptr)(ptr(&buf)), h.key)
		}
		buf = buf[_block:]
		h.blk++
	}

	for len(buf) > 0 {
		if h.len < u64(len(h.buf)) {
			n := copy(h.buf[h.len:], buf)
			h.len += u64(n)
			buf = buf[n:]
			continue
		}

		if hasAVX512 {
			accumBlockAVX512(&h.acc, ptr(&h.buf), h.key)
		} else if hasAVX2 {
			accumBlockAVX2(&h.acc, ptr(&h.buf), h.key)
		} else if hasSSE2 {
			accumBlockSSE(&h.acc, ptr(&h.buf), h.key)
		} else if hasNEON {
			accumBlockNEON(&h.acc, ptr(&h.buf), h.key)
		} else {
			accumBlockScalar(&h.acc, ptr(&h.buf), h.key)
		}

		h.blk++
		h.len = _stripe
		copy(h.buf[:_stripe], h.buf[_block:])
	}
}

// Sum64 returns the 64-bit hash of the written data.
func (h *Hasher) Sum64() uint64 {
	if h.key == nil {
		h.key = key
		h.Reset()
	}

	if h.blk == 0 {
		if h.seed == 0 {
			return Hash(h.buf[:h.len])
		}
		return HashSeed(h.buf[:h.len], h.seed)
	}

	l := h.blk*_block + h.len
	acc := l * prime64_1
	accs := h.acc

	if h.len > 0 {
		// We are only ever doing 1 block here, so no avx512.
		if hasAVX2 {
			accumAVX2(&accs, ptr(&h.buf[0]), h.key, h.len)
		} else if hasSSE2 {
			accumSSE(&accs, ptr(&h.buf[0]), h.key, h.len)
		} else if hasNEON {
			accumNEON(&accs, ptr(&h.buf[0]), h.key, h.len)
		} else {
			accumScalar(&accs, ptr(&h.buf[0]), h.key, h.len)
		}
	}

	if h.seed == 0 {
		acc += mulFold64(accs[0]^key64_011, accs[1]^key64_019)
		acc += mulFold64(accs[2]^key64_027, accs[3]^key64_035)
		acc += mulFold64(accs[4]^key64_043, accs[5]^key64_051)
		acc += mulFold64(accs[6]^key64_059, accs[7]^key64_067)
	} else {
		secret := h.key
		acc += mulFold64(accs[0]^readU64(secret, 11), accs[1]^readU64(secret, 19))
		acc += mulFold64(accs[2]^readU64(secret, 27), accs[3]^readU64(secret, 35))
		acc += mulFold64(accs[4]^readU64(secret, 43), accs[5]^readU64(secret, 51))
		acc += mulFold64(accs[6]^readU64(secret, 59), accs[7]^readU64(secret, 67))
	}

	acc = xxh3Avalanche(acc)

	return acc
}

// Sum128 returns the 128-bit hash of the written data.
func (h *Hasher) Sum128() Uint128 {
	if h.key == nil {
		h.key = key
		h.Reset()
	}

	if h.blk == 0 {
		if h.seed == 0 {
			return Hash128(h.buf[:h.len])
		}
		return Hash128Seed(h.buf[:h.len], h.seed)
	}

	l := h.blk*_block + h.len
	acc := Uint128{Lo: l * prime64_1, Hi: ^(l * prime64_2)}
	accs := h.acc

	if h.len > 0 {
		// We are only ever doing 1 block here, so no avx512.
		if hasAVX2 {
			accumAVX2(&accs, ptr(&h.buf[0]), h.key, h.len)
		} else if hasSSE2 {
			accumSSE(&accs, ptr(&h.buf[0]), h.key, h.len)
		} else if hasNEON {
			accumNEON(&accs, ptr(&h.buf[0]), h.key, h.len)
		} else {
			accumScalar(&accs, ptr(&h.buf[0]), h.key, h.len)
		}
	}

	if h.seed == 0 {
		acc.Lo += mulFold64(accs[0]^key64_011, accs[1]^key64_019)
		acc.Hi += mulFold64(accs[0]^key64_117, accs[1]^key64_125)

		acc.Lo += mulFold64(accs[2]^key64_027, accs[3]^key64_035)
		acc.Hi += mulFold64(accs[2]^key64_133, accs[3]^key64_141)

		acc.Lo += mulFold64(accs[4]^key64_043, accs[5]^key64_051)
		acc.Hi += mulFold64(accs[4]^key64_149, accs[5]^key64_157)

		acc.Lo += mulFold64(accs[6]^key64_059, accs[7]^key64_067)
		acc.Hi += mulFold64(accs[6]^key64_165, accs[7]^key64_173)
	} else {
		secret := h.key
		const hi_off = 117 - 11

		acc.Lo += mulFold64(accs[0]^readU64(secret, 11), accs[1]^readU64(secret, 19))
		acc.Hi += mulFold64(accs[0]^readU64(secret, 11+hi_off), accs[1]^readU64(secret, 19+hi_off))

		acc.Lo += mulFold64(accs[2]^readU64(secret, 27), accs[3]^readU64(secret, 35))
		acc.Hi += mulFold64(accs[2]^readU64(secret, 27+hi_off), accs[3]^readU64(secret, 35+hi_off))

		acc.Lo += mulFold64(accs[4]^readU64(secret, 43), accs[5]^readU64(secret, 51))
		acc.Hi += mulFold64(accs[4]^readU64(secret, 43+hi_off), accs[5]^readU64(secret, 51+hi_off))

		acc.Lo += mulFold64(accs[6]^readU64(secret, 59), accs[7]^readU64(secret, 67))
		acc.Hi += mulFold64(accs[6]^readU64(secret, 59+hi_off), accs[7]^readU64(secret, 67+hi_off))
	}

	acc.Lo = xxh3Avalanche(acc.Lo)
	acc.Hi = xxh3Avalanche(acc.Hi)

	return acc
}
