/*
 * BSD 3-Clause License
 * Copyright (c) 2024, NetFireâ„¢ <https://netfire.com/>
 *
 * Redistribution and use in source and binary forms, with or without modification,
 *  are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation
 *  and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors
 *  may be used to endorse or promote products derived from this software without
 *  specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 *  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 *  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 *  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package version

import (
	`fmt`
	`runtime`
	`strconv`
	`strings`
	`time`

	`golang.org/x/mod/semver`
)

// Version returns the build information. See build.sh.
func Version() (b *BuildInfo, err error) {

	var n int
	var s string
	var sb strings.Builder
	var ok bool
	var canonical string
	var build strings.Builder
	// Why a map?
	// I forget but I had a reason for it once upon a time.
	// If you remove it and this func breaks, now you know why.
	// TODO: how much of this can be replaced by (runtime/debug).ReadBuildInfo()?
	var raw map[string]string = map[string]string{
		"repoUri":        repoUri,
		"sourceControl":  sourceControl,
		"tag":            version,
		"hash":           commitHash,
		"shortHash":      commitShort,
		"postTagCommits": numCommitsAfterTag,
		"dirty":          isDirty,
		"time":           buildTime,
		"user":           buildUser,
		"sudoUser":       buildSudoUser,
		"host":           buildHost,
	}
	var i BuildInfo = BuildInfo{
		SourceControl: raw["sourceControl"],
		GoVersion:     runtime.Version(),
		RepoURI:       raw["repoUri"],
		TagVersion:    raw["tag"],
		// PostTagCommits: 0,
		CommitHash:    raw["hash"],
		CommitId:      raw["shortHash"],
		BuildUser:     raw["user"],
		RealBuildUser: raw["sudoUser"],
		// BuildTime:     time.Time{},
		BuildHost: raw["host"],
		Dirty:     false,
		isDefined: false,
		raw:       raw,
	}

	if s, ok = raw["postTagCommits"]; ok && strings.TrimSpace(s) != "" {
		if n, err = strconv.Atoi(s); err == nil {
			i.PostTagCommits = uint(n)
		}
	}

	if s, ok = raw["time"]; ok && strings.TrimSpace(s) != "" {
		if n, err = strconv.Atoi(s); err == nil {
			i.BuildTime = time.Unix(int64(n), 0).UTC()
		}
	}

	switch strings.ToLower(raw["dirty"]) {
	case "1":
		i.Dirty = true
	case "0", "":
		i.Dirty = false
	}

	// Build the short form. We use this for both BuildInfo.short and BuildInfo.verSplit.
	if i.TagVersion == "" {
		sb.WriteString(i.SourceControl)
	} else {
		sb.WriteString(i.TagVersion)
	}
	/*
		Now the mess. In order to conform to SemVer 2.0 (the spec this code targets):

		1.) MAJOR.
		2.) MINOR.
		3.) PATCH
		4.) -PRERELEASE (OPTIONAL)
			(git commit, if building against a commit made past 1-3. Always included if untagged.)
		5.) +BUILDINFO (OPTIONAL)
			("+x[.y]", where x is # of commits past 4, or tag commit if 4 is empty. 0 is valid.
			 y is optional, and is the string "dirty" if it is a "dirty" build - that is, uncommitted/unstaged changes.
			 if x and y would be 0 and empty, respectively, then 5 is not included.)

		1-3 are already written, or the source control software used if not.

		Technically 4 and 5 are only included if 3 is present. We force patch to 0 if it's a tagged release and patch isn't present --
		so this is not relevant.
	*/
	// PRERELEASE
	if i.TagVersion == "" || i.PostTagCommits > 0 {
		// We use the full commit hash for git versions, short identifier for tagged releases.
		if i.TagVersion == "" {
			i.Pre = i.CommitHash
		} else {
			i.Pre = i.CommitId
		}
		sb.WriteString(fmt.Sprintf("-%v", i.Pre))
	}
	// BUILD
	if i.PostTagCommits > 0 || i.Dirty {
		build.WriteString(strconv.Itoa(int(i.PostTagCommits)))
		if i.Dirty {
			build.WriteString(".dirty")
		}
		i.Build = build.String()
		sb.WriteString(fmt.Sprintf("+%v", i.Build))
	}

	i.short = sb.String()
	if semver.IsValid(i.short) {
		// DON'T DO THIS. It strips the prerelease and build info.
		// i.short = semver.Canonical(i.short)
		// Do this instead.
		canonical = semver.Canonical(i.short)
		// Numeric versions...
		if n, err = strconv.Atoi(strings.TrimPrefix(semver.Major(canonical), "v")); err != nil {
			err = nil
		} else {
			i.Major = uint(n)
		}
		if n, err = strconv.Atoi(strings.Split(semver.MajorMinor(canonical), ".")[1]); err != nil {
			err = nil
		} else {
			i.Minor = uint(n)
		}
		if n, err = strconv.Atoi(patchReIsolated.FindStringSubmatch(strings.Split(canonical, ".")[2])[1]); err != nil {
			err = nil
		} else {
			i.Patch = uint(n)
		}
		// The other tag assignments were performed above.
	}
	// The default is 0 for the numerics, so no big deal.

	i.isDefined = true

	b = &i

	return
}