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

// fraction-type.go
package fract

//https://www.youmath.it/lezioni/algebra-elementare/lezioni-di-algebra-e-aritmetica-per-scuole-medie/553-dalle-frazioni-a-numeri-decimali.html

import (
	"errors"
	"fmt"
	"math"
	"strconv"
	"strings"

	"git.portale-stac.it/go-pkg/expr/kern"
)

type FractionType struct {
	num, den int64
}

func NewFraction(num, den int64) *FractionType {
	num, den = simplifyIntegers(num, den)
	return &FractionType{num, den}
}

func (fr *FractionType) EqualTo(other kern.Equaler) (equal bool) {
	if otherFract, ok := other.(*FractionType); ok && otherFract != nil {
		equal = fr.Equals(*otherFract)
	}
	return
}

func (fr *FractionType) Equals(other FractionType) bool {
	// return fr.num*other.den == other.num*fr.den
	return fr.num == other.num && fr.den == other.den
}

func Float64ToFraction(f float64) (fract *FractionType, err error) {
	var sign string
	intPart, decPart := math.Modf(f)
	if decPart < 0.0 {
		sign = "-"
		intPart = -intPart
		decPart = -decPart
	}
	dec := fmt.Sprintf("%.12f", decPart)
	s := fmt.Sprintf("%s%.f%s", sign, intPart, dec[1:])
	return MakeGeneratingFraction(s)
}

// Based on https://cs.opensource.google/go/go/+/refs/tags/go1.22.3:src/math/big/rat.go;l=39
func MakeGeneratingFraction(s string) (f *FractionType, err error) {
	var num, den int64
	var sign int64 = 1
	var parts []string
	if len(s) == 0 {
		goto exit
	}
	switch s[0] {
	case '-':
		sign = int64(-1)
		s = s[1:]
	case '+':
		s = s[1:]
	}
	//	if strings.HasSuffix(s, "()") {
	//		s = s[0 : len(s)-2]
	//	}
	s = strings.TrimSuffix(s, "()")
	parts = strings.SplitN(s, ".", 2)
	if num, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
		return
	}
	if len(parts) == 1 {
		f = NewFraction(sign*num, 1)
	} else if len(parts) == 2 {
		subParts := strings.SplitN(parts[1], "(", 2)
		if len(subParts) == 1 {
			den = 1
			dec := parts[1]
			lsd := len(dec)
			for i := lsd - 1; i >= 0 && dec[i] == '0'; i-- {
				lsd--
			}
			for _, c := range dec[0:lsd] {
				if c < '0' || c > '9' {
					return nil, kern.ErrExpectedGot("fract", "digit", c)
				}
				num = num*10 + int64(c-'0')
				den = den * 10
			}
			f = NewFraction(sign*num, den)
		} else if len(subParts) == 2 {
			sub := num
			mul := int64(1)
			for _, c := range subParts[0] {
				if c < '0' || c > '9' {
					return nil, kern.ErrExpectedGot("fract", "digit", c)
				}
				num = num*10 + int64(c-'0')
				sub = sub*10 + int64(c-'0')
				mul *= 10
			}
			if len(subParts) == 2 {
				if s[len(s)-1] != ')' {
					goto exit
				}
				p := subParts[1][0 : len(subParts[1])-1]
				for _, c := range p {
					if c < '0' || c > '9' {
						return nil, kern.ErrExpectedGot("fract", "digit", c)
					}
					num = num*10 + int64(c-'0')
					den = den*10 + 9
				}
				den *= mul
			}
			num -= sub
			f = NewFraction(sign*num, den)
		}
	}
exit:
	if f == nil {
		err = errors.New("bad syntax")
	}
	return
}

func (fr *FractionType) N() int64 {
	return fr.num
}

func (fr *FractionType) D() int64 {
	return fr.den
}

func (fr *FractionType) ToFloat() float64 {
	return float64(fr.num) / float64(fr.den)
}

func (fr *FractionType) String() string {
	return fr.ToString(0)
}

func (fr *FractionType) ToString(opt kern.FmtOpt) string {
	var sb strings.Builder
	if opt&kern.MultiLine == 0 {
		sb.WriteString(fmt.Sprintf("%d:%d", fr.num, fr.den))
	} else {
		var sign, num string
		if fr.num < 0 && opt&kern.TTY == 0 {
			num = strconv.FormatInt(-fr.num, 10)
			sign = "-"
		} else {
			num = strconv.FormatInt(fr.num, 10)
		}
		den := strconv.FormatInt(fr.den, 10)
		size := max(len(num), len(den))
		if opt&kern.TTY != 0 {
			sNum := fmt.Sprintf("\x1b[4m%[1]*s\x1b[0m\n", -size, fmt.Sprintf("%[1]*s", (size+len(num))/2, sign+num))
			sb.WriteString(sNum)
		} else {
			if len(sign) > 0 {
				sb.WriteString("  ")
			}
			sb.WriteString(fmt.Sprintf("%[1]*s", -size, fmt.Sprintf("%[1]*s", (size+len(num))/2, num)))
			sb.WriteByte('\n')
			if len(sign) > 0 {
				sb.WriteString(sign)
				sb.WriteByte(' ')
			}
			sb.WriteString(strings.Repeat("-", size))
			sb.WriteByte('\n')
			if len(sign) > 0 {
				sb.WriteString("  ")
			}
		}
		sDen := fmt.Sprintf("%[1]*s", size, fmt.Sprintf("%[1]*s", (size+len(den))/2, den))
		sb.WriteString(sDen)
	}

	return sb.String()
}

