// Package bytesutil defines helper methods for converting integers to byte slices.
package bytesutil

import (
	"encoding/binary"
	"fmt"
	"math/bits"
	"regexp"

	"github.com/pkg/errors"
	types "github.com/prysmaticlabs/eth2-types"
	fieldparams "github.com/prysmaticlabs/prysm/config/fieldparams"
)

var hexRegex = regexp.MustCompile("^0x[0-9a-fA-F]+$")

// ToBytes returns integer x to bytes in little-endian format at the specified length.
// Spec defines similar method uint_to_bytes(n: uint) -> bytes, which is equivalent to ToBytes(n, 8).
func ToBytes(x uint64, length int) []byte {
	if length < 0 {
		length = 0
	}
	makeLength := length
	if length < 8 {
		makeLength = 8
	}
	bytes := make([]byte, makeLength)
	binary.LittleEndian.PutUint64(bytes, x)
	return bytes[:length]
}

// Bytes1 returns integer x to bytes in little-endian format, x.to_bytes(1, 'little').
func Bytes1(x uint64) []byte {
	bytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(bytes, x)
	return bytes[:1]
}

// Bytes2 returns integer x to bytes in little-endian format, x.to_bytes(2, 'little').
func Bytes2(x uint64) []byte {
	bytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(bytes, x)
	return bytes[:2]
}

// Bytes3 returns integer x to bytes in little-endian format, x.to_bytes(3, 'little').
func Bytes3(x uint64) []byte {
	bytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(bytes, x)
	return bytes[:3]
}

// Bytes4 returns integer x to bytes in little-endian format, x.to_bytes(4, 'little').
func Bytes4(x uint64) []byte {
	bytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(bytes, x)
	return bytes[:4]
}

// Bytes8 returns integer x to bytes in little-endian format, x.to_bytes(8, 'little').
func Bytes8(x uint64) []byte {
	bytes := make([]byte, 8)
	binary.LittleEndian.PutUint64(bytes, x)
	return bytes
}

// Bytes32 returns integer x to bytes in little-endian format, x.to_bytes(32, 'little').
func Bytes32(x uint64) []byte {
	bytes := make([]byte, 32)
	binary.LittleEndian.PutUint64(bytes, x)
	return bytes
}

// FromBytes4 returns an integer which is stored in the little-endian format(4, 'little')
// from a byte array.
func FromBytes4(x []byte) uint64 {
	if len(x) < 4 {
		return 0
	}
	empty4bytes := make([]byte, 4)
	return binary.LittleEndian.Uint64(append(x[:4], empty4bytes...))
}

// FromBytes8 returns an integer which is stored in the little-endian format(8, 'little')
// from a byte array.
func FromBytes8(x []byte) uint64 {
	if len(x) < 8 {
		return 0
	}
	return binary.LittleEndian.Uint64(x)
}

// ToBytes4 is a convenience method for converting a byte slice to a fix
// sized 4 byte array. This method will truncate the input if it is larger
// than 4 bytes.
func ToBytes4(x []byte) [4]byte {
	var y [4]byte
	copy(y[:], x)
	return y
}

// ToBytes32 is a convenience method for converting a byte slice to a fix
// sized 32 byte array. This method will truncate the input if it is larger
// than 32 bytes.
func ToBytes32(x []byte) [32]byte {
	var y [32]byte
	copy(y[:], x)
	return y
}

// ToBytes48 is a convenience method for converting a byte slice to a fix
// sized 48 byte array. This method will truncate the input if it is larger
// than 48 bytes.
func ToBytes48(x []byte) [48]byte {
	var y [48]byte
	copy(y[:], x)
	return y
}

// ToBytes64 is a convenience method for converting a byte slice to a fix
// sized 64 byte array. This method will truncate the input if it is larger
// than 64 bytes.
func ToBytes64(x []byte) [64]byte {
	var y [64]byte
	copy(y[:], x)
	return y
}

// ToBytes96 is a convenience method for converting a byte slice to a fix
// sized 96 byte array. This method will truncate the input if it is larger
// than 96 bytes.
func ToBytes96(x []byte) [96]byte {
	var y [96]byte
	copy(y[:], x)
	return y
}

