package config

import (
	"errors"
	"log"

	lua "github.com/yuin/gopher-lua"
	ulua "github.com/zyedidia/micro/v2/internal/lua"
)

// ErrNoSuchFunction is returned when Call is executed on a function that does not exist
var ErrNoSuchFunction = errors.New("No such function exists")

// LoadAllPlugins loads all detected plugins (in runtime/plugins and ConfigDir/plugins)
func LoadAllPlugins() error {
	var reterr error
	for _, p := range Plugins {
		err := p.Load()
		if err != nil {
			reterr = err
		}
	}
	return reterr
}

// RunPluginFn runs a given function in all plugins
// returns an error if any of the plugins had an error
func RunPluginFn(fn string, args ...lua.LValue) error {
	var reterr error
	for _, p := range Plugins {
		if !p.IsLoaded() {
			continue
		}
		_, err := p.Call(fn, args...)
		if err != nil && err != ErrNoSuchFunction {
			reterr = errors.New("Plugin " + p.Name + ": " + err.Error())
		}
	}
	return reterr
}

// RunPluginFnBool runs a function in all plugins and returns
// false if any one of them returned false
// also returns an error if any of the plugins had an error
func RunPluginFnBool(settings map[string]any, fn string, args ...lua.LValue) (bool, error) {
	var reterr error
	retbool := true
	for _, p := range Plugins {
		if !p.IsLoaded() || (settings != nil && settings[p.Name] == false) {
			continue
		}
		val, err := p.Call(fn, args...)
		if err == ErrNoSuchFunction {
			continue
		}
		if err != nil {
			reterr = errors.New("Plugin " + p.Name + ": " + err.Error())
			continue
		}
		if v, ok := val.(lua.LBool); ok {
			retbool = retbool && bool(v)
		}
	}
	return retbool, reterr
}

// Plugin stores information about the source files/info for a plugin
type Plugin struct {
	DirName string        // name of plugin folder
	Name    string        // name of plugin
	Info    *PluginInfo   // json file containing info
	Srcs    []RuntimeFile // lua files
	Loaded  bool
	Builtin bool
}

// IsLoaded returns if a plugin is enabled
func (p *Plugin) IsLoaded() bool {
	if v, ok := GlobalSettings[p.Name]; ok {
		return v.(bool) && p.Loaded
	}
	return true
}

// Plugins is a list of all detected plugins (enabled or disabled)
var Plugins []*Plugin

// Load creates an option for the plugin and runs all source files
func (p *Plugin) Load() error {
	if v, ok := GlobalSettings[p.Name]; ok && !v.(bool) {
		return nil
	}
	for _, f := range p.Srcs {
		dat, err := f.Data()
		if err != nil {
			return err
		}
		err = ulua.LoadFile(p.Name, f.Name(), dat)
		if err != nil {
			return err
		}
	}
	p.Loaded = true
	RegisterCommonOption(p.Name, true)
	return nil
}

// Call calls a given function in this plugin
func (p *Plugin) Call(fn string, args ...lua.LValue) (lua.LValue, error) {
	plug := ulua.L.GetGlobal(p.Name)
	if plug == lua.LNil {
		log.Println("Plugin does not exist:", p.Name, "at", p.DirName, ":", p)
		return nil, nil
	}
	luafn := ulua.L.GetField(plug, fn)
	if luafn == lua.LNil {
		return nil, ErrNoSuchFunction
	}
	err := ulua.L.CallByParam(lua.P{
		Fn:      luafn,
		NRet:    1,
		Protect: true,
	}, args...)
	if err != nil {
		return nil, err
	}
	ret := ulua.L.Get(-1)
	ulua.L.Pop(1)
	return ret, nil
}

// FindPlugin returns the plugin with the given name
func FindPlugin(name string) *Plugin {
	var pl *Plugin
	for _, p := range Plugins {
		if !p.IsLoaded() {
			continue
		}
		if p.Name == name {
			pl = p
			break
		}
	}
	return pl
}
