package struc

import (
	"bytes"
	"encoding/binary"
	"testing"
)

type BenchExample struct {
	Test    [5]byte
	A       int32
	B, C, D int16
	Test2   [4]byte
	Length  int32
}

func BenchmarkArrayEncode(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var buf bytes.Buffer
		if err := Pack(&buf, arrayReference); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkSliceEncode(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var buf bytes.Buffer
		if err := Pack(&buf, sliceReference); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkArrayDecode(b *testing.B) {
	var out ExampleArray
	for i := 0; i < b.N; i++ {
		buf := bytes.NewBuffer(arraySliceReferenceBytes)
		if err := Unpack(buf, &out); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkSliceDecode(b *testing.B) {
	var out ExampleSlice
	for i := 0; i < b.N; i++ {
		buf := bytes.NewBuffer(arraySliceReferenceBytes)
		if err := Unpack(buf, &out); err != nil {
			b.Fatal(err)
		}
	}
}

type BenchStrucExample struct {
	Test    [5]byte `struc:"[5]byte"`
	A       int     `struc:"int32"`
	B, C, D int     `struc:"int16"`
	Test2   [4]byte `struc:"[4]byte"`
	Length  int     `struc:"int32,sizeof=Data"`
	Data    []byte
}

var benchRef = &BenchExample{
	[5]byte{1, 2, 3, 4, 5},
	1, 2, 3, 4,
	[4]byte{1, 2, 3, 4},
	8,
}

var eightBytes = []byte("8bytestr")

var benchStrucRef = &BenchStrucExample{
	[5]byte{1, 2, 3, 4, 5},
	1, 2, 3, 4,
	[4]byte{1, 2, 3, 4},
	8, eightBytes,
}

func BenchmarkEncode(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var buf bytes.Buffer
		err := Pack(&buf, benchStrucRef)
		if err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkStdlibEncode(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var buf bytes.Buffer
		err := binary.Write(&buf, binary.BigEndian, benchRef)
		if err != nil {
			b.Fatal(err)
		}
		_, err = buf.Write(eightBytes)
		if err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkManualEncode(b *testing.B) {
	order := binary.BigEndian
	s := benchStrucRef
	for i := 0; i < b.N; i++ {
		var buf bytes.Buffer
		tmp := make([]byte, 29)
		copy(tmp[0:5], s.Test[:])
		order.PutUint32(tmp[5:9], uint32(s.A))
		order.PutUint16(tmp[9:11], uint16(s.B))
		order.PutUint16(tmp[11:13], uint16(s.C))
		order.PutUint16(tmp[13:15], uint16(s.D))
		copy(tmp[15:19], s.Test2[:])
		order.PutUint32(tmp[19:23], uint32(s.Length))
		copy(tmp[23:], s.Data)
		_, err := buf.Write(tmp)
		if err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkDecode(b *testing.B) {
	var out BenchStrucExample
	var buf bytes.Buffer
	if err := Pack(&buf, benchStrucRef); err != nil {
		b.Fatal(err)
	}
	bufBytes := buf.Bytes()
	for i := 0; i < b.N; i++ {
		buf := bytes.NewReader(bufBytes)
		err := Unpack(buf, &out)
		if err != nil {
			b.Fatal(err)
		}
		out.Data = nil
	}
}

func BenchmarkStdlibDecode(b *testing.B) {
	var out BenchExample
	var buf bytes.Buffer
	binary.Write(&buf, binary.BigEndian, *benchRef)
	_, err := buf.Write(eightBytes)
	if err != nil {
		b.Fatal(err)
	}
	bufBytes := buf.Bytes()
	for i := 0; i < b.N; i++ {
		buf := bytes.NewReader(bufBytes)
		err := binary.Read(buf, binary.BigEndian, &out)
		if err != nil {
			b.Fatal(err)
		}
		tmp := make([]byte, out.Length)
		_, err = buf.Read(tmp)
		if err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkManualDecode(b *testing.B) {
	var o BenchStrucExample
	var buf bytes.Buffer
	if err := Pack(&buf, benchStrucRef); err != nil {
		b.Fatal(err)
	}
	tmp := buf.Bytes()
	order := binary.BigEndian
	for i := 0; i < b.N; i++ {
		copy(o.Test[:], tmp[0:5])
		o.A = int(order.Uint32(tmp[5:9]))
		o.B = int(order.Uint16(tmp[9:11]))
		o.C = int(order.Uint16(tmp[11:13]))
		o.D = int(order.Uint16(tmp[13:15]))
		copy(o.Test2[:], tmp[15:19])
		o.Length = int(order.Uint32(tmp[19:23]))
		o.Data = make([]byte, o.Length)
		copy(o.Data, tmp[23:])
	}
}

func BenchmarkFullEncode(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var buf bytes.Buffer
		if err := Pack(&buf, reference); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkFullDecode(b *testing.B) {
	var out Example
	for i := 0; i < b.N; i++ {
		buf := bytes.NewBuffer(referenceBytes)
		if err := Unpack(buf, &out); err != nil {
			b.Fatal(err)
		}
	}
}
