package mlrval

import (
	"fmt"
	"os"
	"strconv"
	"strings"

	"golang.org/x/text/language"
	"golang.org/x/text/message"
)

//----------------------------------------------------------------
// TODO
//* need int/float
//  llx -> x etc
//  https://golang.org/pkg/fmt/
//
//  pre-stuff
//
//  %
//
//  +-0' space
//
//  ll|l
//  %%
//  bdiouxDOUX fegFEG s
//
//  post-stuff

//* callsites:
//  o fmtnum($mv, "%d")
//    - numeric only
//  o format($mv, "%s")
//    - make this new DSL function
//  o --ofmt
//    - numeric only
//  k format-values verb
//    - -i, -f, -s

// Nil means use default format.
// Set from the CLI parser using mlr --ofmt.
var floatOutputFormatter IFormatter = nil

func SetFloatOutputFormat(formatString string) error {
	formatter, err := GetFormatter(formatString)
	if err != nil {
		return err
	}
	floatOutputFormatter = formatter
	return nil
}

var formatterCache map[string]IFormatter = make(map[string]IFormatter)

type IFormatter interface {
	Format(mlrval *Mlrval) *Mlrval
	FormatFloat(floatValue float64) string // for --ofmt
}

func GetFormatter(
	userLevelFormatString string,
) (IFormatter, error) {
	// Cache hit
	formatter, ok := formatterCache[userLevelFormatString]
	if ok {
		return formatter, nil
	}

	// Cache miss
	formatter, err := newFormatter(userLevelFormatString)
	if err != nil {
		// TODO: temp exit
		fmt.Printf("mlr: %v\n", err)
		return nil, err
	}

	formatterCache[userLevelFormatString] = formatter
	return formatter, nil
}

// People can pass in things like "X%sX" unfortunately :(
func newFormatter(
	userLevelFormatString string,
) (IFormatter, error) {
	numPercents := strings.Count(userLevelFormatString, "%")
	if numPercents < 1 {
		return nil, fmt.Errorf("unhandled format string \"%s\": no leading \"%%\"", userLevelFormatString)
	}
	if numPercents > 1 {
		return nil, fmt.Errorf(
			"unhandled format string \"%s\": needs no \"%%\" after the first", userLevelFormatString,
		)
	}

	// TODO: perhaps a full format-string parser. At present, there's nothing to stop people
	// from doing silly things like "%lllld".
	goFormatString := userLevelFormatString
	goFormatString = strings.ReplaceAll(goFormatString, "lld", "d")
	goFormatString = strings.ReplaceAll(goFormatString, "llx", "x")
	goFormatString = strings.ReplaceAll(goFormatString, "ld", "d")
	goFormatString = strings.ReplaceAll(goFormatString, "lx", "x")
	goFormatString = strings.ReplaceAll(goFormatString, "lf", "f")
	goFormatString = strings.ReplaceAll(goFormatString, "le", "e")
	goFormatString = strings.ReplaceAll(goFormatString, "lg", "g")

	// Miller 5 and below required C format strings compatible with 64-bit ints
	// and double-precision floats: e.g. "%08lld" and "%9.6lf". For Miller 6,
	// we must still accept these for backward compatibility.
	if strings.HasSuffix(goFormatString, "_d") {
		// Special sub-case of "d"; must be checked first
		n := len(goFormatString)
		return newFormatterToSeparatedInt(goFormatString[:n-2] + "d"), nil
	}
	if strings.HasSuffix(goFormatString, "d") {
		return newFormatterToInt(goFormatString), nil
	}
	if strings.HasSuffix(goFormatString, "x") {
		return newFormatterToInt(goFormatString), nil
	}

	if strings.HasSuffix(goFormatString, "_f") {
		// Special sub-case of "f"; must be checked first
		n := len(goFormatString)
		return newFormatterToSeparatedFloat(goFormatString[:n-2] + "f"), nil
	}
	if strings.HasSuffix(goFormatString, "f") {
		return newFormatterToFloat(goFormatString), nil
	}
	if strings.HasSuffix(goFormatString, "e") {
		return newFormatterToFloat(goFormatString), nil
	}
	if strings.HasSuffix(goFormatString, "g") {
		return newFormatterToFloat(goFormatString), nil
	}

	if strings.HasSuffix(goFormatString, "s") {
		return newFormatterToString(goFormatString), nil
	}

	// TODO:
	// return nil, errors.New(fmt.Sprintf("unhandled format string \"%s\"", userLevelFormatString))
	return newFormatterToString(goFormatString), nil
}

