package parse

// TODO move some definitions into parsekit, as far as they match Go standard formatting of numbers.

import (
	"math"
	"strconv"

	"git.makaay.nl/mauricem/go-parsekit/parse"
	"git.makaay.nl/mauricem/go-toml/ast"
)

var (
	// Integers are whole numbers. Positive numbers may be prefixed with a
	// plus sign. Negative numbers are prefixed with a minus sign. For large
	// numbers, you may use underscores between digits to enhance readability.
	// Each underscore must be surrounded by at least one digit on each side.
	// Leading zeros are not allowed.
	integerPrefix    = a.Signed(a.DigitNotZero.Then(a.Digits.Optional())).Or(a.Signed(a.Zero))
	underscoreDigits = m.Drop(a.Underscore).Then(a.Digits)
	integerSuffix    = c.ZeroOrMore(underscoreDigits)
	integer          = integerPrefix.Then(integerSuffix)

	// Integer values -0 and +0 are valid and identical to an unprefixed zero.
	plusZero  = a.Plus.Then(a.Zero)
	minusZero = a.Minus.Then(a.Zero)

	// Non-negative integer values may also be expressed in hexadecimal, octal,
	// or binary. In these formats, leading + is not allowed and leading zeros
	// are allowed (after the prefix). Hex values are case insensitive.
	// Underscores are allowed between digits (but not between the prefix
	// and the value).
	// Hexadecimal with prefix `0x`.
	hexDigits           = c.OneOrMore(a.HexDigit)
	underscoreHexDigits = m.Drop(a.Underscore).Then(hexDigits)
	hexadecimal         = a.Rune('x').Then(tok.Str("x", hexDigits.Then(c.ZeroOrMore(underscoreHexDigits))))
	// Octal with prefix `0o`.
	octalDigits           = c.OneOrMore(a.RuneRange('0', '7'))
	underscoreOctalDigits = m.Drop(a.Underscore).Then(octalDigits)
	octal                 = a.Rune('o').Then(tok.Str("o", octalDigits.Then(c.ZeroOrMore(underscoreOctalDigits))))
	// Binary with prefix `0b`.
	binaryDigits           = c.OneOrMore(a.RuneRange('0', '1'))
	underscoreBinaryDigits = m.Drop(a.Underscore).Then(binaryDigits)
	binary                 = a.Rune('b').Then(tok.Str("b", binaryDigits.Then(c.ZeroOrMore(underscoreBinaryDigits))))

	// A fractional part is a decimal point followed by one or more digits.
	// Similar to integers, you may use underscores to enhance readability.
	// Each underscore must be surrounded by at least one digit.
	fractionalPart = a.Dot.Then(a.Digits).Then(c.ZeroOrMore(underscoreDigits))

	// An exponent part is an E (upper or lower case) followed by an integer
	// part (which follows the same rules as decimal integer values).
	exponentPart = a.Runes('e', 'E').Then(integer)

	// Floats should be implemented as IEEE 754 binary64 values.
	// A float consists of an integer part (which follows the same rules as
	// decimal integer values) followed by a fractional part and/or an exponent
	// part. If both a fractional part and exponent part are present, the
	// fractional part must precede the exponent part.
	fractAndOrExp = c.Any(fractionalPart.Then(c.Optional(exponentPart)), exponentPart)
	float         = integer.Then(fractAndOrExp)

	// Float values -0.0 and +0.0 are valid and should map according to IEEE 754.
	zeroFloat = a.Signed(a.Str("0.0"))

	// Special float values can also be expressed. They are always lowercase.
	inf = a.Signed(a.Str("inf"))
	nan = a.Signed(a.Str("nan"))
)

func (t *parser) parseNumber(p *parse.API) (*ast.Value, bool) {
	switch {
	case p.Accept(tok.Float64(nil, float)):
		return ast.NewValue(ast.TypeFloat, p.Result().Value(0).(float64)), true
	case p.Accept(nan):
		return ast.NewValue(ast.TypeFloat, math.NaN()), true
	case p.Accept(inf):
		if p.Result().Rune(0) == '-' {
			return ast.NewValue(ast.TypeFloat, math.Inf(-1)), true
		}
		return ast.NewValue(ast.TypeFloat, math.Inf(+1)), true
	case p.Accept(a.Zero):
		return t.parseIntegerStartingWithZero(p)
	case p.Accept(tok.Int64(nil, integer)):
		return ast.NewValue(ast.TypeInteger, p.Result().Value(0).(int64)), true
	default:
		p.Expected("a number")
		return nil, false
	}
}

func (t *parser) parseIntegerStartingWithZero(p *parse.API) (*ast.Value, bool) {
	var value int64
	var err error
	switch {
	case p.Accept(hexadecimal):
		value, err = strconv.ParseInt(p.Result().Value(0).(string), 16, 64)
	case p.Accept(octal):
		value, err = strconv.ParseInt(p.Result().Value(0).(string), 8, 64)
	case p.Accept(binary):
		value, err = strconv.ParseInt(p.Result().Value(0).(string), 2, 64)
	default:
		return ast.NewValue(ast.TypeInteger, int64(0)), true
	}
	if err == nil {
		return ast.NewValue(ast.TypeInteger, value), true
	}
	p.Error("invalid integer value 0%s: %s", p.Result().String(), err)
	return nil, false
}
