package buffer

import (
	"bytes"
	"encoding/base64"
	"encoding/binary"
	"encoding/hex"
	"math"
	"math/big"
	"reflect"
	"strconv"

	"github.com/dop251/goja"
	"github.com/dop251/goja_nodejs/errors"
	"github.com/dop251/goja_nodejs/goutil"
	"github.com/dop251/goja_nodejs/require"

	"github.com/dop251/base64dec"
	"golang.org/x/text/encoding/unicode"
)

const ModuleName = "buffer"

type Buffer struct {
	r *goja.Runtime

	bufferCtorObj *goja.Object

	uint8ArrayCtorObj *goja.Object
	uint8ArrayCtor    goja.Constructor
}

var (
	symApi = goja.NewSymbol("api")
)

var (
	reflectTypeArrayBuffer = reflect.TypeOf(goja.ArrayBuffer{})
	reflectTypeString      = reflect.TypeOf("")
	reflectTypeInt         = reflect.TypeOf(int64(0))
	reflectTypeFloat       = reflect.TypeOf(0.0)
	reflectTypeBytes       = reflect.TypeOf(([]byte)(nil))
)

func Enable(runtime *goja.Runtime) {
	runtime.Set("Buffer", require.Require(runtime, ModuleName).ToObject(runtime).Get("Buffer"))
}

func Bytes(r *goja.Runtime, v goja.Value) []byte {
	var b []byte
	err := r.ExportTo(v, &b)
	if err != nil {
		return []byte(v.String())
	}
	return b
}

func mod(r *goja.Runtime) *goja.Object {
	res := r.Get("Buffer")
	if res == nil {
		res = require.Require(r, ModuleName).ToObject(r).Get("Buffer")
	}
	m, ok := res.(*goja.Object)
	if !ok {
		panic(r.NewTypeError("Could not extract Buffer"))
	}
	return m
}

func api(mod *goja.Object) *Buffer {
	if s := mod.GetSymbol(symApi); s != nil {
		b, _ := s.Export().(*Buffer)
		return b
	}

	return nil
}

func GetApi(r *goja.Runtime) *Buffer {
	return api(mod(r))
}

func DecodeBytes(r *goja.Runtime, arg, enc goja.Value) []byte {
	switch arg.ExportType() {
	case reflectTypeArrayBuffer:
		return arg.Export().(goja.ArrayBuffer).Bytes()
	case reflectTypeString:
		var codec StringCodec
		if !goja.IsUndefined(enc) {
			codec = stringCodecs[enc.String()]
		}
		if codec == nil {
			codec = utf8Codec
		}
		return codec.DecodeAppend(arg.String(), nil)
	default:
		if o, ok := arg.(*goja.Object); ok {
			if o.ExportType() == reflectTypeBytes {
				return o.Export().([]byte)
			}
		}
	}
	panic(errors.NewTypeError(r, errors.ErrCodeInvalidArgType, "The \"data\" argument must be of type string or an instance of Buffer, TypedArray, or DataView."))
}

func WrapBytes(r *goja.Runtime, data []byte) *goja.Object {
	m := mod(r)
	if api := api(m); api != nil {
		return api.WrapBytes(data)
	}
	if from, ok := goja.AssertFunction(m.Get("from")); ok {
		ab := r.NewArrayBuffer(data)
		v, err := from(m, r.ToValue(ab))
		if err != nil {
			panic(err)
		}
		return v.ToObject(r)
	}
	panic(r.NewTypeError("Buffer.from is not a function"))
}

// EncodeBytes returns the given byte slice encoded as string with the given encoding. If encoding
// is not specified or not supported, returns a Buffer that wraps the data.
func EncodeBytes(r *goja.Runtime, data []byte, enc goja.Value) goja.Value {
	var codec StringCodec
	if !goja.IsUndefined(enc) {
		codec = StringCodecByName(enc.String())
	}
	if codec != nil {
		return r.ToValue(codec.Encode(data))
	}
	return WrapBytes(r, data)
}

func (b *Buffer) WrapBytes(data []byte) *goja.Object {
	return b.fromBytes(data)
}

func (b *Buffer) ctor(call goja.ConstructorCall) (res *goja.Object) {
	arg := call.Argument(0)
	switch arg.ExportType() {
	case reflectTypeInt, reflectTypeFloat:
		panic(b.r.NewTypeError("Calling the Buffer constructor with numeric argument is not implemented yet"))
		// TODO implement
	}
	return b._from(call.Arguments...)
}

type StringCodec interface {
	DecodeAppend(string, []byte) []byte
	Encode([]byte) string
	Decode(s string) []byte
}

type hexCodec struct{}

func (hexCodec) DecodeAppend(s string, b []byte) []byte {
	l := hex.DecodedLen(len(s))
	dst, res := expandSlice(b, l)
	n, err := hex.Decode(dst, []byte(s))
	if err != nil {
		res = res[:len(b)+n]
	}
	return res
}

