// Copyright GoFrame gf Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.

package cmd

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"regexp"
	"runtime"
	"strings"

	"github.com/gogf/gf/v2/encoding/gbase64"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gbuild"
	"github.com/gogf/gf/v2/os/gcmd"
	"github.com/gogf/gf/v2/os/genv"
	"github.com/gogf/gf/v2/os/gfile"
	"github.com/gogf/gf/v2/os/gproc"
	"github.com/gogf/gf/v2/os/gtime"
	"github.com/gogf/gf/v2/text/gregex"
	"github.com/gogf/gf/v2/text/gstr"
	"github.com/gogf/gf/v2/util/gtag"

	"github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog"
)

var (
	Build = cBuild{
		nodeNameInConfigFile: "gfcli.build",
		packedGoFileName:     "internal/packed/build_pack_data.go",
	}
)

type cBuild struct {
	g.Meta               `name:"build" brief:"{cBuildBrief}" dc:"{cBuildDc}" eg:"{cBuildEg}" ad:"{cBuildAd}"`
	nodeNameInConfigFile string // nodeNameInConfigFile is the node name for compiler configurations in configuration file.
	packedGoFileName     string // packedGoFileName specifies the file name for packing common folders into one single go file.
}

const (
	cBuildDefaultFile = "main.go"
	cBuildBrief       = `cross-building go project for lots of platforms`
	cBuildEg          = `
gf build main.go
gf build main.go --ps public,template
gf build main.go --cgo
gf build main.go -m none 
gf build main.go -n my-app -a all -s all
gf build main.go -n my-app -a amd64,386 -s linux -p .
gf build main.go -n my-app -v 1.0 -a amd64,386 -s linux,windows,darwin -p ./docker/bin
`
	cBuildDc = `
The "build" command is most commonly used command, which is designed as a powerful wrapper for 
"go build" command for convenience cross-compiling usage. 
It provides much more features for building binary:
1. Cross-Compiling for many platforms and architectures.
2. Configuration file support for compiling.
3. Build-In Variables.
`
	cBuildAd = `
PLATFORMS
    darwin    amd64,arm64
    freebsd   386,amd64,arm
    linux     386,amd64,arm,arm64,ppc64,ppc64le,mips,mipsle,mips64,mips64le
    netbsd    386,amd64,arm
    openbsd   386,amd64,arm
    windows   386,amd64
`
	// https://golang.google.cn/doc/install/source
	cBuildPlatforms = `
darwin    amd64
darwin    arm64
ios       amd64
ios       arm64
freebsd   386
freebsd   amd64
freebsd   arm
linux     386
linux     amd64
linux     arm
linux     arm64
linux     ppc64
linux     ppc64le
linux     mips
linux     mipsle
linux     mips64
linux     mips64le
netbsd    386
netbsd    amd64
netbsd    arm
openbsd   386
openbsd   amd64
openbsd   arm
windows   386
windows   amd64
android   arm
dragonfly amd64
plan9     386
plan9     amd64
solaris   amd64
`
)

func init() {
	gtag.Sets(g.MapStrStr{
		`cBuildBrief`: cBuildBrief,
		`cBuildDc`:    cBuildDc,
		`cBuildEg`:    cBuildEg,
		`cBuildAd`:    cBuildAd,
	})
}

type cBuildInput struct {
	g.Meta        `name:"build" config:"gfcli.build"`
	File          string `name:"FILE" arg:"true"    brief:"building file path"`
	Name          string `short:"n"  name:"name"    brief:"output binary name"`
	Version       string `short:"v"  name:"version" brief:"output binary version"`
	Arch          string `short:"a"  name:"arch"    brief:"output binary architecture, multiple arch separated with ','"`
	System        string `short:"s"  name:"system"  brief:"output binary system, multiple os separated with ','"`
	Output        string `short:"o"  name:"output"  brief:"output binary path, used when building single binary file"`
	Path          string `short:"p"  name:"path"    brief:"output binary directory path, default is '.'" d:"."`
	Extra         string `short:"e"  name:"extra"   brief:"extra custom \"go build\" options"`
	Mod           string `short:"m"  name:"mod"     brief:"like \"-mod\" option of \"go build\", use \"-m none\" to disable go module"`
	Cgo           bool   `short:"c"  name:"cgo"     brief:"enable or disable cgo feature, it's disabled in default" orphan:"true"`
	VarMap        g.Map  `short:"r"  name:"varMap"  brief:"custom built embedded variable into binary"`
	PackSrc       string `short:"ps" name:"packSrc" brief:"pack one or more folders into one go file before building"`
	PackDst       string `short:"pd" name:"packDst" brief:"temporary go file path for pack, this go file will be automatically removed after built" d:"internal/packed/build_pack_data.go"`
	ExitWhenError bool   `short:"ew" name:"exitWhenError" brief:"exit building when any error occurs, specially for multiple arch and system buildings. default is false" orphan:"true"`
	DumpENV       bool   `short:"de" name:"dumpEnv" brief:"dump current go build environment before building binary" orphan:"true"`
}

