package fastjson

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"strings"
	"testing"
)

func BenchmarkParseRawString(b *testing.B) {
	for _, s := range []string{`""`, `"a"`, `"abcd"`, `"abcdefghijk"`, `"qwertyuiopasdfghjklzxcvb"`} {
		b.Run(s, func(b *testing.B) {
			benchmarkParseRawString(b, s)
		})
	}
}

func benchmarkParseRawString(b *testing.B, s string) {
	b.ReportAllocs()
	b.SetBytes(int64(len(s)))
	s = s[1:] // skip the opening '"'
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			rs, tail, err := parseRawString(s)
			if err != nil {
				panic(fmt.Errorf("cannot parse %q: %s", s, err))
			}
			if rs != s[:len(s)-1] {
				panic(fmt.Errorf("invalid string obtained; got %q; want %q", rs, s[:len(s)-1]))
			}
			if len(tail) > 0 {
				panic(fmt.Errorf("non-empty tail got: %q", tail))
			}
		}
	})
}

func BenchmarkParseRawNumber(b *testing.B) {
	for _, s := range []string{"1", "1234", "123456", "-1234", "1234567890.1234567", "-1.32434e+12"} {
		b.Run(s, func(b *testing.B) {
			benchmarkParseRawNumber(b, s)
		})
	}
}

func benchmarkParseRawNumber(b *testing.B, s string) {
	b.ReportAllocs()
	b.SetBytes(int64(len(s)))
	b.RunParallel(func(pb *testing.PB) {
		for pb.Next() {
			rn, tail, err := parseRawNumber(s)
			if err != nil {
				panic(fmt.Errorf("cannot parse %q: %s", s, err))
			}
			if rn != s {
				panic(fmt.Errorf("invalid number obtained; got %q; want %q", rn, s))
			}
			if len(tail) > 0 {
				panic(fmt.Errorf("non-empty tail got: %q", tail))
			}
		}
	})
}

func BenchmarkObjectGet(b *testing.B) {
	for _, itemsCount := range []int{10, 100, 1000, 10000, 100000} {
		b.Run(fmt.Sprintf("items_%d", itemsCount), func(b *testing.B) {
			for _, lookupsCount := range []int{0, 1, 2, 4, 8, 16, 32, 64} {
				b.Run(fmt.Sprintf("lookups_%d", lookupsCount), func(b *testing.B) {
					benchmarkObjectGet(b, itemsCount, lookupsCount)
				})
			}
		})
	}
}

func benchmarkObjectGet(b *testing.B, itemsCount, lookupsCount int) {
	b.StopTimer()
	var ss []string
	for i := range itemsCount {
		s := fmt.Sprintf(`"key_%d": "value_%d"`, i, i)
		ss = append(ss, s)
	}
	s := "{" + strings.Join(ss, ",") + "}"
	key := fmt.Sprintf("key_%d", len(ss)/2)
	expectedValue := fmt.Sprintf("value_%d", len(ss)/2)
	b.StartTimer()
	b.ReportAllocs()
	b.SetBytes(int64(len(s)))

	b.RunParallel(func(pb *testing.PB) {
		p := benchPool.Get()
		for pb.Next() {
			v, err := p.Parse(s)
			if err != nil {
				panic(fmt.Errorf("unexpected error: %s", err))
			}
			o := v.GetObject()
			for range lookupsCount {
				sb := o.Get(key).GetStringBytes()
				if string(sb) != expectedValue {
					panic(fmt.Errorf("unexpected value; got %q; want %q", sb, expectedValue))
				}
			}
		}
		benchPool.Put(p)
	})
}

func BenchmarkMarshalTo(b *testing.B) {
	b.Run("small", func(b *testing.B) {
		benchmarkMarshalTo(b, smallFixture)
	})
	b.Run("medium", func(b *testing.B) {
		benchmarkMarshalTo(b, mediumFixture)
	})
	b.Run("large", func(b *testing.B) {
		benchmarkMarshalTo(b, largeFixture)
	})
	b.Run("canada", func(b *testing.B) {
		benchmarkMarshalTo(b, canadaFixture)
	})
	b.Run("citm", func(b *testing.B) {
		benchmarkMarshalTo(b, citmFixture)
	})
	b.Run("twitter", func(b *testing.B) {
		benchmarkMarshalTo(b, twitterFixture)
	})
}

func benchmarkMarshalTo(b *testing.B, s string) {
	p := benchPool.Get()
	v, err := p.Parse(s)
	if err != nil {
		panic(fmt.Errorf("unexpected error: %s", err))
	}

	b.ReportAllocs()
	b.SetBytes(int64(len(s)))
	b.RunParallel(func(pb *testing.PB) {
		var b []byte
		for pb.Next() {
			// It is ok calling v.MarshalTo from concurrent
			// goroutines, since MarshalTo doesn't modify v.
			b = v.MarshalTo(b[:0])
		}
	})
	benchPool.Put(p)
}

