// Copyright 2013-2015 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Semantic Versions http://semver.org

// This file was originally vendored from:
// https://github.com/coreos/go-semver/tree/e214231b295a8ea9479f11b70b35d5acf3556d9b/semver
// There isn't a single line left from the original source today, but being generous about
// attribution won't hurt.
package semver

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"

	"github.com/open-policy-agent/opa/v1/util"
)

// reMetaIdentifier matches pre-release and metadata identifiers against the spec requirements
var reMetaIdentifier = regexp.MustCompile(`^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$`)

// Version represents a parsed SemVer
type Version struct {
	Major      int64
	Minor      int64
	Patch      int64
	PreRelease string `json:"PreRelease,omitempty"`
	Metadata   string `json:"Metadata,omitempty"`
}

// Parse constructs new semver Version from version string.
func Parse(version string) (v Version, err error) {
	version = strings.TrimPrefix(version, "v")

	var foundMetadata bool

	version, v.Metadata, foundMetadata = strings.Cut(version, "+")
	if foundMetadata && !reMetaIdentifier.MatchString(v.Metadata) {
		return v, fmt.Errorf("invalid metadata identifier: %s", v.Metadata)
	}

	var foundPreRelease bool

	version, v.PreRelease, foundPreRelease = strings.Cut(version, "-")
	if foundPreRelease && (!reMetaIdentifier.MatchString(v.PreRelease) || !validPreRelease(v.PreRelease)) {
		return v, fmt.Errorf("invalid pre-release identifier: %s", v.PreRelease)
	}

	if strings.Count(version, ".") != 2 {
		return v, fmt.Errorf("%s should contain major, minor, and patch versions", version)
	}

	major, after := cutDot(version)
	if v.Major, err = parseNumeric(major); err != nil {
		return v, fmt.Errorf("invalid major version: %w", err)
	}

	minor, after := cutDot(after)
	if v.Minor, err = parseNumeric(minor); err != nil {
		return v, fmt.Errorf("invalid minor version: %w", err)
	}

	if v.Patch, err = parseNumeric(after); err != nil {
		return v, fmt.Errorf("invalid patch version: %w", err)
	}

	return v, nil
}

// parseNumeric parses a major, minor or patch identifier, rejecting the empty
// string, a sign or a leading zero (all forbidden by SemVer 2.0.0) before
// converting to int64.
func parseNumeric(s string) (int64, error) {
	if s == "" || s[0] == '+' || s[0] == '-' || (len(s) > 1 && s[0] == '0') {
		return 0, fmt.Errorf("%q is not a valid numeric identifier", s)
	}
	return strconv.ParseInt(s, 10, 64)
}

// validPreRelease reports whether every numeric pre-release identifier is free
// of leading zeroes, as required by SemVer 2.0.0. The identifier character set
// has already been checked by reMetaIdentifier.
func validPreRelease(pre string) bool {
	for id := range strings.SplitSeq(pre, ".") {
		if len(id) > 1 && id[0] == '0' && isAllDecimals(id) {
			return false
		}
	}
	return true
}

// MustParse is like Parse but panics if the version string is invalid instead of returning an error.
func MustParse(version string) Version {
	v, err := Parse(version)
	if err != nil {
		panic(err)
	}

	return v
}

// Compare compares two semver strings.
func Compare(a, b string) int {
	aV, err := Parse(a)
	if err != nil {
		return -1
	}

	bV, err := Parse(b)
	if err != nil {
		return 1
	}

	return aV.Compare(bV)
}

// AppendString appends the textual representation of the version to b and returns the extended buffer.
// This method conforms to the encoding.TextAppender interface, and is useful for serializing the Version
// without allocating, provided the caller has pre-allocated sufficient space in b.
func (v Version) AppendString(b []byte) ([]byte, error) {
	if b == nil {
		b = make([]byte, 0, length(v))
	}

	b = append(strconv.AppendInt(b, v.Major, 10), '.')
	b = append(strconv.AppendInt(b, v.Minor, 10), '.')
	b = strconv.AppendInt(b, v.Patch, 10)

	if v.PreRelease != "" {
		b = append(append(b, '-'), v.PreRelease...)
	}
	if v.Metadata != "" {
		b = append(append(b, '+'), v.Metadata...)
	}

	return b, nil
}

// String returns the string representation of the version.
func (v Version) String() string {
	bs := make([]byte, 0, length(v))
	bs, _ = v.AppendString(bs)

	return string(bs)
}

// Compare tests if v is less than, equal to, or greater than other, returning -1, 0, or +1 respectively.
// Comparison is based on the SemVer specification (https://semver.org/#spec-item-11).
func (v Version) Compare(other Version) int {
	if v.Major > other.Major {
		return 1
	} else if v.Major < other.Major {
		return -1
	}

	if v.Minor > other.Minor {
		return 1
	} else if v.Minor < other.Minor {
		return -1
	}

	if v.Patch > other.Patch {
		return 1
	} else if v.Patch < other.Patch {
		return -1
	}

	if v.PreRelease == other.PreRelease {
		return 0
	}

	// if two versions are otherwise equal it is the one without a pre-release that is greater
	if v.PreRelease == "" && other.PreRelease != "" {
		return 1
	}
	if other.PreRelease == "" && v.PreRelease != "" {
		return -1
	}

	a, afterA := cutDot(v.PreRelease)
	b, afterB := cutDot(other.PreRelease)

	for {
		if a == "" && b != "" {
			return -1
		}
		if a != "" && b == "" {
			return 1
		}

		aIsInt := isAllDecimals(a)
		bIsInt := isAllDecimals(b)

		// numeric identifiers have lower precedence than non-numeric
		if aIsInt && !bIsInt {
			return -1
		} else if !aIsInt && bIsInt {
			return 1
		}

		if aIsInt && bIsInt {
			aInt, _ := strconv.Atoi(a)
			bInt, _ := strconv.Atoi(b)

			if aInt > bInt {
				return 1
			} else if aInt < bInt {
				return -1
			}
		} else {
			// string comparison
			if a > b {
				return 1
			} else if a < b {
				return -1
			}
		}

		// a larger set of pre-release fields has a higher precedence than a
		// smaller set, if all of the preceding identifiers are equal.
		if afterA != "" && afterB == "" {
			return 1
		} else if afterA == "" && afterB != "" {
			return -1
		}

		a, afterA = cutDot(afterA)
		b, afterB = cutDot(afterB)
	}
}

func isAllDecimals(s string) bool {
	for _, r := range s {
		if r < '0' || r > '9' {
			return false
		}
	}
	return s != ""
}

// length allows calculating the length of the version for pre-allocation.
func length(v Version) int {
	n := util.NumDigitsInt64(v.Major) + util.NumDigitsInt64(v.Minor) + util.NumDigitsInt64(v.Patch) + 2
	if v.PreRelease != "" {
		n += len(v.PreRelease) + 1
	}
	if v.Metadata != "" {
		n += len(v.Metadata) + 1
	}
	return n
}

// cutDot is a *slightly* faster version of strings.Cut for the '.' separator,
// skipping the boolean return value. strings.Cut looks the separator up with
// strings.Index, which costs ~12% on BenchmarkCompare next to IndexByte.
//
//nolint:modernize // stringscut: measurably slower here, see above.
func cutDot(s string) (before, after string) {
	if i := strings.IndexByte(s, '.'); i >= 0 {
		return s[:i], s[i+1:]
	}
	return s, ""
}