type cBuildOutput struct{}

func (c cBuild) Index(ctx context.Context, in cBuildInput) (out *cBuildOutput, err error) {
	// print used go env
	if in.DumpENV {
		_, _ = Env.Index(ctx, cEnvInput{})
	}

	mlog.SetHeaderPrint(true)

	mlog.Debugf(`build command input: %+v`, in)
	// Necessary check.
	if gproc.SearchBinary("go") == "" {
		mlog.Fatalf(`command "go" not found in your environment, please install golang first to proceed this command`)
	}

	var (
		parser = gcmd.ParserFromCtx(ctx)
		file   = in.File
	)
	if file == "" {
		file = parser.GetArg(2).String()
		// Check and use the main.go file.
		if gfile.Exists(cBuildDefaultFile) {
			file = cBuildDefaultFile
		} else {
			mlog.Fatal("build file path is empty or main.go not found in current working directory")
		}
	}
	if in.Name == "" {
		in.Name = gfile.Name(file)
	}
	if len(in.Name) < 1 || in.Name == "*" {
		mlog.Fatal("name cannot be empty")
	}
	if in.Mod != "" && in.Mod != "none" {
		mlog.Debugf(`mod is %s`, in.Mod)
		if in.Extra == "" {
			in.Extra = fmt.Sprintf(`-mod=%s`, in.Mod)
		} else {
			in.Extra = fmt.Sprintf(`-mod=%s %s`, in.Mod, in.Extra)
		}
	}
	if in.Extra != "" {
		in.Extra += " "
	}
	var (
		customSystems = gstr.SplitAndTrim(in.System, ",")
		customArches  = gstr.SplitAndTrim(in.Arch, ",")
	)
	if len(in.Version) > 0 {
		in.Path += "/" + in.Version
	}
	// System and arch checks.
	var (
		spaceRegex  = regexp.MustCompile(`\s+`)
		platformMap = make(map[string]map[string]bool)
	)
	for _, line := range strings.Split(strings.TrimSpace(cBuildPlatforms), "\n") {
		line = gstr.Trim(line)
		line = spaceRegex.ReplaceAllString(line, " ")
		var (
			array  = strings.Split(line, " ")
			system = strings.TrimSpace(array[0])
			arch   = strings.TrimSpace(array[1])
		)
		if platformMap[system] == nil {
			platformMap[system] = make(map[string]bool)
		}
		platformMap[system][arch] = true
	}
	// Auto packing.
	if in.PackSrc != "" {
		if in.PackDst == "" {
			mlog.Fatal(`parameter "packDst" should not be empty when "packSrc" is used`)
		}
		if gfile.Exists(in.PackDst) && !gfile.IsFile(in.PackDst) {
			mlog.Fatalf(`parameter "packDst" path "%s" should be type of file not directory`, in.PackDst)
		}
		if !gfile.Exists(in.PackDst) {
			// Remove the go file that is automatically packed resource.
			defer func() {
				_ = gfile.Remove(in.PackDst)
				mlog.Printf(`remove the automatically generated resource go file: %s`, in.PackDst)
			}()
		}
		// remove black space in separator.
		in.PackSrc, _ = gregex.ReplaceString(`,\s+`, `,`, in.PackSrc)
		packCmd := fmt.Sprintf(`gf pack %s %s --keepPath=true`, in.PackSrc, in.PackDst)
		mlog.Print(packCmd)
		gproc.MustShellRun(ctx, packCmd)
	}

	// Injected information by building flags.
	ldFlags := fmt.Sprintf(
		`-X 'github.com/gogf/gf/v2/os/gbuild.builtInVarStr=%v'`,
		c.getBuildInVarStr(ctx, in),
	)

	// start building
	mlog.Print("start building...")
	if in.Cgo {
		genv.MustSet("CGO_ENABLED", "1")
	} else {
		genv.MustSet("CGO_ENABLED", "0")
	}
	for system, item := range platformMap {
		if len(customSystems) > 0 && customSystems[0] != "all" && !gstr.InArray(customSystems, system) {
			continue
		}
		for arch := range item {
			if len(customArches) > 0 && customArches[0] != "all" && !gstr.InArray(customArches, arch) {
				continue
			}
			if len(customSystems) == 0 && len(customArches) == 0 {
				// Single binary building, output the binary to current working folder.
				// For example:
				// `gf build`
				// `gf build -o main.exe`
				c.doBinaryBuild(
					ctx, file,
					in.Output, in.Path,
					runtime.GOOS, runtime.GOARCH, in.Name, ldFlags, in.Extra,
					in.ExitWhenError,
					true,
				)
			} else {
				c.doBinaryBuild(
					ctx, file,
					in.Output, in.Path,
					system, arch, in.Name, ldFlags, in.Extra,
					in.ExitWhenError,
					false,
				)
			}
			// single binary building.
			if len(customSystems) == 0 && len(customArches) == 0 {
				goto buildDone
			}
		}
	}

buildDone:
	mlog.Print("done!")
	return
}

