package nucleus

import (
	"bytes"
	"os/exec"
	"path/filepath"
	goPlugin "plugin"

	"code.fbi.h-da.de/danet/gosdn/controller/customerrs"
	"code.fbi.h-da.de/danet/gosdn/controller/interfaces/plugin"
	"code.fbi.h-da.de/danet/gosdn/controller/nucleus/util"
	log "github.com/sirupsen/logrus"
)

// BuildPlugin builds a new plugin within the given path. The source file must
// be present at the given path. The built plugin is written to a 'plugin.so'
// file.
func BuildPlugin(path string, sourceFileNames []string, args ...string) error {
	pPath := filepath.Join(path, util.PluginOutputName)
	fileNames := make([]string, len(sourceFileNames))
	for i, fileName := range sourceFileNames {
		sPath := filepath.Join(path, fileName)
		fileNames[i] = sPath
	}
	// Provide standard build arguments within a string slice
	buildCommand := []string{
		"go",
		"build",
		"-buildmode=plugin",
		"-trimpath",
		"-o",
		pPath,
	}
	// Append build arguments
	buildCommand = append(buildCommand, args...)
	buildCommand = append(buildCommand, fileNames...)

	var stderr bytes.Buffer
	// Create the command to be executed
	cmd := exec.Command(buildCommand[0], buildCommand[1:]...)
	cmd.Dir = "./"
	cmd.Stderr = &stderr
	// Run the command and build the plugin
	err := cmd.Run()
	if err != nil {
		log.Error(stderr.String())
		return err
	}
	return nil
}

// LoadPlugin opens a go plugin binary which must be named 'plugin.so' at the
// given path. LoadPlugin checks for the symbol 'PluginSymbol' that has to be
// provided within the plugin to be loaded. If the symbol is found, the loaded
// plugin is returned.
func LoadPlugin(path string) (goPlugin.Symbol, error) {
	path = filepath.Join(path, util.PluginOutputName)
	// Open the plugin, which is a binary (named 'plugin.so') within the given
	// path.
	gp, err := goPlugin.Open(path)
	if err != nil {
		return nil, err
	}
	// Search for the specific symbol within the plugin. If it does not contain
	// the symbol is not usable and an error is thrown.
	symbol, err := gp.Lookup("PluginSymbol")
	if err != nil {
		return nil, err
	}

	return symbol, nil
}

// UpdatePlugin updates a given Plugin. Therefore the version of the
// `plugin.yml` manifest file is compared to the version in use. If a new
// version is within the plugin folder, the new version of the plugin is built.
// NOTE:This should only be used with caution.
func UpdatePlugin(p plugin.Plugin) (updated bool, err error) {
	tmpManifest, err := plugin.ReadManifestFromFile(filepath.Join(p.Path(), util.ManifestFileName))
	if err != nil {
		return false, err
	}

	if p.Manifest().Version < tmpManifest.Version {
		err := BuildPlugin(p.Path(), []string{util.GoStructName, util.GoStructAdditionsName})
		if err != nil {
			return false, err
		}
		log.Info("Plugin update executed.")
		return true, nil
	}
	return false, customerrs.PluginVersionError{
		PlugID:      p.ID().String(),
		ProvidedVer: tmpManifest.Version,
		UsedVer:     p.Manifest().Version,
	}
}