package io

import (
	"bytes"
	"encoding/binary"
	"io"
	"reflect"
)

// BinWriter is a convenient wrapper around an io.Writer and err object.
// Used to simplify error handling when writing into an io.Writer
// from a struct with many fields.
type BinWriter struct {
	w   io.Writer
	Err error
	uv  [9]byte
}

// NewBinWriterFromIO makes a BinWriter from io.Writer.
func NewBinWriterFromIO(iow io.Writer) *BinWriter {
	return &BinWriter{w: iow}
}

// WriteU64LE writes a uint64 value into the underlying io.Writer in
// little-endian format.
func (w *BinWriter) WriteU64LE(u64 uint64) {
	binary.LittleEndian.PutUint64(w.uv[:8], u64)
	w.WriteBytes(w.uv[:8])
}

// WriteU32LE writes a uint32 value into the underlying io.Writer in
// little-endian format.
func (w *BinWriter) WriteU32LE(u32 uint32) {
	binary.LittleEndian.PutUint32(w.uv[:4], u32)
	w.WriteBytes(w.uv[:4])
}

// WriteU16LE writes a uint16 value into the underlying io.Writer in
// little-endian format.
func (w *BinWriter) WriteU16LE(u16 uint16) {
	binary.LittleEndian.PutUint16(w.uv[:2], u16)
	w.WriteBytes(w.uv[:2])
}

// WriteU16BE writes a uint16 value into the underlying io.Writer in
// big-endian format.
func (w *BinWriter) WriteU16BE(u16 uint16) {
	binary.BigEndian.PutUint16(w.uv[:2], u16)
	w.WriteBytes(w.uv[:2])
}

// WriteB writes a byte into the underlying io.Writer.
func (w *BinWriter) WriteB(u8 byte) {
	w.uv[0] = u8
	w.WriteBytes(w.uv[:1])
}

// WriteBool writes a boolean value into the underlying io.Writer encoded as
// a byte with values of 0 or 1.
func (w *BinWriter) WriteBool(b bool) {
	var i byte
	if b {
		i = 1
	}
	w.WriteB(i)
}

// WriteArray writes a slice or an array arr into w. Note that nil slices and
// empty slices are gonna be treated the same resulting in an equal zero-length
// array encoded.
func (w *BinWriter) WriteArray(arr any) {
	switch val := reflect.ValueOf(arr); val.Kind() {
	case reflect.Slice, reflect.Array:
		if w.Err != nil {
			return
		}

		typ := val.Type().Elem()

		w.WriteVarUint(uint64(val.Len()))
		for i := range val.Len() {
			el, ok := val.Index(i).Interface().(encodable)
			if !ok {
				el, ok = val.Index(i).Addr().Interface().(encodable)
				if !ok {
					panic(typ.String() + " is not encodable")
				}
			}

			el.EncodeBinary(w)
		}
	default:
		panic("not an array")
	}
}

// WriteVarUint writes a uint64 into the underlying writer using variable-length encoding.
func (w *BinWriter) WriteVarUint(val uint64) {
	if w.Err != nil {
		return
	}

	n := PutVarUint(w.uv[:], val)
	w.WriteBytes(w.uv[:n])
}

// PutVarUint puts a val in the varint form to the pre-allocated buffer.
func PutVarUint(data []byte, val uint64) int {
	_ = data[8]
	if val < 0xfd {
		data[0] = byte(val)
		return 1
	}
	if val < 0xFFFF {
		data[0] = byte(0xfd)
		binary.LittleEndian.PutUint16(data[1:], uint16(val))
		return 3
	}
	if val < 0xFFFFFFFF {
		data[0] = byte(0xfe)
		binary.LittleEndian.PutUint32(data[1:], uint32(val))
		return 5
	}

	data[0] = byte(0xff)
	binary.LittleEndian.PutUint64(data[1:], val)
	return 9
}

// WriteBytes writes a variable byte into the underlying io.Writer without prefix.
func (w *BinWriter) WriteBytes(b []byte) {
	if w.Err != nil {
		return
	}
	_, w.Err = w.w.Write(b)
}

// WriteVarBytes writes a variable length byte array into the underlying io.Writer.
func (w *BinWriter) WriteVarBytes(b []byte) {
	w.WriteVarUint(uint64(len(b)))
	w.WriteBytes(b)
}

// WriteString writes a variable length string into the underlying io.Writer.
func (w *BinWriter) WriteString(s string) {
	w.WriteVarUint(uint64(len(s)))
	if w.Err != nil {
		return
	}
	_, w.Err = io.WriteString(w.w, s)
}

// Grow tries to increase the underlying buffer capacity so that at least n bytes
// can be written without reallocation. If the writer is not a buffer, this is a no-op.
func (w *BinWriter) Grow(n int) {
	if b, ok := w.w.(*bytes.Buffer); ok {
		b.Grow(n)
	}
}