func (c cBuild) doBinaryBuild(
	ctx context.Context,
	filePath string,
	outputPath, dirPath string,
	system, arch, name, ldFlags, extra string,
	exitWhenError bool,
	singleBuild bool,
) {
	var (
		cmd string
		ext string
	)
	// Cross-building, output the compiled binary to specified path.
	if system == "windows" {
		ext = ".exe"
	}
	genv.MustSet("GOOS", system)
	genv.MustSet("GOARCH", arch)

	if outputPath != "" {
		outputPath = "-o " + outputPath
	} else {
		if dirPath == "" {
			dirPath = "."
		} else {
			dirPath = gstr.TrimRight(dirPath, "/")
		}
		if singleBuild {
			outputPath = fmt.Sprintf(
				"-o %s/%s%s",
				dirPath, name, ext,
			)
		} else {
			outputPath = fmt.Sprintf(
				"-o %s/%s/%s%s",
				dirPath, system+"_"+arch, name, ext,
			)
		}
	}
	cmd = fmt.Sprintf(
		`go build %s -ldflags "%s" %s%s`,
		outputPath, ldFlags, extra, filePath,
	)
	mlog.Debug(fmt.Sprintf("build for GOOS=%s GOARCH=%s", system, arch))
	mlog.Debug(cmd)
	// It's not necessary printing the complete command string, filtering ldFlags.
	cmdShow, _ := gregex.ReplaceString(`\s+(-ldflags ".+?")\s+`, " ", cmd)
	mlog.Print(cmdShow)
	if result, err := gproc.ShellExec(ctx, cmd); err != nil {
		mlog.Printf(
			"failed to build, os:%s, arch:%s, error:\n%s\n\n%s\n",
			system, arch, gstr.Trim(result),
			`you may use command option "--debug" to enable debug info and check the details`,
		)
		if exitWhenError {
			os.Exit(1)
		}
	} else {
		mlog.Debug(gstr.Trim(result))
	}
}

// getBuildInVarStr retrieves and returns the custom build-in variables in configuration
// file as json.
func (c cBuild) getBuildInVarStr(ctx context.Context, in cBuildInput) string {
	buildInVarMap := in.VarMap
	if buildInVarMap == nil {
		buildInVarMap = make(g.Map)
	}
	buildInVarMap[gbuild.BuiltGit] = c.getGitCommit(ctx)
	buildInVarMap[gbuild.BuiltTime] = gtime.Now().String()
	buildInVarMap[gbuild.BuiltVersion] = in.Version
	b, err := json.Marshal(buildInVarMap)
	if err != nil {
		mlog.Fatal(err)
	}
	return gbase64.EncodeToString(b)
}

// getGitCommit retrieves and returns the latest git commit hash string if present.
func (c cBuild) getGitCommit(ctx context.Context) string {
	if gproc.SearchBinary("git") == "" {
		return ""
	}
	var (
		cmd  = `git log -1 --format="%cd %H" --date=format:"%Y-%m-%d %H:%M:%S"`
		s, _ = gproc.ShellExec(ctx, cmd)
	)
	mlog.Debug(cmd)
	if s != "" {
		if !gstr.Contains(s, "fatal") {
			return gstr.Trim(s)
		}
	}
	return ""
}
