package versions

import (
	"fmt"

	"github.com/apparentlymart/go-versions/versions/constraints"
)

// ParseVersion attempts to parse the given string as a semantic version
// specification, and returns the result if successful.
//
// If the given string is not parseable then an error is returned that is
// suitable for display directly to a hypothetical end-user that provided this
// version string, as long as they can read English.
func ParseVersion(s string) (Version, error) {
	spec, err := constraints.ParseExactVersion(s)
	if err != nil {
		return Unspecified, err
	}
	return versionFromExactVersionSpec(spec), nil
}

// MustParseVersion is the same as ParseVersion except that it will panic
// instead of returning an error.
func MustParseVersion(s string) Version {
	v, err := ParseVersion(s)
	if err != nil {
		panic(err)
	}
	return v
}

// MeetingConstraints returns a version set that contains all of the versions
// that meet the given constraints, specified using the Spec type from the
// constraints package.
//
// The resulting Set has all pre-release versions excluded, except any that
// are explicitly mentioned as exact selections. For example, the constraint
// "2.0.0-beta1 || >2" contains 2.0.0-beta1 but not 2.0.0-beta2 or 3.0.0-beta1.
// This additional constraint on pre-releases can be avoided by calling
// MeetingConstraintsExact instead, at which point the caller can apply other
// logic to deal with prereleases.
//
// This function expects an internally-consistent Spec like what would be
// generated by that package's constraint parsers. Behavior is undefined --
// including the possibility of panics -- if specs are hand-created and the
// expected invariants aren't met.
func MeetingConstraints(spec constraints.Spec) Set {
	exact := MeetingConstraintsExact(spec)
	reqd := exact.AllRequested().List()
	set := Intersection(Released, exact)
	reqd = reqd.Filter(Prerelease).Filter(exact)
	if len(reqd) != 0 {
		set = Union(Selection(reqd...), set)
	}
	return set
}

// MeetingConstraintsExact is like MeetingConstraints except that it doesn't
// apply the extra rules to exclude pre-release versions that are not
// explicitly requested.
//
// This means that given a constraint ">=1.0.0 <2.0.0" a hypothetical version
// 2.0.0-beta1 _is_ in the returned set, because prerelease versions have
// lower precedence than their corresponding release.
//
// A caller can use this to implement its own specialized handling of
// pre-release versions by applying additional set operations to the result,
// such as intersecting it with the predefined set versions.Released to
// remove prerelease versions altogether.
func MeetingConstraintsExact(spec constraints.Spec) Set {
	if spec == nil {
		return All
	}

	switch ts := spec.(type) {

	case constraints.VersionSpec:
		lowerBound, upperBound := ts.ConstraintBounds()
		switch lowerBound.Operator {
		case constraints.OpUnconstrained:
			return All
		case constraints.OpEqual:
			return Only(versionFromExactVersionSpec(lowerBound.Boundary))
		default:
			return AtLeast(
				versionFromExactVersionSpec(lowerBound.Boundary),
			).Intersection(
				OlderThan(versionFromExactVersionSpec(upperBound.Boundary)))
		}

	case constraints.SelectionSpec:
		lower := ts.Boundary.ConstrainToZero()
		if ts.Operator != constraints.OpEqual && ts.Operator != constraints.OpNotEqual {
			lower.Metadata = "" // metadata is only considered for exact matches
		}

		switch ts.Operator {
		case constraints.OpUnconstrained:
			// Degenerate case, but we'll allow it.
			return All
		case constraints.OpMatch:
			// The match operator uses the constraints implied by the
			// Boundary version spec as the specification.
			// Note that we discard "lower" in this case, because we do want
			// to match our metadata if it's specified.
			return MeetingConstraintsExact(ts.Boundary)
		case constraints.OpEqual, constraints.OpNotEqual:
			set := Only(versionFromExactVersionSpec(lower))
			if ts.Operator == constraints.OpNotEqual {
				// We want everything _except_ what's in our set, then.
				set = All.Subtract(set)
			}
			return set
		case constraints.OpGreaterThan:
			return NewerThan(versionFromExactVersionSpec(lower))
		case constraints.OpGreaterThanOrEqual:
			return AtLeast(versionFromExactVersionSpec(lower))
		case constraints.OpLessThan:
			return OlderThan(versionFromExactVersionSpec(lower))
		case constraints.OpLessThanOrEqual:
			return AtMost(versionFromExactVersionSpec(lower))
		case constraints.OpGreaterThanOrEqualMinorOnly:
			upper := lower
			upper.Major.Num++
			upper.Minor.Num = 0
			upper.Patch.Num = 0
			upper.Prerelease = ""
			return AtLeast(
				versionFromExactVersionSpec(lower),
			).Intersection(
				OlderThan(versionFromExactVersionSpec(upper)))
		case constraints.OpGreaterThanOrEqualPatchOnly:
			upper := lower
			upper.Minor.Num++
			upper.Patch.Num = 0
			upper.Prerelease = ""
			return AtLeast(
				versionFromExactVersionSpec(lower),
			).Intersection(
				OlderThan(versionFromExactVersionSpec(upper)))
		default:
			panic(fmt.Errorf("unsupported constraints.SelectionOp %s", ts.Operator))
		}

	case constraints.UnionSpec:
		if len(ts) == 0 {
			return All
		}
		if len(ts) == 1 {
			return MeetingConstraintsExact(ts[0])
		}
		union := make(setUnion, len(ts))
		for i, subSpec := range ts {
			union[i] = MeetingConstraintsExact(subSpec).setI
		}
		return Set{setI: union}

	case constraints.IntersectionSpec:
		if len(ts) == 0 {
			return All
		}
		if len(ts) == 1 {
			return MeetingConstraintsExact(ts[0])
		}
		intersection := make(setIntersection, len(ts))
		for i, subSpec := range ts {
			intersection[i] = MeetingConstraintsExact(subSpec).setI
		}
		return Set{setI: intersection}

	default:
		// should never happen because the above cases are exhaustive for
		// all valid constraint implementations.
		panic(fmt.Errorf("unsupported constraints.Spec implementation %T", spec))
	}
}

