package util

import (
	"cmp"
	"fmt"
	"strconv"
	"strings"
)

// SemVer stores a numeric semantic version as major, minor, patch, and build
// components. Components that are not provided are represented as zero values.
type SemVer struct {
	Major uint
	Minor uint
	Patch uint
	Build uint
}

// NewSemver constructs a SemVer from v.
//
// NewSemver requires between one and four components ordered as major, minor,
// patch, and build. Components that are not provided are set to zero.
func NewSemver(v ...uint) (SemVer, error) {
	s := &SemVer{}
	switch len(v) {
	case 4:
		s.Build = v[3]
		fallthrough
	case 3:
		s.Patch = v[2]
		fallthrough
	case 2:
		s.Minor = v[1]
		fallthrough
	case 1:
		s.Major = v[0]
	default:
		return *s, fmt.Errorf("new semver: incorrect args count: %d", len(v))
	}
	return *s, nil
}

// MustSemver returns the SemVer produced by NewSemver(v...).
//
// MustSemver panics if NewSemver returns an error.
func MustSemver(v ...uint) SemVer {
	if v, err := NewSemver(v...); err != nil {
		panic(err)
	} else {
		return v
	}
}

// ParseSemver parses input into a SemVer.
//
// ParseSemver accepts versions with one to four numeric components separated by
// dots, for example "1", "1.2", "1.2.3", or "1.2.3.4". The input may also use
// the optional "v" or "v." prefix. Missing minor, patch, and build components
// are set to zero.
func ParseSemver(input string) (SemVer, error) {
	source := input

	switch {
	case strings.HasPrefix(input, "v."):
		input = strings.TrimPrefix(input, "v.")
	case strings.HasPrefix(input, "v"):
		input = strings.TrimPrefix(input, "v")
	}

	if input == "" {
		return SemVer{}, fmt.Errorf("parse semver %q: missing major version", source)
	}

	parts := strings.Split(input, ".")
	if len(parts) > 4 {
		return SemVer{}, fmt.Errorf("parse semver %q: too many version parts: %d", source, len(parts))
	}

	values := make([]uint, 0, len(parts))
	for idx, part := range parts {
		if part == "" {
			return SemVer{}, fmt.Errorf("parse semver %q: empty version part at position %d", source, idx+1)
		}

		value, err := strconv.ParseUint(part, 10, 0)
		if err != nil {
			return SemVer{}, fmt.Errorf("parse semver %q: parse part %q at position %d: %w", source, part, idx+1, err)
		}

		values = append(values, uint(value))
	}

	version, err := NewSemver(values...)
	if err != nil {
		return SemVer{}, fmt.Errorf("parse semver %q: %w", source, err)
	}

	return version, nil
}

func MustParseSemver(input string) SemVer {
	if v, err := ParseSemver(input); err != nil {
		panic(err)
	} else {
		return v
	}
}

// CompareSemver compares two semantic versions and returns:
//
//	+1 if x is less than y,
//	 0 if x equals y,
//	-1 if x is greater than y.
func CompareSemver(x, y SemVer) int {
	return cmp.Or(
		cmp.Compare(y.Major, x.Major),
		cmp.Compare(y.Minor, x.Minor),
		cmp.Compare(y.Patch, x.Patch),
		cmp.Compare(y.Build, x.Build),
	)
}
