package lookup_test

/*
This file contains components to mock a storage for testing
lookup algorithms and measure the number of reads.
*/

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/ethereum/go-ethereum/swarm/log"
	"github.com/ethereum/go-ethereum/swarm/storage/feed/lookup"
)

// Data is a struct to keep a value to store/retrieve during testing
type Data struct {
	Payload uint64
	Time    uint64
}

// String implements fmt.Stringer
func (d *Data) String() string {
	return fmt.Sprintf("%d-%d", d.Payload, d.Time)
}

// Datamap is an internal map to hold the mocked storage
type DataMap map[lookup.EpochID]*Data

// StoreConfig allows to specify the simulated delays for each type of
// read operation
type StoreConfig struct {
	CacheReadTime      time.Duration // time it takes to read from the cache
	FailedReadTime     time.Duration // time it takes to acknowledge a read as failed
	SuccessfulReadTime time.Duration // time it takes to fetch data
}

// StoreCounters will track read count metrics
type StoreCounters struct {
	reads           int
	cacheHits       int
	failed          int
	successful      int
	canceled        int
	maxSimultaneous int
}

// Store simulates a store and keeps track of performance counters
type Store struct {
	StoreConfig
	StoreCounters
	data        DataMap
	cache       DataMap
	lock        sync.RWMutex
	activeReads int
}

// NewStore returns a new mock store ready for use
func NewStore(config *StoreConfig) *Store {
	store := &Store{
		StoreConfig: *config,
		data:        make(DataMap),
	}

	store.Reset()
	return store
}

// Reset reset performance counters and clears the cache
func (s *Store) Reset() {
	s.cache = make(DataMap)
	s.StoreCounters = StoreCounters{}
}

// Put stores a value in the mock store at the given epoch
func (s *Store) Put(epoch lookup.Epoch, value *Data) {
	log.Debug("Write: %d-%d, value='%d'\n", epoch.Base(), epoch.Level, value.Payload)
	s.data[epoch.ID()] = value
}

// Update runs the seed algorithm to place the update in the appropriate epoch
func (s *Store) Update(last lookup.Epoch, now uint64, value *Data) lookup.Epoch {
	epoch := lookup.GetNextEpoch(last, now)
	s.Put(epoch, value)
	return epoch
}

// Get retrieves data at the specified epoch, simulating a delay
func (s *Store) Get(ctx context.Context, epoch lookup.Epoch, now uint64) (value interface{}, err error) {
	epochID := epoch.ID()
	var operationTime time.Duration

	defer func() { // simulate a delay according to what has actually happened
		select {
		case <-lookup.TimeAfter(operationTime):
		case <-ctx.Done():
			s.lock.Lock()
			s.canceled++
			s.lock.Unlock()
			value = nil
			err = ctx.Err()
		}
		s.lock.Lock()
		s.activeReads--
		s.lock.Unlock()
	}()

	s.lock.Lock()
	defer s.lock.Unlock()
	s.reads++
	s.activeReads++
	if s.activeReads > s.maxSimultaneous {
		s.maxSimultaneous = s.activeReads
	}

	// 1.- Simulate a cache read
	item := s.cache[epochID]
	operationTime += s.CacheReadTime

	if item != nil {
		s.cacheHits++
		if item.Time <= now {
			s.successful++
			return item, nil
		}
		return nil, nil
	}

	// 2.- simulate a full read

	item = s.data[epochID]
	if item != nil {
		operationTime += s.SuccessfulReadTime
		s.successful++
		s.cache[epochID] = item
		if item.Time <= now {
			return item, nil
		}
	} else {
		operationTime += s.FailedReadTime
		s.failed++
	}
	return nil, nil
}

// MakeReadFunc returns a read function suitable for the lookup algorithm, mapped
// to this mock storage
func (s *Store) MakeReadFunc() lookup.ReadFunc {
	return func(ctx context.Context, epoch lookup.Epoch, now uint64) (interface{}, error) {
		return s.Get(ctx, epoch, now)
	}
}