/*
 *
 * Copyright 2019 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package primitives_test

import (
	"sync"
	"sync/atomic"
	"testing"
)

type incrementUint64Map interface {
	increment(string)
	result(string) uint64
}

type mapWithLock struct {
	mu sync.Mutex
	m  map[string]uint64
}

func newMapWithLock() incrementUint64Map {
	return &mapWithLock{
		m: make(map[string]uint64),
	}
}

func (mwl *mapWithLock) increment(c string) {
	mwl.mu.Lock()
	mwl.m[c]++
	mwl.mu.Unlock()
}

func (mwl *mapWithLock) result(c string) uint64 {
	return mwl.m[c]
}

type mapWithAtomicFastpath struct {
	mu sync.RWMutex
	m  map[string]*uint64
}

func newMapWithAtomicFastpath() incrementUint64Map {
	return &mapWithAtomicFastpath{
		m: make(map[string]*uint64),
	}
}

func (mwaf *mapWithAtomicFastpath) increment(c string) {
	mwaf.mu.RLock()
	if p, ok := mwaf.m[c]; ok {
		atomic.AddUint64(p, 1)
		mwaf.mu.RUnlock()
		return
	}
	mwaf.mu.RUnlock()

	mwaf.mu.Lock()
	if p, ok := mwaf.m[c]; ok {
		atomic.AddUint64(p, 1)
		mwaf.mu.Unlock()
		return
	}
	var temp uint64 = 1
	mwaf.m[c] = &temp
	mwaf.mu.Unlock()
}

func (mwaf *mapWithAtomicFastpath) result(c string) uint64 {
	return atomic.LoadUint64(mwaf.m[c])
}

type mapWithSyncMap struct {
	m sync.Map
}

func newMapWithSyncMap() incrementUint64Map {
	return &mapWithSyncMap{}
}

func (mwsm *mapWithSyncMap) increment(c string) {
	p, ok := mwsm.m.Load(c)
	if !ok {
		tp := new(uint64)
		p, _ = mwsm.m.LoadOrStore(c, tp)
	}
	atomic.AddUint64(p.(*uint64), 1)
}

func (mwsm *mapWithSyncMap) result(c string) uint64 {
	p, _ := mwsm.m.Load(c)
	return atomic.LoadUint64(p.(*uint64))
}

func benchmarkIncrementUint64Map(b *testing.B, f func() incrementUint64Map) {
	const cat = "cat"
	benches := []struct {
		name           string
		goroutineCount int
	}{
		{
			name:           "   1",
			goroutineCount: 1,
		},
		{
			name:           "  10",
			goroutineCount: 10,
		},
		{
			name:           " 100",
			goroutineCount: 100,
		},
		{
			name:           "1000",
			goroutineCount: 1000,
		},
	}
	for _, bb := range benches {
		b.Run(bb.name, func(b *testing.B) {
			m := f()
			var wg sync.WaitGroup
			wg.Add(bb.goroutineCount)
			b.ResetTimer()
			for i := 0; i < bb.goroutineCount; i++ {
				go func() {
					for j := 0; j < b.N; j++ {
						m.increment(cat)
					}
					wg.Done()
				}()
			}
			wg.Wait()
			b.StopTimer()
			if m.result(cat) != uint64(bb.goroutineCount*b.N) {
				b.Fatalf("result is %d, want %d", m.result(cat), b.N)
			}
		})
	}
}

func BenchmarkMapWithSyncMutexContention(b *testing.B) {
	benchmarkIncrementUint64Map(b, newMapWithLock)
}

func BenchmarkMapWithAtomicFastpath(b *testing.B) {
	benchmarkIncrementUint64Map(b, newMapWithAtomicFastpath)
}

func BenchmarkMapWithSyncMap(b *testing.B) {
	benchmarkIncrementUint64Map(b, newMapWithSyncMap)
}
