package main

import (
	"bufio"
	"flag"
	"fmt"
	"io"
	"os"
	"reflect"
	"regexp"
	"strconv"
	"strings"
)

var (
	// regex with sub groups
	input = flag.String("i", "^.*?$", "input pattern")

	// format string with {0} as placeholders
	// {0} always matches the whole line
	// {1} and onwards match their respective sub groups
	//
	// You can optionally specify a printf-syntax for formatting like this: {1:%s} or {1:%02d}
	// printf-syntax is currently supported for: strings, floats, integers.
	//
	// Addtionally mutators can be provided
	// to further manipulate the value using the given syntax: {1:%d:+1}
	//
	// The value mutated by can also be a back reference to another group
	// using round brackets like this: {1:%d:+(2)}}
	//
	// Multiple mutators can be used at once: {1:%d:+2*3-(2)}
	// Be aware that they will be applied strictly from left to right!
	//
	// The following number mutators (integers and floats) are allowed:
	// + - * / ^ %
	output = flag.String("o", "{0}", "output pattern")

	// don't ignore lines which do not match against input.
	// they will be copied without any changes
	keepUnmatched = flag.Bool("k", false, "keep unmatched lines")

	// if the amount of lines in stdin are not divisible by lineParseAmount,
	// the last lines will be ignored completely.
	// it may be useful to have a boolean flag for this behavior
	lineParseAmount = flag.Int("n", 1, "amount of lines to feed into input pattern")

	replacePattern = regexp.MustCompile(`\{(\d+)(?::(.*?))?(?::(.*?))?\}`)

	numMutationPattern = regexp.MustCompile(`([+\-*/])(\d+|\((\d+)\))`)
)

func main() {
	flag.Parse()

	pattern, err := regexp.Compile(*input)
	if err != nil {
		panic(err)
	}

	for line := range readLines(os.Stdin) {
		line = line[:len(line)-1]
		matches := pattern.FindStringSubmatch(line)

		if len(matches) == 0 {
			if *keepUnmatched {
				fmt.Println(line)
			}
			continue
		}

		fmt.Println(replaceVars(*output, matches...))
	}
}

func readLines(r io.Reader) <-chan string {
	ch := make(chan string, 10)

	go func(out chan<- string, source io.Reader) {
		defer close(out)

		r := bufio.NewReader(source)

		for {
			var line string
			var err error
			lines := make([]string, 0, *lineParseAmount)
			for line, err = r.ReadString('\n'); err == nil; line, err = r.ReadString('\n') {
				lines = append(lines, line)
				if len(lines) == cap(lines) {
					break
				}
			}
			if err != nil {
				return
			}
			out <- strings.Join(lines, "")
		}
	}(ch, r)

	return ch
}

func replaceVars(format string, vars ...string) string {
	replacements := replacePattern.FindAllStringSubmatch(format, -1)

	for _, replacement := range replacements {
		rplStr := replacement[0]
		varIndex, _ := strconv.Atoi(replacement[1])
		rplFmt := replacement[2]

		// default format if not specified by user
		if rplFmt == "" {
			rplFmt = "%s"
		}

		if strings.HasSuffix(rplFmt, "d") { // replace integers
			value, _ := strconv.ParseInt(vars[varIndex], 10, 64)
			mutate := numMut2func[int64](replacement[3])
			format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, mutate(value, vars)), 1)
		} else if strings.HasSuffix(rplFmt, "f") { // replace floats
			value, _ := strconv.ParseFloat(vars[varIndex], 64)
			mutate := numMut2func[float64](replacement[3])
			format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, mutate(value, vars)), 1)
		} else { // replace strings
			format = strings.Replace(format, rplStr, fmt.Sprintf(rplFmt, vars[varIndex]), 1)
		}

	}

	return format
}

var numMutatorCache = map[string]interface{}{}

func numMut2func[T int64 | float64](mutation string) (f func(value T, vars []string) T) {
	if mutation == "" {
		return func(value T, vars []string) T { return value }
	}

	// caching
	if v, ok := numMutatorCache[mutation]; ok {
		return v.(func(value T, vars []string) T)
	}
	defer func() { numMutatorCache[mutation] = f }()

	matches := numMutationPattern.FindAllStringSubmatch(mutation, -1)
	mutators := make([]NumMutator, 0, len(matches))
	var err error
	for _, match := range matches {
		mut := NumMutator{Op: NewNumOperatorFromString(match[1])}
		if match[3] == "" {
			mut.Value, err = strconv.Atoi(match[2])
			mut.Var = false
			if err != nil {
				panic("invalid number in number mutator: " + match[2])
			}
		} else {
			mut.Var = true
			mut.Value, err = strconv.Atoi(match[3])
			if err != nil {
				panic("invalid back reference group in number mutator: " + match[2])
			}
		}
		mutators = append(mutators, mut)
	}

	numberParser := number_parser[T]()

	return func(value T, vars []string) T {
		for _, mutator := range mutators {
			var otherValue T
			if mutator.Var {
				other := numberParser(vars[mutator.Value])
				otherValue = T(other)
			} else {
				otherValue = T(mutator.Value)
			}

			switch mutator.Op {
			case NumOperatorAdd:
				value += otherValue
			case NumOperatorSub:
				value -= otherValue
			case NumOperatorMul:
				value *= otherValue
			case NumOperatorDiv:
				value /= otherValue
			default:
			}
		}

		return value
	}
}

func number_parser[T int64 | float64]() func(str string) T {
	typeOfT := reflect.TypeOf(new(T)).Elem()
	typeOfInt64 := reflect.TypeOf(new(int64)).Elem()
	typeOfFloat64 := reflect.TypeOf(new(float64)).Elem()

	if typeOfT == typeOfInt64 {
		return func(str string) T {
			num, err := strconv.Atoi(str)
			if err != nil {
				panic("expected integer but found " + str)
			}
			return T(num)
		}
	} else if typeOfT == typeOfFloat64 {
		return func(str string) T {
			num, err := strconv.ParseFloat(str, 64)
			if err != nil {
				panic("expected float but found " + str)
			}
			return T(num)
		}
	}

	panic("invalid number type")
}