func (hexCodec) Decode(s string) []byte {
	n, _ := hex.DecodeString(s)
	return n
}
func (hexCodec) Encode(b []byte) string {
	return hex.EncodeToString(b)
}

type _utf8Codec struct{}

func (c _utf8Codec) DecodeAppend(s string, b []byte) []byte {
	r := c.Decode(s)
	dst, res := expandSlice(b, len(r))
	copy(dst, r)
	return res
}

func (_utf8Codec) Decode(s string) []byte {
	r, _ := unicode.UTF8.NewEncoder().String(s)
	return []byte(r)
}
func (_utf8Codec) Encode(b []byte) string {
	r, _ := unicode.UTF8.NewDecoder().Bytes(b)
	return string(r)
}

type base64Codec struct{}

type base64UrlCodec struct {
	base64Codec
}

func (base64Codec) DecodeAppend(s string, b []byte) []byte {
	res, _ := Base64DecodeAppend(b, s)
	return res
}

func (base64Codec) Decode(s string) []byte {
	res, _ := base64.StdEncoding.DecodeString(s)
	return res
}
func (base64Codec) Encode(b []byte) string {
	return base64.StdEncoding.EncodeToString(b)
}

func (base64UrlCodec) Encode(b []byte) string {
	return base64.RawURLEncoding.EncodeToString(b)
}

var utf8Codec StringCodec = _utf8Codec{}

var stringCodecs = map[string]StringCodec{
	"hex":       hexCodec{},
	"utf8":      utf8Codec,
	"utf-8":     utf8Codec,
	"base64":    base64Codec{},
	"base64Url": base64UrlCodec{},
}

func expandSlice(b []byte, l int) (dst, res []byte) {
	if cap(b)-len(b) < l {
		b1 := make([]byte, len(b)+l)
		copy(b1, b)
		dst = b1[len(b):]
		res = b1
	} else {
		dst = b[len(b) : len(b)+l]
		res = b[:len(b)+l]
	}
	return
}

func Base64DecodeAppend(dst []byte, src string) ([]byte, error) {
	l := base64.RawStdEncoding.DecodedLen(len(src))
	d, res := expandSlice(dst, l)
	n, err := base64dec.DecodeBase64(d, src)

	res = res[:len(dst)+n]
	return res, err
}

func (b *Buffer) fromString(str, enc string) *goja.Object {
	codec := stringCodecs[enc]
	if codec == nil {
		codec = utf8Codec
	}
	return b.fromBytes(codec.DecodeAppend(str, nil))
}

func (b *Buffer) fromBytes(data []byte) *goja.Object {
	o, err := b.uint8ArrayCtor(b.bufferCtorObj, b.r.ToValue(b.r.NewArrayBuffer(data)))
	if err != nil {
		panic(err)
	}
	return o
}

func (b *Buffer) _from(args ...goja.Value) *goja.Object {
	if len(args) == 0 {
		panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received undefined"))
	}
	arg := args[0]
	switch arg.ExportType() {
	case reflectTypeArrayBuffer:
		v, err := b.uint8ArrayCtor(b.bufferCtorObj, args...)
		if err != nil {
			panic(err)
		}
		return v
	case reflectTypeString:
		var enc string
		if len(args) > 1 {
			enc = args[1].String()
		}
		return b.fromString(arg.String(), enc)
	default:
		if o, ok := arg.(*goja.Object); ok {
			if o.ExportType() == reflectTypeBytes {
				bb, _ := o.Export().([]byte)
				a := make([]byte, len(bb))
				copy(a, bb)
				return b.fromBytes(a)
			} else {
				if f, ok := goja.AssertFunction(o.Get("valueOf")); ok {
					valueOf, err := f(o)
					if err != nil {
						panic(err)
					}
					if valueOf != o {
						args[0] = valueOf
						return b._from(args...)
					}
				}

				if s := o.GetSymbol(goja.SymToPrimitive); s != nil {
					if f, ok := goja.AssertFunction(s); ok {
						str, err := f(o, b.r.ToValue("string"))
						if err != nil {
							panic(err)
						}
						args[0] = str
						return b._from(args...)
					}
				}
			}
			// array-like
			if v := o.Get("length"); v != nil {
				length := int(v.ToInteger())
				a := make([]byte, length)
				for i := 0; i < length; i++ {
					item := o.Get(strconv.Itoa(i))
					if item != nil {
						a[i] = byte(item.ToInteger())
					}
				}
				return b.fromBytes(a)
			}
		}
	}
	panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received %s", arg))
}

func (b *Buffer) from(call goja.FunctionCall) goja.Value {
	return b._from(call.Arguments...)
}

func StringCodecByName(name string) StringCodec {
	return stringCodecs[name]
}

func (b *Buffer) getStringCodec(enc goja.Value) (codec StringCodec) {
	if !goja.IsUndefined(enc) {
		codec = stringCodecs[enc.String()]
		if codec == nil {
			panic(errors.NewTypeError(b.r, "ERR_UNKNOWN_ENCODING", "Unknown encoding: %s", enc))
		}
	} else {
		codec = utf8Codec
	}
	return
}

