// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package version

import (
	"strings"
)

// AtLeast returns whether version is at least the specified minimum
// version.
//
// Version comparison in Tailscale is a little complex, because we
// switched "styles" a few times, and additionally have a completely
// separate track of version numbers for OSS-only builds.
//
// AtLeast acts conservatively, returning true only if it's certain
// that version is at least minimum. As a result, it can produce false
// negatives, for example when an OSS build supports a given feature,
// but AtLeast is called with an official release number as the
// minimum
//
// version and minimum can both be either an official Tailscale
// version numbers (major.minor.patch-extracommits-extrastring), or an
// OSS build datestamp (date.YYYYMMDD). For Tailscale version numbers,
// AtLeast also accepts a prefix of a full version, in which case all
// missing fields are assumed to be zero.
func AtLeast(version string, minimum string) bool {
	v, ok := parse(version)
	if !ok {
		return false
	}
	m, ok := parse(minimum)
	if !ok {
		return false
	}

	switch {
	case v.Datestamp != 0 && m.Datestamp == 0:
		// OSS version vs. Tailscale version
		return false
	case v.Datestamp == 0 && m.Datestamp != 0:
		// Tailscale version vs. OSS version
		return false
	case v.Datestamp != 0:
		// OSS version vs. OSS version
		return v.Datestamp >= m.Datestamp
	case v.Major == m.Major && v.Minor == m.Minor && v.Patch == m.Patch && v.ExtraCommits == m.ExtraCommits:
		// Exactly equal Tailscale versions
		return true
	case v.Major != m.Major:
		return v.Major > m.Major
	case v.Minor != m.Minor:
		return v.Minor > m.Minor
	case v.Patch != m.Patch:
		return v.Patch > m.Patch
	default:
		return v.ExtraCommits > m.ExtraCommits
	}
}

type parsed struct {
	Major, Minor, Patch, ExtraCommits int // for Tailscale version e.g. e.g. "0.99.1-20"
	Datestamp                         int // for OSS version e.g. "date.20200612"
}

func parse(version string) (parsed, bool) {
	if strings.HasPrefix(version, "date.") {
		stamp, ok := atoi(version[5:])
		if !ok {
			return parsed{}, false
		}
		return parsed{Datestamp: stamp}, true
	}

	var ret parsed

	major, rest, ok := splitNumericPrefix(version)
	if !ok {
		return parsed{}, false
	}
	ret.Major = major
	if len(rest) == 0 {
		return ret, true
	}

	ret.Minor, rest, ok = splitNumericPrefix(rest[1:])
	if !ok {
		return parsed{}, false
	}
	if len(rest) == 0 {
		return ret, true
	}

	// Optional patch version, if the next separator is a dot.
	if rest[0] == '.' {
		ret.Patch, rest, ok = splitNumericPrefix(rest[1:])
		if !ok {
			return parsed{}, false
		}
		if len(rest) == 0 {
			return ret, true
		}
	}

	// Optional extraCommits, if the next bit can be completely
	// consumed as an integer.
	if rest[0] != '-' {
		return parsed{}, false
	}

	var trailer string
	ret.ExtraCommits, trailer, ok = splitNumericPrefix(rest[1:])
	if !ok || (len(trailer) > 0 && trailer[0] != '-') {
		// rest was probably the string trailer, ignore it.
		ret.ExtraCommits = 0
	}
	return ret, true
}

func splitNumericPrefix(s string) (n int, rest string, ok bool) {
	for i, r := range s {
		if r >= '0' && r <= '9' {
			continue
		}
		ret, ok := atoi(s[:i])
		if !ok {
			return 0, "", false
		}
		return ret, s[i:], true
	}

	ret, ok := atoi(s)
	if !ok {
		return 0, "", false
	}
	return ret, "", true
}

const (
	maxUint = ^uint(0)
	maxInt  = int(maxUint >> 1)
)

// atoi parses an int from a string s.
// The bool result reports whether s is a number
// representable by a value of type int.
//
// From Go's runtime/string.go.
func atoi(s string) (int, bool) {
	if s == "" {
		return 0, false
	}

	neg := false
	if s[0] == '-' {
		neg = true
		s = s[1:]
	}

	un := uint(0)
	for i := range len(s) {
		c := s[i]
		if c < '0' || c > '9' {
			return 0, false
		}
		if un > maxUint/10 {
			// overflow
			return 0, false
		}
		un *= 10
		un1 := un + uint(c) - '0'
		if un1 < un {
			// overflow
			return 0, false
		}
		un = un1
	}

	if !neg && un > uint(maxInt) {
		return 0, false
	}
	if neg && un > uint(maxInt)+1 {
		return 0, false
	}

	n := int(un)
	if neg {
		n = -n
	}

	return n, true
}
