package io

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

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

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

// WriteLE writes into the underlying io.Writer from an object v in little-endian format.
func (w *BinWriter) WriteLE(v interface{}) {
	if w.Err != nil {
		return
	}
	w.Err = binary.Write(w.w, binary.LittleEndian, v)
}

// WriteBE writes into the underlying io.Writer from an object v in big-endian format.
func (w *BinWriter) WriteBE(v interface{}) {
	if w.Err != nil {
		return
	}
	w.Err = binary.Write(w.w, binary.BigEndian, v)
}

// WriteArray writes a slice or an array arr into w.
func (w *BinWriter) WriteArray(arr interface{}) {
	switch val := reflect.ValueOf(arr); val.Kind() {
	case reflect.Slice, reflect.Array:
		typ := val.Type().Elem()
		method, ok := typ.MethodByName("EncodeBinary")
		if !ok || !isEncodeBinaryMethod(method) {
			panic(typ.String() + " does not have EncodeBinary(*BinWriter)")
		}

		if w.Err != nil {
			return
		}

		w.WriteVarUint(uint64(val.Len()))
		for i := 0; i < val.Len(); i++ {
			method := val.Index(i).MethodByName("EncodeBinary")
			method.Call([]reflect.Value{reflect.ValueOf(w)})
		}
	default:
		panic("not an array")
	}
}

func isEncodeBinaryMethod(method reflect.Method) bool {
	t := method.Type
	return t != nil &&
		t.NumIn() == 2 && t.In(1) == reflect.TypeOf((*BinWriter)(nil)) &&
		t.NumOut() == 0
}

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

	if val < 0xfd {
		w.Err = binary.Write(w.w, binary.LittleEndian, uint8(val))
		return
	}
	if val < 0xFFFF {
		w.Err = binary.Write(w.w, binary.LittleEndian, byte(0xfd))
		w.Err = binary.Write(w.w, binary.LittleEndian, uint16(val))
		return
	}
	if val < 0xFFFFFFFF {
		w.Err = binary.Write(w.w, binary.LittleEndian, byte(0xfe))
		w.Err = binary.Write(w.w, binary.LittleEndian, uint32(val))
		return

	}

	w.Err = binary.Write(w.w, binary.LittleEndian, byte(0xff))
	w.Err = binary.Write(w.w, binary.LittleEndian, val)

}

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

// WriteString writes a variable length string into the underlying io.Writer.
func (w *BinWriter) WriteString(s string) {
	w.WriteBytes([]byte(s))
}