func (b *Buffer) fill(buf []byte, fill string, enc goja.Value) []byte {
	codec := b.getStringCodec(enc)
	b1 := codec.DecodeAppend(fill, buf[:0])
	if len(b1) > len(buf) {
		return b1[:len(buf)]
	}
	for i := len(b1); i < len(buf); {
		i += copy(buf[i:], buf[:i])
	}
	return buf
}

func (b *Buffer) alloc(call goja.FunctionCall) goja.Value {
	arg0 := call.Argument(0)
	size := -1
	if goja.IsNumber(arg0) {
		size = int(arg0.ToInteger())
	}
	if size < 0 {
		panic(errors.NewArgumentNotNumberTypeError(b.r, "size"))
	}
	fill := call.Argument(1)
	buf := make([]byte, size)
	if !goja.IsUndefined(fill) {
		if goja.IsString(fill) {
			var enc goja.Value
			if a := call.Argument(2); goja.IsString(a) {
				enc = a
			} else {
				enc = goja.Undefined()
			}
			buf = b.fill(buf, fill.String(), enc)
		} else {
			fill = fill.ToNumber()
			if !goja.IsNaN(fill) && !goja.IsInfinity(fill) {
				fillByte := byte(fill.ToInteger())
				if fillByte != 0 {
					for i := range buf {
						buf[i] = fillByte
					}
				}
			}
		}
	}
	return b.fromBytes(buf)
}

func (b *Buffer) proto_toString(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	codec := b.getStringCodec(call.Argument(0))
	start := goutil.CoercedIntegerArgument(call, 1, 0, 0)

	// Node's Buffer class makes this zero if it is negative
	if start < 0 {
		start = 0
	} else if start >= int64(len(bb)) {
		// returns an empty string if start is beyond the length of the buffer
		return b.r.ToValue("")
	}

	// NOTE that Node will default to the length of the buffer, but uses 0 for type mismatch defaults
	end := goutil.CoercedIntegerArgument(call, 2, int64(len(bb)), 0)
	if end < 0 || start >= end {
		// returns an empty string if end < 0 or start >= end
		return b.r.ToValue("")
	} else if end > int64(len(bb)) {
		// and Node ensures you don't go past the Buffer
		end = int64(len(bb))
	}

	return b.r.ToValue(codec.Encode(bb[start:end]))
}

func (b *Buffer) concat(call goja.FunctionCall) goja.Value {
	var bufs []goja.Value
	b.r.ForOf(goutil.RequiredArrayArgument(b.r, call, "list", 0), func(v goja.Value) bool {
		bufs = append(bufs, v)
		return true
	})
	if len(bufs) == 0 {
		return b.fromBytes([]byte{})
	}
	var totalLen, totalLenArg int64
	if len(call.Arguments) > 1 {
		totalLenArg = goutil.RequiredStrictIntegerArgument(b.r, call, "totalLen", 1)
		if totalLenArg < 0 {
			panic(errors.NewRangeError(b.r, errors.ErrCodeOutOfRange, "The value of \"length\" is out of range. It must be >= 0 && <= 9007199254740991. Received %d", totalLenArg))
		}
	} else {
		totalLenArg = -1
	}

	var byteBufs [][]byte
	for i, buf := range bufs {
		if !b.r.InstanceOf(buf, b.uint8ArrayCtorObj) {
			panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The \"list[%d]\" argument must be an instance of Buffer or Uint8Array.", i))
		}
		bufBytes := Bytes(b.r, buf)
		byteBufs = append(byteBufs, bufBytes)
		totalLen += int64(len(bufBytes))
	}

	if totalLenArg >= 0 {
		totalLen = totalLenArg
	}
	res := make([]byte, totalLen)
	offset := 0
	for _, buf := range byteBufs {
		n := copy(res[offset:], buf)
		if n != len(buf) {
			break
		}
		offset += n
	}
	return b.fromBytes(res)
}

func (b *Buffer) RequiredBufferArgument(call goja.FunctionCall, argName string, argIdx int) []byte {
	arg := call.Argument(argIdx)
	if b.r.InstanceOf(arg, b.uint8ArrayCtorObj) {
		return Bytes(b.r, arg)
	}
	panic(errors.NewTypeError(b.r, errors.ErrCodeInvalidArgType, "The %q argument must be an instance of Buffer or Uint8Array.", argName))
}

func (b *Buffer) proto_equals(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	otherBytes := b.RequiredBufferArgument(call, "otherBuffer", 0)
	return b.r.ToValue(bytes.Equal(bb, otherBytes))
}

// readBigInt64BE reads a big-endian 64-bit signed integer from the buffer
func (b *Buffer) readBigInt64BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 8)
	value := int64(binary.BigEndian.Uint64(bb[offset : offset+8]))

	return b.r.ToValue(big.NewInt(value))
}

