package internal

import (
	"sync"

	"fyne.io/fyne/v2"
)

// InMemoryPreferences provides an implementation of the fyne.Preferences API that is stored in memory.
type InMemoryPreferences struct {
	values          map[string]interface{}
	lock            sync.RWMutex
	changeListeners []func()
	wg              *sync.WaitGroup
}

// Declare conformity with Preferences interface
var _ fyne.Preferences = (*InMemoryPreferences)(nil)

// AddChangeListener allows code to be notified when some preferences change. This will fire on any update.
// The passed 'listener' should not try to write values.
func (p *InMemoryPreferences) AddChangeListener(listener func()) {
	p.lock.Lock()
	defer p.lock.Unlock()

	p.changeListeners = append(p.changeListeners, listener)
}

// ChangeListeners returns the list of listeners registered for this set of preferences.
func (p *InMemoryPreferences) ChangeListeners() []func() {
	return p.changeListeners
}

// ReadValues provides read access to the underlying value map - for internal use only...
// You should not retain a reference to the map nor write to the values in the callback function
func (p *InMemoryPreferences) ReadValues(fn func(map[string]interface{})) {
	p.lock.RLock()
	fn(p.values)
	p.lock.RUnlock()
}

// WriteValues provides write access to the underlying value map - for internal use only...
// You should not retain a reference to the map passed to the callback function
func (p *InMemoryPreferences) WriteValues(fn func(map[string]interface{})) {
	p.lock.Lock()
	fn(p.values)
	p.lock.Unlock()

	p.fireChange()
}

func (p *InMemoryPreferences) set(key string, value interface{}) {
	p.lock.Lock()

	if stored, ok := p.values[key]; ok && stored == value {
		p.lock.Unlock()
		return
	}

	p.values[key] = value
	p.lock.Unlock()

	p.fireChange()
}

func (p *InMemoryPreferences) get(key string) (interface{}, bool) {
	p.lock.RLock()
	defer p.lock.RUnlock()

	v, err := p.values[key]
	return v, err
}

func (p *InMemoryPreferences) remove(key string) {
	p.lock.Lock()
	delete(p.values, key)
	p.lock.Unlock()

	p.fireChange()
}

func (p *InMemoryPreferences) fireChange() {
	p.lock.RLock()
	listeners := p.changeListeners
	p.lock.RUnlock()

	for _, l := range listeners {
		p.wg.Add(1)
		go func(listener func()) {
			defer p.wg.Done()
			listener()
		}(l)
	}

	p.wg.Wait()
}

// Bool looks up a boolean value for the key
func (p *InMemoryPreferences) Bool(key string) bool {
	return p.BoolWithFallback(key, false)
}

// BoolWithFallback looks up a boolean value and returns the given fallback if not found
func (p *InMemoryPreferences) BoolWithFallback(key string, fallback bool) bool {
	value, ok := p.get(key)
	if !ok {
		return fallback
	}

	val, ok := value.(bool)
	if !ok {
		return false
	}
	return val
}

// SetBool saves a boolean value for the given key
func (p *InMemoryPreferences) SetBool(key string, value bool) {
	p.set(key, value)
}

// Float looks up a float64 value for the key
func (p *InMemoryPreferences) Float(key string) float64 {
	return p.FloatWithFallback(key, 0.0)
}

// FloatWithFallback looks up a float64 value and returns the given fallback if not found
func (p *InMemoryPreferences) FloatWithFallback(key string, fallback float64) float64 {
	value, ok := p.get(key)
	if !ok {
		return fallback
	}

	val, ok := value.(float64)
	if !ok {
		return 0.0
	}
	return val
}

// SetFloat saves a float64 value for the given key
func (p *InMemoryPreferences) SetFloat(key string, value float64) {
	p.set(key, value)
}

// Int looks up an integer value for the key
func (p *InMemoryPreferences) Int(key string) int {
	return p.IntWithFallback(key, 0)
}

// IntWithFallback looks up an integer value and returns the given fallback if not found
func (p *InMemoryPreferences) IntWithFallback(key string, fallback int) int {
	value, ok := p.get(key)
	if !ok {
		return fallback
	}

	// integers can be de-serialised as floats, so support both
	if intVal, ok := value.(int); ok {
		return intVal
	}
	val, ok := value.(float64)
	if !ok {
		return 0
	}
	return int(val)
}

// SetInt saves an integer value for the given key
func (p *InMemoryPreferences) SetInt(key string, value int) {
	p.set(key, value)
}

// String looks up a string value for the key
func (p *InMemoryPreferences) String(key string) string {
	return p.StringWithFallback(key, "")
}

// StringWithFallback looks up a string value and returns the given fallback if not found
func (p *InMemoryPreferences) StringWithFallback(key, fallback string) string {
	value, ok := p.get(key)
	if !ok {
		return fallback
	}

	return value.(string)
}

// SetString saves a string value for the given key
func (p *InMemoryPreferences) SetString(key string, value string) {
	p.set(key, value)
}

// RemoveValue deletes a value on the given key
func (p *InMemoryPreferences) RemoveValue(key string) {
	p.remove(key)
}

// NewInMemoryPreferences creates a new preferences implementation stored in memory
func NewInMemoryPreferences() *InMemoryPreferences {
	return &InMemoryPreferences{
		values: make(map[string]interface{}),
		wg:     &sync.WaitGroup{},
	}
}