type formatterToFloat struct {
	goFormatString string
}

func newFormatterToFloat(goFormatString string) IFormatter {
	return &formatterToFloat{
		goFormatString: goFormatString,
	}
}

func (fmtr *formatterToFloat) Format(mv *Mlrval) *Mlrval {
	floatValue, isFloat := mv.GetFloatValue()
	if isFloat {
		formatted := fmt.Sprintf(fmtr.goFormatString, floatValue)
		return TryFromFloatString(formatted)
	}
	intValue, isInt := mv.GetIntValue()
	if isInt {
		formatted := fmt.Sprintf(fmtr.goFormatString, float64(intValue))
		return TryFromFloatString(formatted)
	}
	return mv
}

func (fmtr *formatterToFloat) FormatFloat(floatValue float64) string {
	return fmt.Sprintf(fmtr.goFormatString, floatValue)
}

func getLanguageTag() language.Tag {
	v, ok := os.LookupEnv("LANG")
	if ok {
		return language.Make(v)
	}
	return language.Make("en")
}

type formatterToSeparatedInt struct {
	goFormatString string
	printer        *message.Printer
}

func newFormatterToSeparatedInt(goFormatString string) IFormatter {
	return &formatterToSeparatedInt{
		goFormatString: goFormatString,
		printer:        message.NewPrinter(getLanguageTag()),
	}
}

func (fmtr *formatterToSeparatedInt) Format(mv *Mlrval) *Mlrval {
	intValue, isInt := mv.GetIntValue()
	if isInt {
		formatted := fmtr.printer.Sprintf(fmtr.goFormatString, intValue)
		return TryFromIntString(formatted)
	}
	floatValue, isFloat := mv.GetFloatValue()
	if isFloat {
		formatted := fmtr.printer.Sprintf(fmtr.goFormatString, int(floatValue))
		return TryFromIntString(formatted)
	}
	return mv
}

func (fmtr *formatterToSeparatedInt) FormatFloat(floatValue float64) string {
	return fmtr.printer.Sprintf(fmtr.goFormatString, int(floatValue))
}

type formatterToSeparatedFloat struct {
	goFormatString string
	printer        *message.Printer
}

func newFormatterToSeparatedFloat(goFormatString string) IFormatter {
	return &formatterToSeparatedFloat{
		goFormatString: goFormatString,
		printer:        message.NewPrinter(getLanguageTag()),
	}
}

func (fmtr *formatterToSeparatedFloat) Format(mv *Mlrval) *Mlrval {
	floatValue, isFloat := mv.GetFloatValue()
	if isFloat {
		formatted := fmtr.printer.Sprintf(fmtr.goFormatString, floatValue)
		return TryFromFloatString(formatted)
	}
	intValue, isInt := mv.GetIntValue()
	if isInt {
		formatted := fmtr.printer.Sprintf(fmtr.goFormatString, float64(intValue))
		return TryFromFloatString(formatted)
	}
	return mv
}

func (fmtr *formatterToSeparatedFloat) FormatFloat(floatValue float64) string {
	return fmtr.printer.Sprintf(fmtr.goFormatString, floatValue)
}

type formatterToInt struct {
	goFormatString string
}

func newFormatterToInt(goFormatString string) IFormatter {
	return &formatterToInt{
		goFormatString: goFormatString,
	}
}

func (fmtr *formatterToInt) Format(mv *Mlrval) *Mlrval {
	intValue, isInt := mv.GetIntValue()
	if isInt {
		formatted := fmt.Sprintf(fmtr.goFormatString, intValue)
		return TryFromIntString(formatted)
	}
	floatValue, isFloat := mv.GetFloatValue()
	if isFloat {
		formatted := fmt.Sprintf(fmtr.goFormatString, int(floatValue))
		return TryFromIntString(formatted)
	}
	return mv
}

func (fmtr *formatterToInt) FormatFloat(floatValue float64) string {
	return fmt.Sprintf(fmtr.goFormatString, int(floatValue))
}

type formatterToString struct {
	goFormatString string
}

func newFormatterToString(goFormatString string) IFormatter {
	return &formatterToString{
		goFormatString: goFormatString,
	}
}

func (fmtr *formatterToString) Format(mv *Mlrval) *Mlrval {
	return FromString(
		fmt.Sprintf(
			fmtr.goFormatString,
			mv.String(),
		),
	)
}

func (fmtr *formatterToString) FormatFloat(floatValue float64) string {
	return strconv.FormatFloat(floatValue, 'g', -1, 64)
}