// readBigInt64LE reads a little-endian 64-bit signed integer from the buffer
func (b *Buffer) readBigInt64LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 8)
	value := int64(binary.LittleEndian.Uint64(bb[offset : offset+8]))

	return b.r.ToValue(big.NewInt(value))
}

// readBigUInt64BE reads a big-endian 64-bit unsigned integer from the buffer
func (b *Buffer) readBigUInt64BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 8)
	value := binary.BigEndian.Uint64(bb[offset : offset+8])

	return b.r.ToValue(new(big.Int).SetUint64(value))
}

// readBigUInt64LE reads a little-endian 64-bit unsigned integer from the buffer
func (b *Buffer) readBigUInt64LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 8)
	value := binary.LittleEndian.Uint64(bb[offset : offset+8])

	return b.r.ToValue(new(big.Int).SetUint64(value))
}

// readDoubleBE reads a big-endian 64-bit floating-point number from the buffer
func (b *Buffer) readDoubleBE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 8)
	value := binary.BigEndian.Uint64(bb[offset : offset+8])

	return b.r.ToValue(math.Float64frombits(value))
}

// readDoubleLE reads a little-endian 64-bit floating-point number from the buffer
func (b *Buffer) readDoubleLE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 8)
	value := binary.LittleEndian.Uint64(bb[offset : offset+8])

	return b.r.ToValue(math.Float64frombits(value))
}

// readFloatBE reads a big-endian 32-bit floating-point number from the buffer
func (b *Buffer) readFloatBE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 4)
	value := binary.BigEndian.Uint32(bb[offset : offset+4])

	return b.r.ToValue(math.Float32frombits(value))
}

// readFloatLE reads a little-endian 32-bit floating-point number from the buffer
func (b *Buffer) readFloatLE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 4)
	value := binary.LittleEndian.Uint32(bb[offset : offset+4])

	return b.r.ToValue(math.Float32frombits(value))
}

// readInt8 reads an 8-bit signed integer from the buffer
func (b *Buffer) readInt8(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 1)
	value := int8(bb[offset])

	return b.r.ToValue(value)
}

// readInt16BE reads a big-endian 16-bit signed integer from the buffer
func (b *Buffer) readInt16BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 2)
	value := int16(binary.BigEndian.Uint16(bb[offset : offset+2]))

	return b.r.ToValue(value)
}

// readInt16LE reads a little-endian 16-bit signed integer from the buffer
func (b *Buffer) readInt16LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 2)
	value := int16(binary.LittleEndian.Uint16(bb[offset : offset+2]))

	return b.r.ToValue(value)
}

// readInt32BE reads a big-endian 32-bit signed integer from the buffer
func (b *Buffer) readInt32BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 4)
	value := int32(binary.BigEndian.Uint32(bb[offset : offset+4]))

	return b.r.ToValue(value)
}

// readInt32LE reads a little-endian 32-bit signed integer from the buffer
func (b *Buffer) readInt32LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 4)
	value := int32(binary.LittleEndian.Uint32(bb[offset : offset+4]))

	return b.r.ToValue(value)
}

// readIntBE reads a big-endian signed integer of variable byte length
func (b *Buffer) readIntBE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset, byteLength := b.getVariableLengthReadArguments(call, bb)

	var value int64
	for i := int64(0); i < byteLength; i++ {
		value = (value << 8) | int64(bb[offset+i])
	}

	value = signExtend(value, byteLength)

	return b.r.ToValue(value)
}

// readIntLE reads a little-endian signed integer of variable byte length
func (b *Buffer) readIntLE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset, byteLength := b.getVariableLengthReadArguments(call, bb)

	var value int64
	for i := byteLength - 1; i >= 0; i-- {
		value = (value << 8) | int64(bb[offset+i])
	}

	value = signExtend(value, byteLength)

	return b.r.ToValue(value)
}

// readUInt8 reads an 8-bit unsigned integer from the buffer
func (b *Buffer) readUInt8(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 1)
	value := bb[offset]

	return b.r.ToValue(value)
}

// readUInt16BE reads a big-endian 16-bit unsigned integer from the buffer
func (b *Buffer) readUInt16BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 2)
	value := binary.BigEndian.Uint16(bb[offset : offset+2])

	return b.r.ToValue(value)
}

// readUInt16LE reads a little-endian 16-bit unsigned integer from the buffer
func (b *Buffer) readUInt16LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 2)
	value := binary.LittleEndian.Uint16(bb[offset : offset+2])

	return b.r.ToValue(value)
}

// readUInt32BE reads a big-endian 32-bit unsigned integer from the buffer
func (b *Buffer) readUInt32BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 4)
	value := binary.BigEndian.Uint32(bb[offset : offset+4])

	return b.r.ToValue(value)
}