// ToBool is a convenience method for converting a byte to a bool.
// This method will use the first bit of the 0 byte to generate the returned value.
func ToBool(x byte) bool {
	return x&1 == 1
}

// FromBytes2 returns an integer which is stored in the little-endian format(2, 'little')
// from a byte array.
func FromBytes2(x []byte) uint16 {
	if len(x) < 2 {
		return 0
	}
	return binary.LittleEndian.Uint16(x[:2])
}

// FromBool is a convenience method for converting a bool to a byte.
// This method will use the first bit to generate the returned value.
func FromBool(x bool) byte {
	if x {
		return 1
	}
	return 0
}

// FromBytes48 is a convenience method for converting a fixed-size byte array
// to a byte slice.
func FromBytes48(x [48]byte) []byte {
	return x[:]
}

// FromBytes48Array is a convenience method for converting an array of
// fixed-size byte arrays to an array of byte slices.
func FromBytes48Array(x [][48]byte) [][]byte {
	y := make([][]byte, len(x))
	for i := range x {
		y[i] = x[i][:]
	}
	return y
}

// Trunc truncates the byte slices to 6 bytes.
func Trunc(x []byte) []byte {
	if len(x) > 6 {
		return x[:6]
	}
	return x
}

// ToLowInt64 returns the lowest 8 bytes interpreted as little endian.
func ToLowInt64(x []byte) int64 {
	if len(x) < 8 {
		return 0
	}
	// Use the first 8 bytes.
	x = x[:8]
	return int64(binary.LittleEndian.Uint64(x)) // lint:ignore uintcast -- A negative number might be the expected result.
}

// SafeCopyRootAtIndex takes a copy of an 32-byte slice in a slice of byte slices. Returns error if index out of range.
func SafeCopyRootAtIndex(input [][]byte, idx uint64) ([]byte, error) {
	if input == nil {
		return nil, nil
	}

	if uint64(len(input)) <= idx {
		return nil, fmt.Errorf("index %d out of range", idx)
	}
	item := make([]byte, 32)
	copy(item, input[idx])
	return item, nil
}

// SafeCopyBytes will copy and return a non-nil byte array, otherwise it returns nil.
func SafeCopyBytes(cp []byte) []byte {
	if cp != nil {
		copied := make([]byte, len(cp))
		copy(copied, cp)
		return copied
	}
	return nil
}

// SafeCopy2dBytes will copy and return a non-nil 2d byte array, otherwise it returns nil.
func SafeCopy2dBytes(ary [][]byte) [][]byte {
	if ary != nil {
		copied := make([][]byte, len(ary))
		for i, a := range ary {
			copied[i] = SafeCopyBytes(a)
		}
		return copied
	}
	return nil
}

// SafeCopy2d32Bytes will copy and return a non-nil 2d byte array, otherwise it returns nil.
func SafeCopy2d32Bytes(ary [][32]byte) [][32]byte {
	if ary != nil {
		copied := make([][32]byte, len(ary))
		copy(copied, ary)
		return copied
	}
	return nil
}

// ReverseBytes32Slice will reverse the provided slice's order.
func ReverseBytes32Slice(arr [][32]byte) [][32]byte {
	for i, j := 0, len(arr)-1; i < j; i, j = i+1, j-1 {
		arr[i], arr[j] = arr[j], arr[i]
	}
	return arr
}

// PadTo pads a byte slice to the given size. If the byte slice is larger than the given size, the
// original slice is returned.
func PadTo(b []byte, size int) []byte {
	if len(b) > size {
		return b
	}
	return append(b, make([]byte, size-len(b))...)
}

// SetBit sets the index `i` of bitlist `b` to 1.
// It grows and returns a longer bitlist with 1 set
// if index `i` is out of range.
func SetBit(b []byte, i int) []byte {
	if i >= len(b)*8 {
		h := (i + (8 - i%8)) / 8
		b = append(b, make([]byte, h-len(b))...)
	}

	bit := uint8(1 << (i % 8))
	b[i/8] |= bit
	return b
}

// ClearBit clears the index `i` of bitlist `b`.
// Returns the original bitlist if the index `i`
// is out of range.
func ClearBit(b []byte, i int) []byte {
	if i >= len(b)*8 || i < 0 {
		return b
	}

	bit := uint8(1 << (i % 8))
	b[i/8] &^= bit
	return b
}

