// Mgmt
// Copyright (C) 2013-2024+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.

// Package vars provides a framework for language vars.
package vars

import (
	"fmt"
	"strings"

	"github.com/purpleidea/mgmt/lang/interfaces"
)

const (
	// ConstNamespace is the string prefix for all top-level built-in vars.
	ConstNamespace = "const"

	// ResourceNamespace is the string prefix for all top-level resource
	// specific built-in vars, that exist under the ConstNamespace header.
	ResourceNamespace = "res"
)

// Value is a shortcut to using this type.
// XXX: Eventually we might get rid of this entirely and use types.Value instead
// of interfaces.Var which seems to be unnecessary at the moment.
type Value = interfaces.Var

// registeredVars is a global map of all possible vars which can be used. You
// should never touch this map directly. Use methods like Register instead.
var registeredVars = make(map[string]func() interfaces.Var) // must initialize

// Register takes a var and its name and makes it available for use. It is
// commonly called in the init() method of the var at program startup. There is
// no matching Unregister function.
func Register(name string, fn func() interfaces.Var) {
	if _, ok := registeredVars[name]; ok {
		panic(fmt.Sprintf("a var named %s is already registered", name))
	}
	//gob.Register(fn())
	registeredVars[name] = fn
}

// ModuleRegister is exactly like Register, except that it registers within a
// named module. This is a helper function.
func ModuleRegister(module, name string, v func() interfaces.Var) {
	Register(module+interfaces.ModuleSep+name, v)
}

// resourceConstHelper is a helper function to manage the const topology.
func resourceConstHelper(kind, param, field string) string {
	// const.res.file.state.exists = "exists"
	// TODO: should it be: const.res.file.params.state.exists = "exists" ?
	chunks := []string{
		ConstNamespace,
		ResourceNamespace,
		kind,
		param,
		field,
	}
	//return ConstNamespace + interfaces.ModuleSep + ResourceNamespace + interfaces.ModuleSep + kind + interfaces.ModuleSep + param + interfaces.ModuleSep + field
	return strings.Join(chunks, interfaces.ModuleSep) // cleaner code
}

// RegisterResourceParam registers a single const param for a resource. You
// might prefer to use RegisterResourceParams instead.
func RegisterResourceParam(kind, param, field string, value func() interfaces.Var) {
	Register(resourceConstHelper(kind, param, field), value)
}

// RegisterResourceParams registers a map of const params for a resource. The
// params mapping keys are the param name and the param field name. Finally, the
// value is the specific type value for that constant.
func RegisterResourceParams(kind string, params map[string]map[string]func() interfaces.Var) {
	for param, mapping := range params {
		for field, value := range mapping {
			Register(resourceConstHelper(kind, param, field), value)
		}
	}
}

// Lookup returns a pointer to the var implementation.
func Lookup(name string) (interfaces.Var, error) {
	f, exists := registeredVars[name]
	if !exists {
		return nil, fmt.Errorf("not found")
	}
	return f(), nil
}

// LookupPrefix returns a map of names to vars that start with a module prefix.
// This search automatically adds the period separator. So if you want vars in
// the `const` prefix, search for `const`, not `const.` and it will find all the
// correctly registered vars. This removes that prefix from the result in the
// map keys that it returns. If you search for an empty prefix, then this will
// return all the top-level functions that aren't in a module.
func LookupPrefix(prefix string) map[string]func() interfaces.Var {
	result := make(map[string]func() interfaces.Var)
	for name, f := range registeredVars {
		// requested top-level vars, and no module separators...
		if prefix == "" {
			if !strings.Contains(name, interfaces.ModuleSep) {
				result[name] = f // copy
			}
			continue
		}
		sep := prefix + interfaces.ModuleSep
		if !strings.HasPrefix(name, sep) {
			continue
		}
		s := strings.TrimPrefix(name, sep) // remove the prefix
		result[s] = f                      // copy
	}
	return result
}

// Map returns a map from all registered var names to a function to return that
// one. We return a copy of our internal registered var store so that this
// result can be manipulated safely. We return the vars that produce the Var
// interface because we might use this result to create multiple vars, and each
// one might need to have its own unique memory address to work properly.
func Map() map[string]func() interfaces.Var {
	m := make(map[string]func() interfaces.Var)
	for name, fn := range registeredVars { // copy
		m[name] = fn
	}
	return m
}