// readUInt32LE reads a little-endian 32-bit unsigned integer from the buffer
func (b *Buffer) readUInt32LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset := b.getOffsetArgument(call, 0, bb, 4)
	value := binary.LittleEndian.Uint32(bb[offset : offset+4])

	return b.r.ToValue(value)
}

// readUIntBE reads a big-endian unsigned integer of variable byte length
func (b *Buffer) readUIntBE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset, byteLength := b.getVariableLengthReadArguments(call, bb)

	var value uint64
	for i := int64(0); i < byteLength; i++ {
		value = (value << 8) | uint64(bb[offset+i])
	}

	return b.r.ToValue(value)
}

// readUIntLE reads a little-endian unsigned integer of variable byte length
func (b *Buffer) readUIntLE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	offset, byteLength := b.getVariableLengthReadArguments(call, bb)

	var value uint64
	for i := byteLength - 1; i >= 0; i-- {
		value = (value << 8) | uint64(bb[offset+i])
	}

	return b.r.ToValue(value)
}

// write will write a string to the Buffer at offset according to the character encoding. The length parameter is
// the number of bytes to write. If buffer did not contain enough space to fit the entire string, only part of string
// will be written.
func (b *Buffer) write(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	str := goutil.RequiredStringArgument(b.r, call, "string", 0)
	// note that we are passing in zero for numBytes, since the length parameter, which depends on offset,
	// will dictate the number of bytes
	offset := b.getOffsetArgument(call, 1, bb, 0)
	// the length defaults to the size of the buffer - offset
	maxLength := int64(len(bb)) - offset
	length := goutil.OptionalIntegerArgument(b.r, call, "length", 2, maxLength)
	codec := b.getStringCodec(call.Argument(3))

	raw := codec.Decode(str)
	if int64(len(raw)) < length {
		// make sure we only write up to raw bytes
		length = int64(len(raw))
	}
	n := copy(bb[offset:], raw[:length])
	return b.r.ToValue(n)
}

// writeBigInt64BE writes a big-endian 64-bit signed integer to the buffer
func (b *Buffer) writeBigInt64BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredBigIntArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 8)

	intValue := value.Int64()
	binary.BigEndian.PutUint64(bb[offset:offset+8], uint64(intValue))

	return b.r.ToValue(offset + 8)
}

// writeBigInt64LE writes a little-endian 64-bit signed integer to the buffer
func (b *Buffer) writeBigInt64LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredBigIntArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 8)

	intValue := value.Int64()
	binary.LittleEndian.PutUint64(bb[offset:offset+8], uint64(intValue))

	return b.r.ToValue(offset + 8)
}

// writeBigUInt64BE writes a big-endian 64-bit unsigned integer to the buffer
func (b *Buffer) writeBigUInt64BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredBigIntArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 8)

	uintValue := value.Uint64()
	binary.BigEndian.PutUint64(bb[offset:offset+8], uintValue)

	return b.r.ToValue(offset + 8)
}

// writeBigUInt64LE writes a little-endian 64-bit unsigned integer to the buffer
func (b *Buffer) writeBigUInt64LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredBigIntArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 8)

	uintValue := value.Uint64()
	binary.LittleEndian.PutUint64(bb[offset:offset+8], uintValue)

	return b.r.ToValue(offset + 8)
}

// writeDoubleBE writes a big-endian 64-bit double to the buffer
func (b *Buffer) writeDoubleBE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredFloatArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 8)

	bits := math.Float64bits(value)
	binary.BigEndian.PutUint64(bb[offset:offset+8], bits)

	return b.r.ToValue(offset + 8)
}

// writeDoubleLE writes a little-endian 64-bit double to the buffer
func (b *Buffer) writeDoubleLE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredFloatArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 8)

	bits := math.Float64bits(value)
	binary.LittleEndian.PutUint64(bb[offset:offset+8], bits)

	return b.r.ToValue(offset + 8)
}

// writeFloatBE writes a big-endian 32-bit float to the buffer
func (b *Buffer) writeFloatBE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredFloatArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 4)

	b.ensureWithinFloat32Range(value)

	bits := math.Float32bits(float32(value))
	binary.BigEndian.PutUint32(bb[offset:offset+4], bits)

	return b.r.ToValue(offset + 4)
}

// writeFloatLE writes a little-endian 32-bit floating-point number to the buffer
func (b *Buffer) writeFloatLE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredFloatArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 4)

	b.ensureWithinFloat32Range(value)

	bits := math.Float32bits(float32(value))
	binary.LittleEndian.PutUint32(bb[offset:offset+4], bits)

	return b.r.ToValue(offset + 4)
}

// writeInt8 writes an 8-bit signed integer to the buffer
func (b *Buffer) writeInt8(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 1)

	if value < math.MinInt8 || value > math.MaxInt8 {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}

	bb[offset] = byte(int8(value))

	return b.r.ToValue(offset + 1)
}

