package version

import (
	"fmt"
	"runtime/debug"
	"strconv"
	"strings"
	"time"
)

var (
	// The versionString is provided through ldflags while building. This
	// variable should only be set through ldflags!
	// Defaults to `0.0.0`.
	versionString = "0.0.0"
)

// BuildInformation contains additional information about the build. The information
// is taken from `debug.ReadBuildInfo`.
type BuildInformation struct {
	// goos describes the os the binary is built for.
	goos string
	// goarch describes the architecture the binary is built for.
	goarch string
	// revision is a reference to the last git commit the binary is based on.
	revision string
	// time is the timestamp of the last git commit the binary is based on.
	time time.Time
	// modified returns true if the build is modified and contains additional
	// changes from the last commit - a so called dirty build.
	modified bool
}

// Version describes the current version of the goSDN controller and contains
// additional build information.
type Version struct {
	major int
	minor int
	patch int

	BuildInformation
}

// NewVersion creates a new Version from the versionString, provided via
// ldflags while running `go build`. If no version information is provided
// through out the build process a default value is used.
//
// Additional build information is gathered with the help of
// `debug.ReadBuildInfo` and is added to the Version.
func NewVersion() (*Version, error) {
	buildInfo, ok := debug.ReadBuildInfo()
	if !ok {
		return nil, fmt.Errorf("Failed reading the binaries build information")
	}

	parsedVersion, err := parseVersionString(versionString)
	if err != nil {
		return nil, fmt.Errorf("Could not parse version string: %w", err)
	}

	version := &Version{
		major: parsedVersion[0],
		minor: parsedVersion[1],
		patch: parsedVersion[2],
	}

	for _, bSetting := range buildInfo.Settings {
		switch bSetting.Key {
		case "GOOS":
			version.goos = bSetting.Value
		case "GOARCH":
			version.goarch = bSetting.Value
		case "vcs.revision":
			version.revision = bSetting.Value
		case "vcs.time":
			parsedTime, _ := time.Parse(time.RFC3339, bSetting.Value)
			version.time = parsedTime
		case "vcs.modified":
			version.modified = bSetting.Value == "true"
		}
	}

	return version, nil
}

func (v *Version) String() string {
	return fmt.Sprintf(
		`Version: %d.%d.%d
Build Information:
    - GOOS: %s
    - GOARCH: %s
    - Revision: %s
    - Time: %s
    - Modified (dirty): %t`,
		v.major, v.minor, v.patch, v.goos, v.goarch, v.revision, v.time.String(), v.modified,
	)
}

// Compare compares the provided version with the current version. Some of the
// build information is included in the comparison. This means that revision,
// time and the modified values are compared aswell.
//
// Returns 0, -1, or 1 based on the fact if the version is equal, smaller or
// larger than the incoming version.
// If one of the versions is modified then a comparison is not possible, since
// it cannot be said with certainty that the two versions are the same. In this
// case a -2 is returned.
func (v *Version) Compare(toCompare *Version) int {
	if v.modified || toCompare.modified {
		return -2
	}

	// compare the major segment.
	if c := compareVersionSegment(v.major, toCompare.major); c != 0 {
		return c
	}
	// compare the minor segment.
	if c := compareVersionSegment(v.minor, toCompare.minor); c != 0 {
		return c
	}
	// compare the patch segment.
	if c := compareVersionSegment(v.patch, toCompare.patch); c != 0 {
		return c
	}

	// basic version is the same; now the commit information is compared.
	if v.revision != toCompare.revision {
		if v.time.Before(toCompare.time) {
			return -1
		}
		return 1
	}

	return 0
}

// compareVersionSegment is a helper function that allows to compare version
// segments like, e.g. major, minor and patch between two versions.
//
// Left describes the segment of the version that should be compared against
// and right describes the incoming version.
//
// The function returns 0, -1, or 1 based on the fact if the version segment
// to be compared against is equal, smaller or larger than the incoming version
// segment.
func compareVersionSegment(left, right int) int {
	if left < right {
		return -1
	}

	if left > right {
		return 1
	}

	return 0
}

// parseVersionString is a helper function that parses the given string into
// three integers: major, minor and patch. The integers are returned as an
// array of integers in that respective order.
func parseVersionString(s string) ([]int, error) {
	versionSegments := strings.SplitN(s, ".", 3)

	res := make([]int, 3)
	for i, versionSegment := range versionSegments {
		versionSegmentAsInt, err := strconv.ParseInt(versionSegment, 10, 64)
		if err != nil {
			return nil, err
		}
		res[i] = int(versionSegmentAsInt)
	}
	return res, nil
}
