// 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 service

import (
	"context"
	"runtime"
	"strings"

	"github.com/gogf/gf/v2/container/garray"
	"github.com/gogf/gf/v2/container/gset"
	"github.com/gogf/gf/v2/frame/g"
	"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/text/gstr"
	"github.com/gogf/gf/v2/util/gconv"

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

var (
	Install = serviceInstall{}
)

type serviceInstall struct{}

type serviceInstallAvailablePath struct {
	dirPath   string
	filePath  string
	writable  bool
	installed bool
	IsSelf    bool
}

func (s serviceInstall) Run(ctx context.Context) (err error) {
	// Ask where to install.
	paths := s.getAvailablePaths()
	if len(paths) <= 0 {
		mlog.Printf("no path detected, you can manually install gf by copying the binary to path folder.")
		return
	}
	mlog.Printf("I found some installable paths for you(from $PATH): ")
	mlog.Printf("  %2s | %8s | %9s | %s", "Id", "Writable", "Installed", "Path")

	// Print all paths status and determine the default selectedID value.
	var (
		selectedID = -1
		newPaths   []serviceInstallAvailablePath
		pathSet    = gset.NewStrSet() // Used for repeated items filtering.
	)
	for _, path := range paths {
		if !pathSet.AddIfNotExist(path.dirPath) {
			continue
		}
		newPaths = append(newPaths, path)
	}
	paths = newPaths
	for id, path := range paths {
		mlog.Printf("  %2d | %8t | %9t | %s", id, path.writable, path.installed, path.dirPath)
		if selectedID == -1 {
			// Use the previously installed path as the most priority choice.
			if path.installed {
				selectedID = id
			}
		}
	}
	// If there's no previously installed path, use the first writable path.
	if selectedID == -1 {
		// Order by choosing priority.
		commonPaths := garray.NewStrArrayFrom(g.SliceStr{
			s.getGoPathBin(),
			`/usr/local/bin`,
			`/usr/bin`,
			`/usr/sbin`,
			`C:\Windows`,
			`C:\Windows\system32`,
			`C:\Go\bin`,
			`C:\Program Files`,
			`C:\Program Files (x86)`,
		})
		// Check the common installation directories.
		commonPaths.Iterator(func(k int, v string) bool {
			for id, aPath := range paths {
				if strings.EqualFold(aPath.dirPath, v) {
					selectedID = id
					return false
				}
			}
			return true
		})
		if selectedID == -1 {
			selectedID = 0
		}
	}

	if allyes.Check() {
		// Use the default selectedID.
		mlog.Printf("please choose one installation destination [default %d]: %d", selectedID, selectedID)
	} else {
		for {
			// Get input and update selectedID.
			var (
				inputID int
				input   = gcmd.Scanf("please choose one installation destination [default %d]: ", selectedID)
			)
			if input != "" {
				inputID = gconv.Int(input)
			} else {
				break
			}
			// Check if out of range.
			if inputID >= len(paths) || inputID < 0 {
				mlog.Printf("invalid install destination Id: %d", inputID)
				continue
			}
			selectedID = inputID
			break
		}
	}

	// Get selected destination path.
	dstPath := paths[selectedID]

	// Install the new binary.
	mlog.Debugf(`copy file from "%s" to "%s"`, gfile.SelfPath(), dstPath.filePath)
	err = gfile.CopyFile(gfile.SelfPath(), dstPath.filePath)
	if err != nil {
		mlog.Printf("install gf binary to '%s' failed: %v", dstPath.dirPath, err)
		mlog.Printf("you can manually install gf by copying the binary to folder: %s", dstPath.dirPath)
	} else {
		mlog.Printf("gf binary is successfully installed to: %s", dstPath.filePath)
	}
	return
}

// IsInstalled checks and returns whether the binary is installed.
func (s serviceInstall) IsInstalled() (*serviceInstallAvailablePath, bool) {
	paths := s.getAvailablePaths()
	for _, aPath := range paths {
		if aPath.installed {
			return &aPath, true
		}
	}
	return nil, false
}

// getGoPathBin retrieves ad returns the GOPATH/bin path for binary.
func (s serviceInstall) getGoPathBin() string {
	if goPath := genv.Get(`GOPATH`).String(); goPath != "" {
		return gfile.Join(goPath, "bin")
	}
	return ""
}

// getAvailablePaths returns the installation paths data for the binary.
func (s serviceInstall) getAvailablePaths() []serviceInstallAvailablePath {
	var (
		folderPaths    []serviceInstallAvailablePath
		binaryFileName = "gf" + gfile.Ext(gfile.SelfPath())
	)
	// $GOPATH/bin
	if goPathBin := s.getGoPathBin(); goPathBin != "" {
		folderPaths = s.checkAndAppendToAvailablePath(
			folderPaths, goPathBin, binaryFileName,
		)
	}
	switch runtime.GOOS {
	case "darwin":
		darwinInstallationCheckPaths := []string{"/usr/local/bin"}
		for _, v := range darwinInstallationCheckPaths {
			folderPaths = s.checkAndAppendToAvailablePath(
				folderPaths, v, binaryFileName,
			)
		}
		fallthrough

	default:
		// Search and find the writable directory path.
		envPath := genv.Get("PATH", genv.Get("Path").String()).String()
		if gstr.Contains(envPath, ";") {
			// windows.
			for _, v := range gstr.SplitAndTrim(envPath, ";") {
				if v == "." {
					continue
				}
				folderPaths = s.checkAndAppendToAvailablePath(
					folderPaths, v, binaryFileName,
				)
			}
		} else if gstr.Contains(envPath, ":") {
			// *nix.
			for _, v := range gstr.SplitAndTrim(envPath, ":") {
				if v == "." {
					continue
				}
				folderPaths = s.checkAndAppendToAvailablePath(
					folderPaths, v, binaryFileName,
				)
			}
		} else if envPath != "" {
			folderPaths = s.checkAndAppendToAvailablePath(
				folderPaths, envPath, binaryFileName,
			)
		} else {
			folderPaths = s.checkAndAppendToAvailablePath(
				folderPaths, "/usr/local/bin", binaryFileName,
			)
		}
	}
	return folderPaths
}

// checkAndAppendToAvailablePath checks if `path` is writable and already installed.
// It adds the `path` to `folderPaths` if it is writable or already installed, or else it ignores the `path`.
func (s serviceInstall) checkAndAppendToAvailablePath(folderPaths []serviceInstallAvailablePath, dirPath string, binaryFileName string) []serviceInstallAvailablePath {
	var (
		filePath  = gfile.Join(dirPath, binaryFileName)
		writable  = gfile.IsWritable(dirPath)
		installed = gfile.Exists(filePath)
		self      = gfile.SelfPath() == filePath
	)
	if !writable && !installed {
		return folderPaths
	}
	return append(
		folderPaths,
		serviceInstallAvailablePath{
			dirPath:   dirPath,
			writable:  writable,
			filePath:  filePath,
			installed: installed,
			IsSelf:    self,
		})
}