// writeInt16BE writes a big-endian 16-bit signed integer to the buffer
func (b *Buffer) writeInt16BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 2)

	b.ensureWithinInt16Range(value)

	binary.BigEndian.PutUint16(bb[offset:offset+2], uint16(value))

	return b.r.ToValue(offset + 2)
}

// writeInt16LE writes a little-endian 16-bit signed integer to the buffer
func (b *Buffer) writeInt16LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 2)

	b.ensureWithinInt16Range(value)

	binary.LittleEndian.PutUint16(bb[offset:offset+2], uint16(value))

	return b.r.ToValue(offset + 2)
}

// writeInt32BE writes a big-endian 32-bit signed integer to the buffer
func (b *Buffer) writeInt32BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 4)

	b.ensureWithinInt32Range(value)

	binary.BigEndian.PutUint32(bb[offset:offset+4], uint32(value))

	return b.r.ToValue(offset + 4)
}

// writeInt32LE writes a little-endian 32-bit signed integer to the buffer
func (b *Buffer) writeInt32LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 4)

	b.ensureWithinInt32Range(value)

	binary.LittleEndian.PutUint32(bb[offset:offset+4], uint32(value))

	return b.r.ToValue(offset + 4)
}

// writeIntBE writes a big-endian signed integer of variable byte length
func (b *Buffer) writeIntBE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset, byteLength := b.getVariableLengthWriteArguments(call, bb)

	b.ensureWithinIntRange(byteLength, value)

	// Write bytes in big-endian order (most significant byte first)
	for i := int64(0); i < byteLength; i++ {
		shift := uint(8 * (byteLength - 1 - i))
		bb[offset+i] = byte(value >> shift)
	}

	return b.r.ToValue(offset + byteLength)
}

// writeIntLE writes a little-endian signed integer of variable byte length
func (b *Buffer) writeIntLE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset, byteLength := b.getVariableLengthWriteArguments(call, bb)

	b.ensureWithinIntRange(byteLength, value)

	// Write bytes in little-endian order
	for i := int64(0); i < byteLength; i++ {
		shift := uint(8 * i)
		bb[offset+i] = byte(value >> shift)
	}

	return b.r.ToValue(offset + byteLength)
}

// writeUInt8 writes an 8-bit unsigned integer to the buffer
func (b *Buffer) writeUInt8(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 1)

	if value < 0 || value > 255 {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}

	bb[offset] = uint8(value)

	return b.r.ToValue(offset + 1)
}

// writeUInt16BE writes a big-endian 16-bit unsigned integer to the buffer
func (b *Buffer) writeUInt16BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 2)

	b.ensureWithinUInt16Range(value)

	binary.BigEndian.PutUint16(bb[offset:offset+2], uint16(value))

	return b.r.ToValue(offset + 2)
}

// writeUInt16LE writes a little-endian 16-bit unsigned integer to the buffer
func (b *Buffer) writeUInt16LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 2)

	b.ensureWithinUInt16Range(value)

	binary.LittleEndian.PutUint16(bb[offset:offset+2], uint16(value))

	return b.r.ToValue(offset + 2)
}

// writeUInt32BE writes a big-endian 32-bit unsigned integer to the buffer
func (b *Buffer) writeUInt32BE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 4)

	b.ensureWithinUInt32Range(value)

	binary.BigEndian.PutUint32(bb[offset:offset+4], uint32(value))

	return b.r.ToValue(offset + 4)
}

// writeUInt32LE writes a little-endian 32-bit unsigned integer to the buffer
func (b *Buffer) writeUInt32LE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset := b.getOffsetArgument(call, 1, bb, 4)

	b.ensureWithinUInt32Range(value)

	binary.LittleEndian.PutUint32(bb[offset:offset+4], uint32(value))

	return b.r.ToValue(offset + 4)
}

// writeUIntBE writes a big-endian unsigned integer of variable byte length
func (b *Buffer) writeUIntBE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset, byteLength := b.getVariableLengthWriteArguments(call, bb)

	b.ensureWithinUIntRange(byteLength, value)

	// Write the bytes in big-endian order (most significant byte first)
	for i := int64(0); i < byteLength; i++ {
		shift := (byteLength - 1 - i) * 8
		bb[offset+i] = byte(value >> shift)
	}

	return b.r.ToValue(offset + byteLength)
}

// writeUIntLE writes a little-endian unsigned integer of variable byte length
func (b *Buffer) writeUIntLE(call goja.FunctionCall) goja.Value {
	bb := Bytes(b.r, call.This)
	value := goutil.RequiredIntegerArgument(b.r, call, "value", 0)
	offset, byteLength := b.getVariableLengthWriteArguments(call, bb)

	b.ensureWithinUIntRange(byteLength, value)

	// Write the bytes in little-endian order
	for i := int64(0); i < byteLength; i++ {
		shift := uint(8 * i)
		bb[offset+i] = byte(value >> shift)
	}

	return b.r.ToValue(offset + byteLength)
}