func (fr *FractionType) TypeName() string {
	return "fraction"
}

// -------- fraction utility functions

// greatest common divider
func Gcd(a, b int64) (g int64) {
	if a < 0 {
		a = -a
	}
	if b < 0 {
		b = -b
	}
	if a < b {
		a, b = b, a
	}
	r := a % b
	for r > 0 {
		a, b = b, r
		r = a % b
	}
	g = b
	return
}

// lower common multiple
func lcm(a, b int64) (l int64) {
	g := Gcd(a, b)
	l = a * b / g
	return
}

// Sum two fractions
func SumFract(f1, f2 *FractionType) (sum *FractionType) {
	m := lcm(f1.den, f2.den)
	sum = NewFraction(f1.num*(m/f1.den)+f2.num*(m/f2.den), m)
	return
}

// Multiply two fractions
func MulFract(f1, f2 *FractionType) (prod *FractionType) {
	prod = NewFraction(f1.num*f2.num, f1.den*f2.den)
	return
}

func anyToFract(v any) (f *FractionType, err error) {
	var ok bool
	if f, ok = v.(*FractionType); !ok {
		if n, ok := v.(int64); ok {
			f = intToFraction(n)
		} else if dec, ok := v.(float64); ok {
			f, err = Float64ToFraction(dec)
		} else {
			err = kern.ErrExpectedGot("fract", kern.TypeFraction, v)
		}
	}
	return
}

func anyPairToFract(v1, v2 any) (f1, f2 *FractionType, err error) {
	if f1, err = anyToFract(v1); err != nil {
		return
	}
	if f2, err = anyToFract(v2); err != nil {
		return
	}
	return
}

func SumAnyFract(af1, af2 any) (sum any, err error) {
	var f1, f2 *FractionType
	if f1, f2, err = anyPairToFract(af1, af2); err != nil {
		return
	}
	f := SumFract(f1, f2)
	if f.num == 0 {
		sum = 0
	} else {
		sum = simplifyFraction(f)
	}
	return
}

// Returns
//
//		 <0 if af1 < af2
//		 =0 if af1 == af2
//		 >0 if af1 > af2
//	 err if af1 or af2 is not convertible to fraction
func CmpAnyFract(af1, af2 any) (result int, err error) {
	var f1, f2 *FractionType
	if f1, f2, err = anyPairToFract(af1, af2); err != nil {
		return
	}
	result = cmpFract(f1, f2)
	return
}

// Returns
//
//	<0 if af1 < af2
//	=0 if af1 == af2
//	>0 if af1 > af2
func cmpFract(f1, f2 *FractionType) (result int) {
	if f1 != nil && f2 != nil {
		f2.num = -f2.num
		f := SumFract(f1, f2)
		if f.num < 0 {
			result = -1
		} else if f.num > 0 {
			result = 1
		} else {
			result = 0
		}
	}
	return
}

func SubAnyFract(af1, af2 any) (sum any, err error) {
	var f1, f2 *FractionType
	if f1, f2, err = anyPairToFract(af1, af2); err != nil {
		return
	}
	f2.num = -f2.num
	f := SumFract(f1, f2)
	if f.num == 0 {
		sum = 0
	} else {
		sum = simplifyFraction(f)
	}
	return
}

func MulAnyFract(af1, af2 any) (prod any, err error) {
	var f1, f2 *FractionType
	if f1, f2, err = anyPairToFract(af1, af2); err != nil {
		return
	}
	if f1.num == 0 || f2.num == 0 {
		prod = 0
	} else {
		f := &FractionType{f1.num * f2.num, f1.den * f2.den}
		prod = simplifyFraction(f)
	}
	return
}

func DivAnyFract(af1, af2 any) (quot any, err error) {
	var f1, f2 *FractionType
	if f1, f2, err = anyPairToFract(af1, af2); err != nil {
		return
	}
	if f2.num == 0 {
		err = errors.New("division by zero")
		return
	}
	if f1.num == 0 || f2.den == 0 {
		quot = 0
	} else {
		f := &FractionType{f1.num * f2.den, f1.den * f2.num}
		quot = simplifyFraction(f)
	}
	return
}

func simplifyFraction(f *FractionType) (v any) {
	f.num, f.den = simplifyIntegers(f.num, f.den)
	if f.den == 1 {
		v = f.num
	} else {
		v = &FractionType{f.num, f.den}
	}
	return v
}

func simplifyIntegers(num, den int64) (a, b int64) {
	if num == 0 {
		return 0, 1
	}
	if den == 0 {
		panic("fraction with denominator == 0")
	}
	if den < 0 {
		den = -den
		num = -num
	}
	g := Gcd(num, den)
	a = num / g
	b = den / g
	return
}

func intToFraction(n int64) *FractionType {
	return &FractionType{n, 1}
}

func IsFraction(v any) (ok bool) {
	_, ok = v.(*FractionType)
	return ok
}

// func IsFract(v any) (ok bool) {
// 	_, ok = v.(*FractionType)
// 	return ok
// }

func IsRational(v any) (ok bool) {
	if _, ok = v.(*FractionType); !ok {
		_, ok = v.(int64)
	}
	return ok
}
