package binding

import (
	"encoding/json"
	"sync"
	"unsafe"
)

// DB is our database of method bindings
type DB struct {
	//  map[packagename] -> map[structname] -> map[methodname]*method
	store map[string]map[string]map[string]*BoundMethod

	// This uses fully qualified method names as a shortcut for store traversal.
	// It used for performance gains at runtime
	methodMap map[string]*BoundMethod

	// These are slices of methods registered using WailsInit and WailsShutdown
	wailsInitMethods     []*BoundMethod
	wailsShutdownMethods []*BoundMethod

	// Lock to ensure sync access to the data
	lock sync.RWMutex
}

func newDB() *DB {
	return &DB{
		store:     make(map[string]map[string]map[string]*BoundMethod),
		methodMap: make(map[string]*BoundMethod),
	}
}

// GetMethodFromStore returns the method for the given package/struct/method names
// nil is returned if any one of those does not exist
func (d *DB) GetMethodFromStore(packageName string, structName string, methodName string) *BoundMethod {

	// Lock the db whilst processing and unlock on return
	d.lock.RLock()
	defer d.lock.RUnlock()

	structMap, exists := d.store[packageName]
	if !exists {
		return nil
	}
	methodMap, exists := structMap[structName]
	if !exists {
		return nil
	}
	return methodMap[methodName]
}

// GetMethod returns the method for the given qualified method name
// qualifiedMethodName is "packagename.structname.methodname"
func (d *DB) GetMethod(qualifiedMethodName string) *BoundMethod {

	// Lock the db whilst processing and unlock on return
	d.lock.RLock()
	defer d.lock.RUnlock()

	return d.methodMap[qualifiedMethodName]
}

// AddMethod adds the given method definition to the db using the given qualified path: packageName.structName.methodName
func (d *DB) AddMethod(packageName string, structName string, methodName string, methodDefinition *BoundMethod) {

	// TODO: Validate inputs?

	// Lock the db whilst processing and unlock on return
	d.lock.Lock()
	defer d.lock.Unlock()

	// Get the map associated with the package name
	structMap, exists := d.store[packageName]
	if !exists {
		// Create a new map for this packagename
		d.store[packageName] = make(map[string]map[string]*BoundMethod)
		structMap = d.store[packageName]
	}

	// Get the map associated with the struct name
	methodMap, exists := structMap[structName]
	if !exists {
		// Create a new map for this packagename
		structMap[structName] = make(map[string]*BoundMethod)
		methodMap = structMap[structName]
	}

	// Store the method definition
	methodMap[methodName] = methodDefinition

	// Store in the methodMap
	key := packageName + "." + structName + "." + methodName
	d.methodMap[key] = methodDefinition

}

// AddWailsInit checks the given method is a WailsInit method and if it
// is, adds it to the list of WailsInit methods
func (d *DB) AddWailsInit(method *BoundMethod) error {
	err := method.VerifyWailsInit()
	if err != nil {
		return err
	}

	// Lock the db whilst processing and unlock on return
	d.lock.Lock()
	defer d.lock.Unlock()

	d.wailsInitMethods = append(d.wailsInitMethods, method)
	return nil
}

// AddWailsShutdown checks the given method is a WailsInit method and if it
// is, adds it to the list of WailsShutdown methods
func (d *DB) AddWailsShutdown(method *BoundMethod) error {
	err := method.VerifyWailsShutdown()
	if err != nil {
		return err
	}

	// Lock the db whilst processing and unlock on return
	d.lock.Lock()
	defer d.lock.Unlock()

	d.wailsShutdownMethods = append(d.wailsShutdownMethods, method)
	return nil
}

// ToJSON converts the method map to JSON
func (d *DB) ToJSON() (string, error) {

	// Lock the db whilst processing and unlock on return
	d.lock.RLock()
	defer d.lock.RUnlock()

	bytes, err := json.Marshal(&d.store)

	// Return zero copy string as this string will be read only
	return *(*string)(unsafe.Pointer(&bytes)), err
}

// WailsInitMethods returns the list of registered WailsInit methods
func (d *DB) WailsInitMethods() []*BoundMethod {
	return d.wailsInitMethods
}

// WailsShutdownMethods returns the list of registered WailsInit methods
func (d *DB) WailsShutdownMethods() []*BoundMethod {
	return d.wailsShutdownMethods
}
