// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.

// builtin-math-arith.go
package expr

import (
	"fmt"
	"io"
)

func checkNumberParamExpected(funcName string, paramValue any, paramPos, level, subPos int) (err error) {
	if !(IsNumber(paramValue) || isFraction(paramValue)) /*|| isList(paramValue)*/ {
		err = fmt.Errorf("%s(): param nr %d (%d in %d) has wrong type %T, number expected",
			funcName, paramPos+1, subPos+1, level, paramValue)
	}
	return
}

func doAdd(ctx ExprContext, name string, it Iterator, count, level int) (result any, err error) {
	var sumAsFloat, sumAsFract bool
	var floatSum float64 = 0.0
	var intSum int64 = 0
	var fractSum *FractionType
	var v any

	level++

	for v, err = it.Next(); err == nil; v, err = it.Next() {
		if list, ok := v.(*ListType); ok {
			v = NewListIterator(list, nil)
		}
		if subIter, ok := v.(Iterator); ok {
			if v, err = doAdd(ctx, name, subIter, count, level); err != nil {
				break
			}
			if extIter, ok := v.(ExtIterator); ok && extIter.HasOperation(cleanName) {
				if _, err = extIter.CallOperation(cleanName, nil); err != nil {
					return
				}
			}
		} else if err = checkNumberParamExpected(name, v, count, level, it.Index()); err != nil {
			break
		}
		count++

		if !sumAsFloat {
			if IsFloat(v) {
				sumAsFloat = true
				if sumAsFract {
					floatSum = fractSum.toFloat()
				} else {
					floatSum = float64(intSum)
				}
			} else if !sumAsFract && isFraction(v) {
				fractSum = newFraction(intSum, 1)
				sumAsFract = true
			}
		}

		if sumAsFloat {
			floatSum += numAsFloat(v)
		} else if sumAsFract {
			var item *FractionType
			var ok bool
			if item, ok = v.(*FractionType); !ok {
				iv, _ := v.(int64)
				item = newFraction(iv, 1)
			}
			fractSum = sumFract(fractSum, item)
		} else {
			iv, _ := v.(int64)
			intSum += iv
		}
	}
	if err == nil || err == io.EOF {
		err = nil
		if sumAsFloat {
			result = floatSum
		} else if sumAsFract {
			result = fractSum
		} else {
			result = intSum
		}
	}
	return
}

func addFunc(ctx ExprContext, name string, args []any) (result any, err error) {
	result, err = doAdd(ctx, name, NewArrayIterator(args), 0, -1)
	return
}

func doMul(ctx ExprContext, name string, it Iterator, count, level int) (result any, err error) {
	var mulAsFloat, mulAsFract bool
	var floatProd float64 = 1.0
	var intProd int64 = 1
	var fractProd *FractionType
	var v any

	level++
	for v, err = it.Next(); err == nil; v, err = it.Next() {
		if list, ok := v.(*ListType); ok {
			v = NewListIterator(list, nil)
		}
		if subIter, ok := v.(Iterator); ok {
			if v, err = doMul(ctx, name, subIter, count, level); err != nil {
				break
			}
			if extIter, ok := v.(ExtIterator); ok && extIter.HasOperation(cleanName) {
				if _, err = extIter.CallOperation(cleanName, nil); err != nil {
					return
				}
			}
		} else {
			if err = checkNumberParamExpected(name, v, count, level, it.Index()); err != nil {
				break
			}
		}
		count++

		if !mulAsFloat {
			if IsFloat(v) {
				mulAsFloat = true
				if mulAsFract {
					floatProd = fractProd.toFloat()
				} else {
					floatProd = float64(intProd)
				}
			} else if !mulAsFract && isFraction(v) {
				fractProd = newFraction(intProd, 1)
				mulAsFract = true
			}
		}

		if mulAsFloat {
			floatProd *= numAsFloat(v)
		} else if mulAsFract {
			var item *FractionType
			var ok bool
			if item, ok = v.(*FractionType); !ok {
				iv, _ := v.(int64)
				item = newFraction(iv, 1)
			}
			fractProd = mulFract(fractProd, item)
		} else {
			iv, _ := v.(int64)
			intProd *= iv
		}
	}
	if err == nil || err == io.EOF {
		err = nil
		if mulAsFloat {
			result = floatProd
		} else if mulAsFract {
			result = fractProd
		} else {
			result = intProd
		}
	}
	return
}

func mulFunc(ctx ExprContext, name string, args []any) (result any, err error) {
	result, err = doMul(ctx, name, NewArrayIterator(args), 0, -1)
	return
}

func ImportMathFuncs(ctx ExprContext) {
	ctx.RegisterFunc("add", &golangFunctor{f: addFunc}, TypeNumber, []ExprFuncParam{
		NewFuncParamFlagDef(ParamValue, PfDefault|PfRepeat, int64(0)),
	})

	ctx.RegisterFunc("mul", &golangFunctor{f: mulFunc}, TypeNumber, []ExprFuncParam{
		NewFuncParamFlagDef(ParamValue, PfDefault|PfRepeat, int64(1)),
	})
}

func init() {
	RegisterBuiltinModule("math.arith", ImportMathFuncs, "Functions add() and mul()")
}