// MakeEmptyBitlists returns an empty bitlist with
// input size `i`.
func MakeEmptyBitlists(i int) []byte {
	return make([]byte, (i+(8-i%8))/8)
}

// HighestBitIndex returns the index of the highest
// bit set from bitlist `b`.
func HighestBitIndex(b []byte) (int, error) {
	if len(b) == 0 {
		return 0, errors.New("input list can't be empty or nil")
	}

	for i := len(b) - 1; i >= 0; i-- {
		if b[i] == 0 {
			continue
		}
		return bits.Len8(b[i]) + (i * 8), nil
	}

	return 0, nil
}

// HighestBitIndexAt returns the index of the highest
// bit set from bitlist `b` that is at `index` (inclusive).
func HighestBitIndexAt(b []byte, index int) (int, error) {
	bLength := len(b)
	if b == nil || bLength == 0 {
		return 0, errors.New("input list can't be empty or nil")
	}
	if index < 0 {
		return 0, errors.Errorf("index is negative: %d", index)
	}

	start := index / 8
	if start >= bLength {
		start = bLength - 1
	}

	mask := byte(1<<(index%8) - 1)
	for i := start; i >= 0; i-- {
		if index/8 > i {
			mask = 0xff
		}
		masked := b[i] & mask
		minBitsMasked := bits.Len8(masked)
		if b[i] == 0 || (minBitsMasked == 0 && index/8 <= i) {
			continue
		}

		return minBitsMasked + (i * 8), nil
	}

	return 0, nil
}

// Uint64ToBytesLittleEndian conversion.
func Uint64ToBytesLittleEndian(i uint64) []byte {
	buf := make([]byte, 8)
	binary.LittleEndian.PutUint64(buf, i)
	return buf
}

// Uint64ToBytesBigEndian conversion.
func Uint64ToBytesBigEndian(i uint64) []byte {
	buf := make([]byte, 8)
	binary.BigEndian.PutUint64(buf, i)
	return buf
}

// BytesToUint64BigEndian conversion. Returns 0 if empty bytes or byte slice with length less
// than 8.
func BytesToUint64BigEndian(b []byte) uint64 {
	if len(b) < 8 { // This will panic otherwise.
		return 0
	}
	return binary.BigEndian.Uint64(b)
}

// EpochToBytesLittleEndian conversion.
func EpochToBytesLittleEndian(i types.Epoch) []byte {
	return Uint64ToBytesLittleEndian(uint64(i))
}

// EpochToBytesBigEndian conversion.
func EpochToBytesBigEndian(i types.Epoch) []byte {
	return Uint64ToBytesBigEndian(uint64(i))
}

// BytesToEpochBigEndian conversion.
func BytesToEpochBigEndian(b []byte) types.Epoch {
	return types.Epoch(BytesToUint64BigEndian(b))
}

// SlotToBytesBigEndian conversion.
func SlotToBytesBigEndian(i types.Slot) []byte {
	return Uint64ToBytesBigEndian(uint64(i))
}

// BytesToSlotBigEndian conversion.
func BytesToSlotBigEndian(b []byte) types.Slot {
	return types.Slot(BytesToUint64BigEndian(b))
}

// IsHex checks whether the byte array is a hex number prefixed with '0x'.
func IsHex(b []byte) bool {
	if b == nil {
		return false
	}
	return hexRegex.Match(b)
}

// ReverseByteOrder Switch the endianness of a byte slice by reversing its order.
// this function does not modify the actual input bytes.
func ReverseByteOrder(input []byte) []byte {
	b := make([]byte, len(input))
	copy(b, input)
	for i := 0; i < len(b)/2; i++ {
		b[i], b[len(b)-i-1] = b[len(b)-i-1], b[i]
	}
	return b
}

// ZeroRoot returns whether or not a root is of proper length and non-zero hash.
func ZeroRoot(root []byte) bool {
	return string(make([]byte, fieldparams.RootLength)) == string(root)
}

// IsRoot checks whether the byte array is a root.
func IsRoot(root []byte) bool {
	return len(root) == fieldparams.RootLength
}

// IsValidRoot checks whether the byte array is a valid root.
func IsValidRoot(root []byte) bool {
	return IsRoot(root) && !ZeroRoot(root)
}