func (b *Buffer) getOffsetArgument(call goja.FunctionCall, argIndex int, bb []byte, numBytes int64) int64 {
	offset := goutil.OptionalIntegerArgument(b.r, call, "offset", argIndex, 0)

	if offset < 0 || offset+numBytes > int64(len(bb)) {
		panic(errors.NewArgumentOutOfRangeError(b.r, "offset", offset))
	}

	return offset
}

func (b *Buffer) getVariableLengthReadArguments(call goja.FunctionCall, bb []byte) (int64, int64) {
	return b.getVariableLengthArguments(call, bb, 0, 1)
}

func (b *Buffer) getVariableLengthWriteArguments(call goja.FunctionCall, bb []byte) (int64, int64) {
	return b.getVariableLengthArguments(call, bb, 1, 2)
}

func (b *Buffer) getVariableLengthArguments(call goja.FunctionCall, bb []byte, offsetArgIndex, byteLengthArgIndex int) (int64, int64) {
	offset := goutil.RequiredIntegerArgument(b.r, call, "offset", offsetArgIndex)
	byteLength := goutil.RequiredIntegerArgument(b.r, call, "byteLength", byteLengthArgIndex)

	if byteLength < 1 || byteLength > 6 {
		panic(errors.NewArgumentOutOfRangeError(b.r, "byteLength", byteLength))
	}
	if offset < 0 || offset+byteLength > int64(len(bb)) {
		panic(errors.NewArgumentOutOfRangeError(b.r, "offset", offset))
	}

	return offset, byteLength
}

func (b *Buffer) ensureWithinFloat32Range(value float64) {
	if value < -math.MaxFloat32 || value > math.MaxFloat32 {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}
}

func (b *Buffer) ensureWithinInt16Range(value int64) {
	if value < math.MinInt16 || value > math.MaxInt16 {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}
}

func (b *Buffer) ensureWithinInt32Range(value int64) {
	if value < math.MinInt32 || value > math.MaxInt32 {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}
}

// ensureWithinIntRange checks to make sure that value is within the integer range
// defined by the byteLength. Note that byteLength can be at most 6 bytes, so a
// 48 bit integer is the largest possible value.
func (b *Buffer) ensureWithinIntRange(byteLength, value int64) {
	// Calculate the valid range for the given byte length
	bits := byteLength * 8
	minValue := -(int64(1) << (bits - 1))
	maxValue := (int64(1) << (bits - 1)) - 1

	if value < minValue || value > maxValue {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}
}

func (b *Buffer) ensureWithinUInt16Range(value int64) {
	if value < 0 || value > math.MaxUint16 {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}
}

func (b *Buffer) ensureWithinUInt32Range(value int64) {
	if value < 0 || value > math.MaxUint32 {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}
}

// ensureWithinUIntRange checks to make sure that value is within the unsigned integer
// range defined by the byteLength. Note that byteLength can be at most 6 bytes, so a
// 48 bit unsigned integer is the largest possible value.
func (b *Buffer) ensureWithinUIntRange(byteLength, value int64) {
	// Validate that the value is within the valid range for the given byteLength
	maxValue := (int64(1) << (8 * byteLength)) - 1
	if value < 0 || value > maxValue {
		panic(errors.NewArgumentOutOfRangeError(b.r, "value", value))
	}
}

func signExtend(value int64, numBytes int64) int64 {
	// we don't have to turn this to a uint64 first because numBytes < 8 so
	// the sign bit will never pushed out of the int64 range
	return (value << (64 - 8*numBytes)) >> (64 - 8*numBytes)
}

