package henchman

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"path"
	"strings"
	"reflect"
)

var ModuleSearchPath = []string{
	"modules",
}

// FIXME: Have custom error types when parsing modules
type Module struct {
	Name   string
	Params map[string]string
}

func getRemainingToken(str []byte, sep byte) ([]byte, error) {
	readbuffer := bytes.NewBuffer(str)
	reader := bufio.NewReader(readbuffer)
	remainingToken, err := reader.ReadBytes(sep)
	return remainingToken, err
}

func parseModuleArgsInterfaceMap(args map[interface{}]interface{}) (map[string]string, error) {
	params := make(map[string]string)
	for k, v := range args {
		kstr, kerr := k.(string)
		vstr, verr := v.(string)
		if kerr && verr {
			params[kstr] = vstr
		}
	}
	
	return params, nil
}

// Split args from the cli that are of the form,
// "a=x b=y c=z" as a map of form { "a": "b", "b": "y", "c": "z" }
// These plan arguments override the variables that may be defined
// as part of the plan file.
func parseModuleArgsString(args string) (map[string]string, error) {
	extraArgs := make(map[string]string)
	scanner := bufio.NewScanner(strings.NewReader(args))

	split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
		advance, nextToken, err := bufio.ScanWords(data, atEOF)
		tokenParts := strings.Split(string(nextToken), "=")
		seps := []byte{'"', '\''}
		for _, sep := range seps {
			if len(tokenParts) > 1 && tokenParts[1][0] == sep && tokenParts[1][len(tokenParts[1])-1] != sep {
				//get the remaining token
				remainingToken, err := getRemainingToken(data[(advance-1):], sep)
				if err == nil {
					token = append(nextToken, remainingToken...)
					break
				}
			} else {
				token = nextToken
			}
		}
		return
	}

	scanner.Split(split)
	// Validate the input
	for scanner.Scan() {
		text := scanner.Text()
		if extraArgsHasText(extraArgs, text) {
			continue
		} else if strings.Contains(text, "=") {
			splitValues := strings.Split(text, "=")
			//this may happen for cases where '=' is in the string
			if len(splitValues) > 2 {
				buffer := bytes.NewBufferString(splitValues[1])
				for i := 2; i < len(splitValues); i++ {
					buffer.WriteString("=")
					buffer.WriteString(splitValues[i])
				}
				splitValues[1] = buffer.String()
			}
			extraArgs[splitValues[0]] = splitValues[1]
		} else {
			// this check takes care of 2nd part of " def'" part of 'abc def'
			return nil, HenchErr(fmt.Errorf("Module args are invalid"), map[string]interface{}{
				"args":     args,
				"solution": "Refer to wiki on proper use of modules",
			}, "")
		}
	}
	// remove all quotes. Value for the respective key
	// should not have quotes
	extraArgs = stripQuotes(extraArgs)

	if err := scanner.Err(); err != nil {
		return extraArgs, HenchErr(err, nil, "Invalid input")
	}
	return extraArgs, nil
}

func stripQuotes(args map[string]string) map[string]string {
	for k, v := range args {
		trimmed := strings.Trim(v, "\"")
		if len(trimmed) == len(v) {
			// Don't double trim the string
			trimmed = strings.Trim(trimmed, "'")
		}
		args[k] = trimmed
	}
	return args
}

func extraArgsHasText(extraArgs map[string]string, text string) bool {
	for _, v := range extraArgs {
		if strings.Contains(v, text) {
			return true
		}
	}
	return false
}

func NewModule(args ...interface{}) (Module, error) {
	name := args[0].(string)
	module := Module{}
	module.Name = name
	paramTable, err := parseModuleArgs(name, args[1])
	if err != nil {
		return module, err
	}
	module.Params = paramTable
	return module, nil
}

func parseModuleArgs(args ...interface{}) (map[string]string, error) {
	name := args[0]
	paramsType := reflect.TypeOf(args[1]).String() 
	switch paramsType {
		case "string":
			return parseModuleArgsString(args[1].(string))
		case "map[interface {}]interface {}":
			return parseModuleArgsInterfaceMap(args[1].(map[interface{}]interface{}))
		default:
			return nil, HenchErr(fmt.Errorf("Invalide module params object: %s", paramsType), map[string]interface{}{
				"module": name,
			}, "While parsing args")
	}
}

// Resolve(...) checks to see if a module is valid and if it's a standalone module
func (module Module) Resolve(osName string) (string, bool, error) {
	fullPath, err := checkForBinary(module.Name, osName)
	if err != nil {
		return "", false, err
	}
	if fullPath != "" {
		return fullPath, true, nil
	}

	fullPath, standalone, err := checkForScripts(module.Name)
	if err != nil {
		return "", false, err
	}

	return fullPath, standalone, nil
}

// checkForBinary is a helper function to module.Resolve(...).  It checks for specific os binaries in a the specified module folder.
func checkForBinary(modName, osName string) (string, error) {
	// checks specific osName folder
	for _, dir := range ModuleSearchPath {
		fullPath := path.Join(dir, modName, modName+"."+osName)
		finfo, err := os.Stat(fullPath)
		if err == nil && !finfo.IsDir() {
			return fullPath, nil
		} else if err != nil && !os.IsNotExist(err) {
			return "", HenchErr(fmt.Errorf("Module %s couldn't be resolved. Could not get info on binary %s", modName, osName), map[string]interface{}{
				"module": modName,
			}, "")
		}
	}

	return "", nil
}

// checkForScripts is a helper function to module.Resolve(...).  It checks for module scripts.  Module scripts are the name of the module itself.  E.g shell would be shell
func checkForScripts(modName string) (string, bool, error) {
	standalone := true
	for _, dir := range ModuleSearchPath {
		fullPath := path.Join(dir, modName, modName)
		finfo, err := os.Stat(fullPath)
		if err == nil {
			if finfo.IsDir() {
				tmpPath := path.Join(fullPath, "exec")
				finfo, err = os.Stat(tmpPath)
				if err != nil || finfo.IsDir() {
					return "", standalone, HenchErr(fmt.Errorf("Module %s couldn't be resolved. Could not find exec", modName), map[string]interface{}{
						"module":  modName,
						"mod dir": fullPath,
						//"solution": "Check if the non-standalone module has an exec.  Or the standalone module isn't in a folder",
					}, "")
				} else {
					standalone = false
				}
			}
			return fullPath, standalone, err
		}
	}

	return "", standalone, HenchErr(fmt.Errorf("Module %s couldn't be resolved", modName), map[string]interface{}{
		"module":   modName,
		"solution": "Check if module exists",
	}, "")
}

// ExecOrder() returns the execution order of specific modules
func (module Module) ExecOrder() ([]string, error) {
	/*
		execOrder := map[string][]string{"default": []string{"exec_module"},
			"copy": []string{"put_for_copy", "copy_remote", "exec_module"},
			"template": []string{"process_template", "put_for_copy", "copy_remote",
				"reset_src", "exec_module"},
			"curl": []string{"exec_tar_module"},
		}
	*/

	var defaultOrder []string
	for moduleType, order := range Config.ExecOrder {
		if moduleType == module.Name {
			return order, nil
		}
		if moduleType == "default" {
			defaultOrder = order
		}
	}
	//default
	return defaultOrder, nil
}
