package internal

import (
	"reflect"
	"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]any
	lock            sync.RWMutex
	changeListeners []func()
}

// 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)
}

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

func (p *InMemoryPreferences) BoolList(key string) []bool {
	return p.BoolListWithFallback(key, []bool{})
}

func (p *InMemoryPreferences) BoolListWithFallback(key string, fallback []bool) []bool {
	value, ok := p.get(key)
	if !ok {
		return fallback
	}
	valb, ok := value.([]bool)
	if !ok {
		return fallback
	}
	return valb
}

// 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
	}
	valb, ok := value.(bool)
	if !ok {
		return fallback
	}
	return valb
}

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

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

func (p *InMemoryPreferences) FloatList(key string) []float64 {
	return p.FloatListWithFallback(key, []float64{})
}

func (p *InMemoryPreferences) FloatListWithFallback(key string, fallback []float64) []float64 {
	value, ok := p.get(key)
	if !ok {
		return fallback
	}
	valf, ok := value.([]float64)
	if ok {
		return valf
	}
	vali, ok := value.([]int)
	if ok {
		flts := make([]float64, len(vali))
		for i, f := range vali {
			flts[i] = float64(f)
		}
		return flts
	}
	return fallback
}

// 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
	}
	valf, ok := value.(float64)
	if ok {
		return valf
	}
	vali, ok := value.(int)
	if ok {
		return float64(vali)
	}
	return fallback
}

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

func (p *InMemoryPreferences) IntList(key string) []int {
	return p.IntListWithFallback(key, []int{})
}

func (p *InMemoryPreferences) IntListWithFallback(key string, fallback []int) []int {
	value, ok := p.get(key)
	if !ok {
		return fallback
	}
	vali, ok := value.([]int)
	if ok {
		return vali
	}
	// integers can be de-serialised as floats, so support both
	valf, ok := value.([]float64)
	if ok {
		ints := make([]int, len(valf))
		for i, f := range valf {
			ints[i] = int(f)
		}
		return ints
	}
	return fallback
}

// 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
	}
	vali, ok := value.(int)
	if ok {
		return vali
	}
	// integers can be de-serialised as floats, so support both
	valf, ok := value.(float64)
	if !ok {
		return fallback
	}
	return int(valf)
}

// 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]any)) {
	p.lock.RLock()
	fn(p.values)
	p.lock.RUnlock()
}

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

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

func (p *InMemoryPreferences) SetBoolList(key string, value []bool) {
	p.set(key, value)
}

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

func (p *InMemoryPreferences) SetFloatList(key string, value []float64) {
	p.set(key, value)
}

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

func (p *InMemoryPreferences) SetIntList(key string, value []int) {
	p.set(key, value)
}

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

func (p *InMemoryPreferences) SetStringList(key string, value []string) {
	p.set(key, value)
}

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

func (p *InMemoryPreferences) StringList(key string) []string {
	return p.StringListWithFallback(key, []string{})
}

func (p *InMemoryPreferences) StringListWithFallback(key string, fallback []string) []string {
	value, ok := p.get(key)
	if !ok {
		return fallback
	}
	vals, ok := value.([]string)
	if !ok {
		return fallback
	}
	return vals
}

// 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
	}
	vals, ok := value.(string)
	if !ok {
		return fallback
	}
	return vals
}

// 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]any)) {
	p.lock.Lock()
	fn(p.values)
	p.lock.Unlock()

	p.fireChange()
}

// NewInMemoryPreferences creates a new preferences implementation stored in memory
func NewInMemoryPreferences() *InMemoryPreferences {
	return &InMemoryPreferences{values: make(map[string]any)}
}

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

	var wg sync.WaitGroup

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

	wg.Wait()
}

func (p *InMemoryPreferences) get(key string) (any, 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) set(key string, value any) {
	p.lock.Lock()

	if reflect.TypeOf(value).Kind() == reflect.Slice {
		s := reflect.ValueOf(value)
		old := reflect.ValueOf(p.values[key])
		if p.values[key] != nil && s.Len() == old.Len() {
			changed := false
			for i := 0; i < s.Len(); i++ {
				if s.Index(i).Interface() != old.Index(i).Interface() {
					changed = true
					break
				}
			}
			if !changed {
				p.lock.Unlock()
				return
			}
		}
	} else {
		if stored, ok := p.values[key]; ok && stored == value {
			p.lock.Unlock()
			return
		}
	}

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

	p.fireChange()
}