func Require(runtime *goja.Runtime, module *goja.Object) {
	b := &Buffer{r: runtime}
	uint8Array := runtime.Get("Uint8Array")
	if c, ok := goja.AssertConstructor(uint8Array); ok {
		b.uint8ArrayCtor = c
	} else {
		panic(runtime.NewTypeError("Uint8Array is not a constructor"))
	}
	uint8ArrayObj := uint8Array.ToObject(runtime)

	ctor := runtime.ToValue(b.ctor).ToObject(runtime)
	ctor.SetPrototype(uint8ArrayObj)
	ctor.DefineDataPropertySymbol(symApi, runtime.ToValue(b), goja.FLAG_FALSE, goja.FLAG_FALSE, goja.FLAG_FALSE)
	b.bufferCtorObj = ctor
	b.uint8ArrayCtorObj = uint8ArrayObj

	proto := runtime.NewObject()
	proto.SetPrototype(uint8ArrayObj.Get("prototype").ToObject(runtime))
	proto.DefineDataProperty("constructor", ctor, goja.FLAG_TRUE, goja.FLAG_TRUE, goja.FLAG_FALSE)
	proto.Set("equals", b.proto_equals)
	proto.Set("toString", b.proto_toString)
	proto.Set("readBigInt64BE", b.readBigInt64BE)
	proto.Set("readBigInt64LE", b.readBigInt64LE)
	proto.Set("readBigUInt64BE", b.readBigUInt64BE)
	// aliases for readBigUInt64BE
	proto.Set("readBigUint64BE", b.readBigUInt64BE)

	proto.Set("readBigUInt64LE", b.readBigUInt64LE)
	// aliases for readBigUInt64LE
	proto.Set("readBigUint64LE", b.readBigUInt64LE)

	proto.Set("readDoubleBE", b.readDoubleBE)
	proto.Set("readDoubleLE", b.readDoubleLE)
	proto.Set("readFloatBE", b.readFloatBE)
	proto.Set("readFloatLE", b.readFloatLE)
	proto.Set("readInt8", b.readInt8)
	proto.Set("readInt16BE", b.readInt16BE)
	proto.Set("readInt16LE", b.readInt16LE)
	proto.Set("readInt32BE", b.readInt32BE)
	proto.Set("readInt32LE", b.readInt32LE)
	proto.Set("readIntBE", b.readIntBE)
	proto.Set("readIntLE", b.readIntLE)
	proto.Set("readUInt8", b.readUInt8)
	// aliases for readUInt8
	proto.Set("readUint8", b.readUInt8)

	proto.Set("readUInt16BE", b.readUInt16BE)
	// aliases for readUInt16BE
	proto.Set("readUint16BE", b.readUInt16BE)

	proto.Set("readUInt16LE", b.readUInt16LE)
	// aliases for readUInt16LE
	proto.Set("readUint16LE", b.readUInt16LE)

	proto.Set("readUInt32BE", b.readUInt32BE)
	// aliases for readUInt32BE
	proto.Set("readUint32BE", b.readUInt32BE)

	proto.Set("readUInt32LE", b.readUInt32LE)
	// aliases for readUInt32LE
	proto.Set("readUint32LE", b.readUInt32LE)

	proto.Set("readUIntBE", b.readUIntBE)
	// aliases for readUIntBE
	proto.Set("readUintBE", b.readUIntBE)

	proto.Set("readUIntLE", b.readUIntLE)
	// aliases for readUIntLE
	proto.Set("readUintLE", b.readUIntLE)
	proto.Set("write", b.write)
	proto.Set("writeBigInt64BE", b.writeBigInt64BE)
	proto.Set("writeBigInt64LE", b.writeBigInt64LE)
	proto.Set("writeBigUInt64BE", b.writeBigUInt64BE)
	// aliases for writeBigUInt64BE
	proto.Set("writeBigUint64BE", b.writeBigUInt64BE)

	proto.Set("writeBigUInt64LE", b.writeBigUInt64LE)
	// aliases for writeBigUInt64LE
	proto.Set("writeBigUint64LE", b.writeBigUInt64LE)

	proto.Set("writeDoubleBE", b.writeDoubleBE)
	proto.Set("writeDoubleLE", b.writeDoubleLE)
	proto.Set("writeFloatBE", b.writeFloatBE)
	proto.Set("writeFloatLE", b.writeFloatLE)
	proto.Set("writeInt8", b.writeInt8)
	proto.Set("writeInt16BE", b.writeInt16BE)
	proto.Set("writeInt16LE", b.writeInt16LE)
	proto.Set("writeInt32BE", b.writeInt32BE)
	proto.Set("writeInt32LE", b.writeInt32LE)
	proto.Set("writeIntBE", b.writeIntBE)
	proto.Set("writeIntLE", b.writeIntLE)
	proto.Set("writeUInt8", b.writeUInt8)
	// aliases for writeUInt8
	proto.Set("writeUint8", b.writeUInt8)

	proto.Set("writeUInt16BE", b.writeUInt16BE)
	// aliases for writeUInt16BE
	proto.Set("writeUint16BE", b.writeUInt16BE)

	proto.Set("writeUInt16LE", b.writeUInt16LE)
	// aliases for writeUInt16LE
	proto.Set("writeUint16LE", b.writeUInt16LE)

	proto.Set("writeUInt32BE", b.writeUInt32BE)
	// aliases for writeUInt32BE
	proto.Set("writeUint32BE", b.writeUInt32BE)

	proto.Set("writeUInt32LE", b.writeUInt32LE)
	// aliases for writeUInt32LE
	proto.Set("writeUint32LE", b.writeUInt32LE)

	proto.Set("writeUIntBE", b.writeUIntBE)
	// aliases for writeUIntBE
	proto.Set("writeUintBE", b.writeUIntBE)

	proto.Set("writeUIntLE", b.writeUIntLE)
	// aliases for writeUIntLE
	proto.Set("writeUintLE", b.writeUIntLE)

	ctor.Set("prototype", proto)
	ctor.Set("poolSize", 8192)
	ctor.Set("from", b.from)
	ctor.Set("alloc", b.alloc)
	ctor.Set("concat", b.concat)

	exports := module.Get("exports").(*goja.Object)
	exports.Set("Buffer", ctor)
}

func init() {
	require.RegisterCoreModule(ModuleName, Require)
}
