// Package printf implements a parser for fmt.Printf-style format
// strings.
//
// It parses verbs according to the following syntax:
//
//	Numeric -> '0'-'9'
//	Letter -> 'a'-'z' | 'A'-'Z'
//	Index -> '[' Numeric+ ']'
//	Star -> '*'
//	Star -> Index '*'
//
//	Precision -> Numeric+ | Star
//	Width -> Numeric+ | Star
//
//	WidthAndPrecision -> Width '.' Precision
//	WidthAndPrecision -> Width '.'
//	WidthAndPrecision -> Width
//	WidthAndPrecision -> '.' Precision
//	WidthAndPrecision -> '.'
//
//	Flag -> '+' | '-' | '#' | ' ' | '0'
//	Verb -> Letter | '%'
//
//	Input -> '%' [ Flag+ ] [ WidthAndPrecision ] [ Index ] Verb
package printf

import (
	"errors"
	"regexp"
	"strconv"
	"strings"
)

// ErrInvalid is returned for invalid format strings or verbs.
var ErrInvalid = errors.New("invalid format string")

type Verb struct {
	Letter rune
	Flags  string

	Width     Argument
	Precision Argument
	// Which value in the argument list the verb uses.
	// -1 denotes the next argument,
	// values > 0 denote explicit arguments.
	// The value 0 denotes that no argument is consumed. This is the case for %%.
	Value int

	Raw string
}

// Argument is an implicit or explicit width or precision.
type Argument interface {
	isArgument()
}

// The Default value, when no width or precision is provided.
type Default struct{}

// Zero is the implicit zero value.
// This value may only appear for precisions in format strings like %6.f
type Zero struct{}

// Star is a * value, which may either refer to the next argument (Index == -1) or an explicit argument.
type Star struct{ Index int }

// A Literal value, such as 6 in %6d.
type Literal int

func (Default) isArgument() {}
func (Zero) isArgument()    {}
func (Star) isArgument()    {}
func (Literal) isArgument() {}

// Parse parses f and returns a list of actions.
// An action may either be a literal string, or a Verb.
func Parse(f string) ([]any, error) {
	var out []any
	for len(f) > 0 {
		if f[0] == '%' {
			v, n, err := ParseVerb(f)
			if err != nil {
				return nil, err
			}
			f = f[n:]
			out = append(out, v)
		} else {
			n := strings.IndexByte(f, '%')
			if n > -1 {
				out = append(out, f[:n])
				f = f[n:]
			} else {
				out = append(out, f)
				f = ""
			}
		}
	}

	return out, nil
}

func atoi(s string) int {
	n, _ := strconv.Atoi(s)
	return n
}

// ParseVerb parses the verb at the beginning of f.
// It returns the verb, how much of the input was consumed, and an error, if any.
func ParseVerb(f string) (Verb, int, error) {
	if len(f) < 2 {
		return Verb{}, 0, ErrInvalid
	}
	const (
		flags = 1

		width      = 2
		widthStar  = 3
		widthIndex = 5

		dot       = 6
		prec      = 7
		precStar  = 8
		precIndex = 10

		verbIndex = 11
		verb      = 12
	)

	m := re.FindStringSubmatch(f)
	if m == nil {
		return Verb{}, 0, ErrInvalid
	}

	v := Verb{
		Letter: []rune(m[verb])[0],
		Flags:  m[flags],
		Raw:    m[0],
	}

	if m[width] != "" {
		// Literal width
		v.Width = Literal(atoi(m[width]))
	} else if m[widthStar] != "" {
		// Star width
		if m[widthIndex] != "" {
			v.Width = Star{atoi(m[widthIndex])}
		} else {
			v.Width = Star{-1}
		}
	} else {
		// Default width
		v.Width = Default{}
	}

	if m[dot] == "" {
		// default precision
		v.Precision = Default{}
	} else {
		if m[prec] != "" {
			// Literal precision
			v.Precision = Literal(atoi(m[prec]))
		} else if m[precStar] != "" {
			// Star precision
			if m[precIndex] != "" {
				v.Precision = Star{atoi(m[precIndex])}
			} else {
				v.Precision = Star{-1}
			}
		} else {
			// Zero precision
			v.Precision = Zero{}
		}
	}

	if m[verb] == "%" {
		v.Value = 0
	} else if m[verbIndex] != "" {
		v.Value = atoi(m[verbIndex])
	} else {
		v.Value = -1
	}

	return v, len(m[0]), nil
}

const (
	flags             = `([+#0 -]*)`
	verb              = `([a-zA-Z%])`
	index             = `(?:\[([0-9]+)\])`
	star              = `((` + index + `)?\*)`
	width1            = `([0-9]+)`
	width2            = star
	width             = `(?:` + width1 + `|` + width2 + `)`
	precision         = width
	widthAndPrecision = `(?:(?:` + width + `)?(?:(\.)(?:` + precision + `)?)?)`
)

var re = regexp.MustCompile(`^%` + flags + widthAndPrecision + `?` + index + `?` + verb)
