// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Adapted from math/big/ftoa.go.

package apd

import (
	"fmt"
	"strconv"
)

// Text converts the floating-point number x to a string according
// to the given format. The format is one of:
//
//	'e'	-d.dddde±dd, decimal exponent, exponent digits
//	'E'	-d.ddddE±dd, decimal exponent, exponent digits
//	'f'	-ddddd.dddd, no exponent
//	'g'	like 'e' for large exponents, like 'f' otherwise
//	'G'	like 'E' for large exponents, like 'f' otherwise
//
// If format is a different character, Text returns a "%" followed by the
// unrecognized.Format character. The 'f' format has the possibility of
// displaying precision that is not present in the Decimal when it appends
// zeros (the 'g' format avoids the use of 'f' in this case). All other
// formats always show the exact precision of the Decimal.
func (d *Decimal) Text(format byte) string {
	var buf [16]byte
	return string(d.Append(buf[:0], format))
}

// String formats x like x.Text('G'). It matches the to-scientific-string
// conversion of the GDA spec.
func (d *Decimal) String() string {
	return d.Text('G')
}

// lowestZeroNegativeCoefficientCockroach is the smallest co-efficient in
// Cockroach supports using 0E<coefficient>.
const lowestZeroNegativeCoefficientCockroach = -2000

// Append appends to buf the string form of the decimal number d,
// as generated by d.Text, and returns the extended buffer.
func (d *Decimal) Append(buf []byte, fmtString byte) []byte {
	// sign
	if d.Negative {
		buf = append(buf, '-')
	}

	switch d.Form {
	case Finite:
		// ignore
	case NaN:
		return append(buf, "NaN"...)
	case NaNSignaling:
		return append(buf, "sNaN"...)
	case Infinite:
		return append(buf, "Infinity"...)
	default:
		return append(buf, "unknown"...)
	}

	var scratch [16]byte
	digits := d.Coeff.Append(scratch[:0], 10)
	switch fmtString {
	case 'e', 'E':
		return fmtE(buf, fmtString, d, digits)
	case 'f':
		return fmtF(buf, d, digits)
	case 'g', 'G':
		digitLen := len(digits)
		// PG formats all 0s after the decimal point in the 0E-<exponent> case
		// (e.g. 0E-9 should be 0.000000000). With the `adjExponentLimit` below,
		// this does not do that, so for 0 with negative coefficients we pad
		// the digit length.
		// Ref: https://github.com/cockroachdb/cockroach/issues/102217
		//
		// To avoid leaking too much memory for pathological cases, e.g.
		// 0E-100000000, we also fall back to the default exponent format
		// handling when the exponent is below cockroach's lowest supported 0
		// coefficient.
		if d.Coeff.BitLen() == 0 && d.Exponent >= lowestZeroNegativeCoefficientCockroach && d.Exponent < 0 {
			digitLen += int(-d.Exponent)
		}
		// See: http://speleotrove.com/decimal/daconvs.html#reftostr
		const adjExponentLimit = -6
		adj := int(d.Exponent) + (digitLen - 1)
		if d.Exponent <= 0 && adj >= adjExponentLimit {
			return fmtF(buf, d, digits)
		}
		// We need to convert the either g or G into a e or E since that's what fmtE
		// expects. This is indeed fmtString - 2, but attempting to do that in a way that
		// illustrates the intention.
		return fmtE(buf, fmtString+'e'-'g', d, digits)
	}

	if d.Negative {
		buf = buf[:len(buf)-1] // sign was added prematurely - remove it again
	}
	return append(buf, '%', fmtString)
}

// %e: d.ddddde±d
func fmtE(buf []byte, fmt byte, d *Decimal, digits []byte) []byte {
	adj := int64(d.Exponent) + int64(len(digits)) - 1
	buf = append(buf, digits[0])
	if len(digits) > 1 {
		buf = append(buf, '.')
		buf = append(buf, digits[1:]...)
	}
	buf = append(buf, fmt)
	var ch byte
	if adj < 0 {
		ch = '-'
		adj = -adj
	} else {
		ch = '+'
	}
	buf = append(buf, ch)
	return strconv.AppendInt(buf, adj, 10)
}

// %f: ddddddd.ddddd
func fmtF(buf []byte, d *Decimal, digits []byte) []byte {
	if d.Exponent < 0 {
		if left := -int(d.Exponent) - len(digits); left >= 0 {
			buf = append(buf, "0."...)
			for i := 0; i < left; i++ {
				buf = append(buf, '0')
			}
			buf = append(buf, digits...)
		} else if left < 0 {
			offset := -left
			buf = append(buf, digits[:offset]...)
			buf = append(buf, '.')
			buf = append(buf, digits[offset:]...)
		}
	} else if d.Exponent >= 0 {
		buf = append(buf, digits...)
		for i := int32(0); i < d.Exponent; i++ {
			buf = append(buf, '0')
		}
	}
	return buf
}

var _ fmt.Formatter = decimalZero // *Decimal must implement fmt.Formatter

// Format implements fmt.Formatter. It accepts many of the regular formats for
// floating-point numbers ('e', 'E', 'f', 'F', 'g', 'G') as well as 's' and 'v',
// which are handled like 'G'. Format also supports the output field width, as
// well as the format flags '+' and ' ' for sign control, '0' for space or zero
// padding, and '-' for left or right justification. It does not support
// precision. See the fmt package for details.
func (d *Decimal) Format(s fmt.State, format rune) {
	switch format {
	case 'e', 'E', 'f', 'g', 'G':
		// nothing to do
	case 'F':
		// (*Decimal).Text doesn't support 'F'; handle like 'f'
		format = 'f'
	case 'v', 's':
		// handle like 'G'
		format = 'G'
	default:
		fmt.Fprintf(s, "%%!%c(*apd.Decimal=%s)", format, d.String())
		return
	}
	var buf []byte
	buf = d.Append(buf, byte(format))
	if len(buf) == 0 {
		buf = []byte("?") // should never happen, but don't crash
	}
	// len(buf) > 0

	var sign string
	switch {
	case buf[0] == '-':
		sign = "-"
		buf = buf[1:]
	case buf[0] == '+':
		// +Inf
		sign = "+"
		if s.Flag(' ') {
			sign = " "
		}
		buf = buf[1:]
	case s.Flag('+'):
		sign = "+"
	case s.Flag(' '):
		sign = " "
	}

	var padding int
	if width, hasWidth := s.Width(); hasWidth && width > len(sign)+len(buf) {
		padding = width - len(sign) - len(buf)
	}

	switch {
	case s.Flag('0') && d.Form == Finite:
		// 0-padding on left
		writeMultiple(s, sign, 1)
		writeMultiple(s, "0", padding)
		s.Write(buf)
	case s.Flag('-'):
		// padding on right
		writeMultiple(s, sign, 1)
		s.Write(buf)
		writeMultiple(s, " ", padding)
	default:
		// padding on left
		writeMultiple(s, " ", padding)
		writeMultiple(s, sign, 1)
		s.Write(buf)
	}
}

// write count copies of text to s
func writeMultiple(s fmt.State, text string, count int) {
	if len(text) > 0 {
		b := []byte(text)
		for ; count > 0; count-- {
			s.Write(b)
		}
	}
}