func BenchmarkParse(b *testing.B) {
	b.Run("small", func(b *testing.B) {
		benchmarkParse(b, smallFixture)
	})
	b.Run("medium", func(b *testing.B) {
		benchmarkParse(b, mediumFixture)
	})
	b.Run("large", func(b *testing.B) {
		benchmarkParse(b, largeFixture)
	})
	b.Run("canada", func(b *testing.B) {
		benchmarkParse(b, canadaFixture)
	})
	b.Run("citm", func(b *testing.B) {
		benchmarkParse(b, citmFixture)
	})
	b.Run("twitter", func(b *testing.B) {
		benchmarkParse(b, twitterFixture)
	})
}

var (
	// small, medium and large fixtures are from https://github.com/buger/jsonparser/blob/f04e003e4115787c6272636780bc206e5ffad6c4/benchmark/benchmark.go
	smallFixture  = getFromFile("testdata/small.json")
	mediumFixture = getFromFile("testdata/medium.json")
	largeFixture  = getFromFile("testdata/large.json")

	// canada, citm and twitter fixtures are from https://github.com/serde-rs/json-benchmark/tree/0db02e043b3ae87dc5065e7acb8654c1f7670c43/data
	canadaFixture  = getFromFile("testdata/canada.json")
	citmFixture    = getFromFile("testdata/citm_catalog.json")
	twitterFixture = getFromFile("testdata/twitter.json")
)

func getFromFile(filename string) string {
	data, err := ioutil.ReadFile(filename)
	if err != nil {
		panic(fmt.Errorf("cannot read %s: %s", filename, err))
	}
	return string(data)
}

func benchmarkParse(b *testing.B, s string) {
	b.Run("stdjson-map", func(b *testing.B) {
		benchmarkStdJSONParseMap(b, s)
	})
	b.Run("stdjson-struct", func(b *testing.B) {
		benchmarkStdJSONParseStruct(b, s)
	})
	b.Run("stdjson-empty-struct", func(b *testing.B) {
		benchmarkStdJSONParseEmptyStruct(b, s)
	})
	b.Run("fastjson", func(b *testing.B) {
		benchmarkFastJSONParse(b, s)
	})
	b.Run("fastjson-get", func(b *testing.B) {
		benchmarkFastJSONParseGet(b, s)
	})
}

func benchmarkFastJSONParse(b *testing.B, s string) {
	b.ReportAllocs()
	b.SetBytes(int64(len(s)))
	b.RunParallel(func(pb *testing.PB) {
		p := benchPool.Get()
		for pb.Next() {
			v, err := p.Parse(s)
			if err != nil {
				panic(fmt.Errorf("unexpected error: %s", err))
			}
			if v.Type() != TypeObject {
				panic(fmt.Errorf("unexpected value type; got %s; want %s", v.Type(), TypeObject))
			}
		}
		benchPool.Put(p)
	})
}

func benchmarkFastJSONParseGet(b *testing.B, s string) {
	b.ReportAllocs()
	b.SetBytes(int64(len(s)))
	b.RunParallel(func(pb *testing.PB) {
		p := benchPool.Get()
		var n int
		for pb.Next() {
			v, err := p.Parse(s)
			if err != nil {
				panic(fmt.Errorf("unexpected error: %s", err))
			}
			n += v.GetInt("sid")
			n += len(v.GetStringBytes("uuid"))
			p := v.Get("person")
			if p != nil {
				n++
			}
			c := v.Get("company")
			if c != nil {
				n++
			}
			u := v.Get("users")
			if u != nil {
				n++
			}
			a := v.GetArray("features")
			n += len(a)
			a = v.GetArray("topicSubTopics")
			n += len(a)
			o := v.Get("search_metadata")
			if o != nil {
				n++
			}
		}
		benchPool.Put(p)
	})
}

var benchPool ParserPool

func benchmarkStdJSONParseMap(b *testing.B, s string) {
	b.ReportAllocs()
	b.SetBytes(int64(len(s)))
	bb := s2b(s)
	b.RunParallel(func(pb *testing.PB) {
		var m map[string]interface{}
		for pb.Next() {
			if err := json.Unmarshal(bb, &m); err != nil {
				panic(fmt.Errorf("unexpected error: %s", err))
			}
		}
	})
}

func benchmarkStdJSONParseStruct(b *testing.B, s string) {
	b.ReportAllocs()
	b.SetBytes(int64(len(s)))
	bb := s2b(s)
	b.RunParallel(func(pb *testing.PB) {
		var m struct {
			Sid            int
			UUID           string
			Person         map[string]interface{}
			Company        map[string]interface{}
			Users          []interface{}
			Features       []map[string]interface{}
			TopicSubTopics map[string]interface{}
			SearchMetadata map[string]interface{}
		}
		for pb.Next() {
			if err := json.Unmarshal(bb, &m); err != nil {
				panic(fmt.Errorf("unexpected error: %s", err))
			}
		}
	})
}

func benchmarkStdJSONParseEmptyStruct(b *testing.B, s string) {
	b.ReportAllocs()
	b.SetBytes(int64(len(s)))
	bb := s2b(s)
	b.RunParallel(func(pb *testing.PB) {
		var m struct{}
		for pb.Next() {
			if err := json.Unmarshal(bb, &m); err != nil {
				panic(fmt.Errorf("unexpected error: %s", err))
			}
		}
	})
}