// MeetingConstraintsString attempts to parse the given spec as a constraints
// string in our canonical format, which is most similar to the syntax used by
// npm, Go's "dep" tool, Rust's "cargo", etc.
//
// This is a covenience wrapper around calling constraints.Parse and then
// passing the result to MeetingConstraints. Call into the constraints package
// yourself for access to the constraint tree.
//
// If unsuccessful, the error from the underlying parser is returned verbatim.
// Parser errors are suitable for showing to an end-user in situations where
// the given spec came from user input.
func MeetingConstraintsString(spec string) (Set, error) {
	s, err := constraints.Parse(spec)
	if err != nil {
		return None, err
	}
	return MeetingConstraints(s), nil
}

// MeetingConstraintsStringRuby attempts to parse the given spec as a
// "Ruby-style" version constraint string, and returns the set of versions
// that match the constraint if successful.
//
// If unsuccessful, the error from the underlying parser is returned verbatim.
// Parser errors are suitable for showing to an end-user in situations where
// the given spec came from user input.
//
// "Ruby-style" here is not a promise of exact compatibility with rubygems
// or any other Ruby tools. Rather, it refers to this parser using a syntax
// that is intended to feel familiar to those who are familiar with rubygems
// syntax.
//
// Constraints are parsed in "multi" mode, allowing multiple comma-separated
// constraints that are combined with the Intersection operator. For more
// control over the parsing process, use the constraints package API directly
// and then call MeetingConstraints.
func MeetingConstraintsStringRuby(spec string) (Set, error) {
	s, err := constraints.ParseRubyStyleMulti(spec)
	if err != nil {
		return None, err
	}
	return MeetingConstraints(s), nil
}

// MustMakeSet can be used to wrap any function that returns a set and an error
// to make it panic if an error occurs and return the set otherwise.
//
// This is intended for tests and other situations where input is from
// known-good constants.
func MustMakeSet(set Set, err error) Set {
	if err != nil {
		panic(err)
	}
	return set
}

func versionFromExactVersionSpec(spec constraints.VersionSpec) Version {
	return Version{
		Major:      spec.Major.Num,
		Minor:      spec.Minor.Num,
		Patch:      spec.Patch.Num,
		Prerelease: VersionExtra(spec.Prerelease),
		Metadata:   VersionExtra(spec.Metadata),
	}
}
