package expander

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
)

// these are helper functions that bring bash-substitution to the drone yaml file.
// see http://tldp.org/LDP/abs/html/parameter-substitution.html

type substituteFunc func(str, key, val string) string

var substitutors = []substituteFunc{
	substituteQ,
	substitute,
	substitutePrefix,
	substituteSuffix,
	substituteDefault,
	substituteReplace,
	substituteLeft,
	substituteSubstr,
}

// substitute is a helper function that substitutes a simple parameter using
// ${parameter} notation.
func substitute(str, key, val string) string {
	key = fmt.Sprintf("${%s}", key)
	return strings.Replace(str, key, val, -1)
}

// substituteQ is a helper function that substitutes a simple parameter using
// "${parameter}" notation with the escaped value, using %q.
func substituteQ(str, key, val string) string {
	key = fmt.Sprintf(`"${%s}"`, key)
	val = fmt.Sprintf("%q", val)
	return strings.Replace(str, key, val, -1)
}

// substitutePrefix is a helper function that substitutes paramters using
// ${parameter##prefix} notation with the parameter value minus the trimmed prefix.
func substitutePrefix(str, key, val string) string {
	key = fmt.Sprintf("\\${%s##(.+)}", key)
	reg, err := regexp.Compile(key)
	if err != nil {
		return str
	}
	for _, match := range reg.FindAllStringSubmatch(str, -1) {
		if len(match) != 2 {
			continue
		}
		val_ := strings.TrimPrefix(val, match[1])
		str = strings.Replace(str, match[0], val_, -1)
	}
	return str
}

// substituteSuffix is a helper function that substitutes paramters using
// ${parameter%%suffix} notation with the parameter value minus the trimmed suffix.
func substituteSuffix(str, key, val string) string {
	key = fmt.Sprintf("\\${%s%%%%(.+)}", key)
	reg, err := regexp.Compile(key)
	if err != nil {
		return str
	}
	for _, match := range reg.FindAllStringSubmatch(str, -1) {
		if len(match) != 2 {
			continue
		}
		val_ := strings.TrimSuffix(val, match[1])
		str = strings.Replace(str, match[0], val_, -1)
	}
	return str
}

// substituteDefault is a helper function that substitutes paramters using
// ${parameter=default} notation with the parameter value. When empty the
// default value is used.
func substituteDefault(str, key, val string) string {
	key = fmt.Sprintf("\\${%s=(.+)}", key)
	reg, err := regexp.Compile(key)
	if err != nil {
		return str
	}
	for _, match := range reg.FindAllStringSubmatch(str, -1) {
		if len(match) != 2 {
			continue
		}
		if len(val) == 0 {
			str = strings.Replace(str, match[0], match[1], -1)
		} else {
			str = strings.Replace(str, match[0], val, -1)
		}
	}
	return str
}

// unescapeBackslash is a helper function to unescape any backslashes in str.
// Note that no actual literal conversions are done.
func unescapeBackslash(str string) string {
	re := regexp.MustCompile(`\\(.)`)

	return string(re.ReplaceAll([]byte(str), []byte("$1")))
}

// substituteReplace is a helper function that substitutes paramters using
// ${parameter/old/new} notation with the parameter value. A find and replace
// is performed before injecting the strings, replacing the old pattern with
// the new value.
func substituteReplace(str, key, val string) string {
	key = fmt.Sprintf(`\${%s/((?:\\.|[^\\])+)/(.+)}`, key)
	reg, err := regexp.Compile(key)
	if err != nil {
		return str
	}
	match := reg.FindStringSubmatch(str)
	if match == nil {
		return str
	}

	old := unescapeBackslash(match[1])
	new := unescapeBackslash(match[2])

	with := strings.Replace(val, old, new, -1)
	return strings.Replace(str, match[0], with, -1)
}

// substituteLeft is a helper function that substitutes paramters using
// ${parameter:pos} notation with the parameter value, sliced up to the
// specified position.
func substituteLeft(str, key, val string) string {
	key = fmt.Sprintf("\\${%s:([0-9]*)}", key)
	reg, err := regexp.Compile(key)
	if err != nil {
		return str
	}
	for _, match := range reg.FindAllStringSubmatch(str, -1) {
		if len(match) != 2 {
			continue
		}
		index, err := strconv.Atoi(match[1])
		if err != nil {
			continue // skip
		}
		if index > len(val)-1 {
			continue // skip
		}

		str = strings.Replace(str, match[0], val[:index], -1)
	}
	return str
}

// substituteLeft is a helper function that substitutes paramters using
// ${parameter:pos:len} notation with the parameter value as a substring,
// starting at the specified position for the specified length.
func substituteSubstr(str, key, val string) string {
	key = fmt.Sprintf("\\${%s:([0-9]*):([0-9]*)}", key)
	reg, err := regexp.Compile(key)
	if err != nil {
		return str
	}
	for _, match := range reg.FindAllStringSubmatch(str, -1) {
		if len(match) != 3 {
			continue
		}
		pos, err := strconv.Atoi(match[1])
		if err != nil {
			continue // skip
		}
		length, err := strconv.Atoi(match[2])
		if err != nil {
			continue // skip
		}
		if pos+length > len(val)-1 {
			continue // skip
		}
		str = strings.Replace(str, match[0], val[pos:pos+length], -1)
	}
	return str
}
