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

// builtin-math-arith.go
package expr

import (
	"fmt"
	"io"

	"git.portale-stac.it/go-pkg/expr/kern"
	"git.portale-stac.it/go-pkg/expr/types"
	"git.portale-stac.it/go-pkg/expr/types/array"
	"git.portale-stac.it/go-pkg/expr/types/float"
	"git.portale-stac.it/go-pkg/expr/types/fract"
)

func checkNumberParamExpected(funcName string, paramValue any, paramPos, level, subPos int) (err error) {
	if !(types.IsNumber(paramValue) || fract.IsFraction(paramValue)) /*|| isArray(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 kern.ExprContext, name string, it kern.Iterator, count, level int) (result any, err error) {
	var sumAsFloat, sumAsFract bool
	var floatSum float64 = 0.0
	var intSum int64 = 0
	var fractSum *fract.FractionType
	var v any

	level++

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

		if !sumAsFloat {
			if float.IsFloat(v) {
				sumAsFloat = true
				if sumAsFract {
					floatSum = fractSum.ToFloat()
				} else {
					floatSum = float64(intSum)
				}
			} else if !sumAsFract && fract.IsFraction(v) {
				fractSum = fract.NewFraction(intSum, 1)
				sumAsFract = true
			}
		}

		if sumAsFloat {
			floatSum += types.NumAsFloat(v)
		} else if sumAsFract {
			var item *fract.FractionType
			var ok bool
			if item, ok = v.(*fract.FractionType); !ok {
				iv, _ := v.(int64)
				item = fract.NewFraction(iv, 1)
			}
			fractSum = fract.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 kern.ExprContext, name string, args map[string]any) (result any, err error) {
	argv := args[kern.ParamValue].([]any)
	result, err = doAdd(ctx, name, NewArrayIterator(argv), 0, -1)
	return
}

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

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

		if !mulAsFloat {
			if float.IsFloat(v) {
				mulAsFloat = true
				if mulAsFract {
					floatProd = fractProd.ToFloat()
				} else {
					floatProd = float64(intProd)
				}
			} else if !mulAsFract && fract.IsFraction(v) {
				fractProd = fract.NewFraction(intProd, 1)
				mulAsFract = true
			}
		}

		if mulAsFloat {
			floatProd *= types.NumAsFloat(v)
		} else if mulAsFract {
			var item *fract.FractionType
			var ok bool
			if item, ok = v.(*fract.FractionType); !ok {
				iv, _ := v.(int64)
				item = fract.NewFraction(iv, 1)
			}
			fractProd = fract.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 kern.ExprContext, name string, args map[string]any) (result any, err error) {
	argv := args[kern.ParamValue].([]any)
	result, err = doMul(ctx, name, NewArrayIterator(argv), 0, -1)
	return
}

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

	ctx.RegisterFunc("mul", kern.NewGolangFunctor(mulFunc), kern.TypeNumber, []kern.ExprFuncParam{
		kern.NewFuncParamFlagDef(kern.ParamValue, kern.PfDefault|kern.PfRepeat, int64(1)),
	})
}

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