func readPubSpecLock() (*PubSpecLock, error) {
	p := &PubSpecLock{}
	file, err := os.Open("pubspec.lock")
	if err != nil {
		if os.IsNotExist(err) {
			return nil, errors.New("no pubspec.lock file found")
		}
		return nil, errors.Wrap(err, "failed to open pubspec.lock")
	}
	defer file.Close()
	err = yaml.NewDecoder(file).Decode(p)
	if err != nil {
		return nil, errors.Wrap(err, "failed to decode pubspec.lock")
	}
	return p, nil
}
readLocal reads pubspec.lock in the current working directory.
package cmd
import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strings"
	"time"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
	"golang.org/x/mod/modfile"
	"gopkg.in/yaml.v2"
	"github.com/go-flutter-desktop/hover/internal/build"
	"github.com/go-flutter-desktop/hover/internal/fileutils"
	"github.com/go-flutter-desktop/hover/internal/log"
	"github.com/go-flutter-desktop/hover/internal/modx"
	"github.com/go-flutter-desktop/hover/internal/pubspec"
)
const standaloneImplementationListAPI = "https://raw.githubusercontent.com/go-flutter-desktop/plugins/master/list.json"
var (
	listAllPluginDependencies bool
	tidyPurge                 bool
	dryRun                    bool
	reImport                  bool
)
func init() {
	pluginTidyCmd.Flags().BoolVar(&tidyPurge, "purge", false, "Remove all go platform plugins imports from the project.")
	pluginListCmd.Flags().BoolVarP(&listAllPluginDependencies, "all", "a", false, "List all platform plugins dependencies, even the one have no go-flutter support")
	pluginGetCmd.Flags().BoolVar(&reImport, "force", false, "Re-import already imported plugins.")
	pluginCmd.PersistentFlags().BoolVarP(&dryRun, "dry-run", "n", false, "Perform a trial run with no changes made.")
	pluginCmd.AddCommand(pluginListCmd)
	pluginCmd.AddCommand(pluginGetCmd)
	pluginCmd.AddCommand(pluginTidyCmd)
	rootCmd.AddCommand(pluginCmd)
}
var pluginCmd = &cobra.Command{
	Use:   "plugins",
	Short: "Tools for plugins",
	Long:  "A collection of commands to help with finding/importing go-flutter implementations of plugins.",
}
type PubSpecLock struct {
	Packages map[string]PubDep
}
type PubDep struct {
	Dependency  string
	Description interface{}
	Source      string
	Version     string
	name    string
	android bool
	ios     bool
	desktop bool
	path string  
	host string  
	autoImport bool
	pluginGoSource string
	standaloneImpl bool
}
func (p PubDep) imported() bool {
	pluginImportOutPath := filepath.Join(build.BuildPath, "cmd", fmt.Sprintf("import-%s-plugin.go", p.name))
	if _, err := os.Stat(pluginImportOutPath); err == nil {
		return true
	}
	return false
}
func (p PubDep) platforms() []string {
	var platforms []string
	if p.android {
		platforms = append(platforms, "android")
	}
	if p.ios {
		platforms = append(platforms, "ios")
	}
	if p.desktop {
		platforms = append(platforms, build.BuildPath)
	}
	return platforms
}
var pluginListCmd = &cobra.Command{
	Use:   "list",
	Short: "List golang platform plugins in the application",
	Args: func(cmd *cobra.Command, args []string) error {
		if len(args) > 0 {
			return errors.New("does not take arguments")
		}
		return nil
	},
	Run: func(cmd *cobra.Command, args []string) {
		assertInFlutterProject()
		dependencyList, err := listPlatformPlugin()
		if err != nil {
			log.Errorf("%v", err)
			os.Exit(1)
		}
		var hasNewPlugin bool
		var hasPlugins bool
		for _, dep := range dependencyList {
			if !(dep.desktop || listAllPluginDependencies) {
				continue
			}
			if hasPlugins {
				fmt.Println("")
			}
			hasPlugins = true
			log.Infof("     - %s", dep.name)
			log.Infof("         version:   %s", dep.Version)
			log.Infof("         platforms: [%s]", strings.Join(dep.platforms(), ", "))
			if dep.desktop {
				if dep.standaloneImpl {
					log.Infof("         source:    This go plugin isn't maintained by the official plugin creator.")
				}
				if dep.imported() {
					log.Infof("         import:    [OK] The plugin is already imported in the project.")
					continue
				}
				if dep.autoImport || dep.standaloneImpl {
					hasNewPlugin = true
					log.Infof("         import:    [Missing] The plugin can be imported by hover.")
				} else {
					log.Infof("         import:    [Manual import] The plugin is missing the import.go.tmpl file required for hover import.")
				}
				if dep.path != "" {
					log.Infof("         dev:       Plugin replaced in go.mod to path: '%s'", dep.path)
				}
			}
		}
		if hasNewPlugin {
			log.Infof(fmt.Sprintf("run `%s` to import the missing plugins!", log.Au().Magenta("hover plugins get")))
		}
	},
}
var pluginTidyCmd = &cobra.Command{
	Use:   "tidy",
	Short: "Removes unused platform plugins.",
	Args: func(cmd *cobra.Command, args []string) error {
		if len(args) > 0 {
			return errors.New("does not take arguments")
		}
		return nil
	},
	Run: func(cmd *cobra.Command, args []string) {
		var (
			err   error
			gomod *modfile.File
		)
		assertInFlutterProject()
		assertHoverInitialized()
		gomod, err = modx.Open(build.BuildPath)
		if err != nil {
			log.Errorf("failed to open go.mod", err)
			os.Exit(1)
		}
		desktopCmdPath := filepath.Join(build.BuildPath, "cmd")
		dependencyList, err := listPlatformPlugin()
		if err != nil {
			log.Errorf("%v", err)
			os.Exit(1)
		}
		importedPlugins, err := ioutil.ReadDir(desktopCmdPath)
		if err != nil {
			log.Errorf("Failed to search for plugins: %v", err)
			os.Exit(1)
		}
		for _, f := range importedPlugins {
			isPlugin := strings.HasPrefix(f.Name(), "import-")
			isPlugin = isPlugin && strings.HasSuffix(f.Name(), "-plugin.go")
			if isPlugin {
				pluginName := strings.TrimPrefix(f.Name(), "import-")
				pluginName = strings.TrimSuffix(pluginName, "-plugin.go")
				pluginImportPath := filepath.Join(desktopCmdPath, f.Name())
				pluginInUse := false
				for _, dep := range dependencyList {
					if dep.name == pluginName {
						pluginInUse = true
						break
					}
				}
				if !pluginInUse || tidyPurge {
					pluginImportStr, err := readPluginGoImport(pluginImportPath, pluginName)
					if err != nil || pluginImportStr == "" {
						log.Warnf("Couldn't clean the '%s' plugin from the 'go.mod' file. Error: %v", pluginName, err)
					} else {
						if err = modx.RemoveModule(gomod, pluginImportStr); err != nil {
							log.Warnf("failed remove %s from %s/go.mod: %v", pluginImportStr, build.BuildPath, err)
						}
					}
					if !dryRun {
						if err = os.Remove(pluginImportPath); err != nil {
							log.Warnf("Couldn't remove plugin %s: %v", pluginName, err)
							continue
						}
					}
					log.Infof("       plugin: [%s] removed", pluginName)
				}
			}
		}
		if dryRun {
			s, err := modx.Print(gomod)
			if err != nil {
				log.Errorf("failed to print updated go.mod: %v", err)
				os.Exit(1)
			}
			log.Infof("modified go.mod:\n%s", s)
		} else {
			err = modx.Replace(build.BuildPath, gomod)
			if err != nil {
				log.Errorf("failed to update go.mod: %v", err)
				os.Exit(1)
			}
		}
		if tidyPurge {
			intermediatesDirectoryPath, err := filepath.Abs(filepath.Join(build.BuildPath, "build", "intermediates"))
			if err != nil {
				log.Errorf("Failed to resolve absolute path for intermediates directory: %v", err)
				os.Exit(1)
			}
			if fileutils.IsDirectory(intermediatesDirectoryPath) {
				_ = os.RemoveAll(intermediatesDirectoryPath)
			}
		}
	},
}
var pluginGetCmd = &cobra.Command{
	Use:   "get",
	Short: "Imports missing platform plugins in the application",
	Args: func(cmd *cobra.Command, args []string) error {
		if len(args) > 0 {
			return errors.New("does not take arguments")
		}
		return nil
	},
	Run: func(cmd *cobra.Command, args []string) {
		assertInFlutterProject()
		assertHoverInitialized()
		hoverPluginGet(false)
	},
}
func hoverPluginGet(dryRun bool) bool {
	dependencyList, err := listPlatformPlugin()
	if err != nil {
		log.Errorf("%v", err)
		os.Exit(1)
	}
	for _, dep := range dependencyList {
		if !dep.desktop {
			continue
		}
		if !dep.autoImport {
			log.Infof("       plugin: [%s] couldn't be imported, check the plugin's README for manual instructions", dep.name)
			continue
		}
		if dryRun {
			if dep.imported() {
				log.Infof("       plugin: [%s] can be updated", dep.name)
			} else {
				log.Infof("       plugin: [%s] can be imported", dep.name)
			}
			continue
		}
		pluginImportOutPath := filepath.Join(build.BuildPath, "cmd", fmt.Sprintf("import-%s-plugin.go", dep.name))
		if dep.imported() && !reImport {
			pluginImportStr, err := readPluginGoImport(pluginImportOutPath, dep.name)
			if err != nil {
				log.Warnf("Couldn't read the plugin '%s' import URL", dep.name)
				log.Warnf("Fallback to the latest version installed.")
				continue
			}
			if !goGetModuleSuccess(pluginImportStr, dep.Version) {
				log.Warnf("Couldn't download version '%s' of plugin '%s'", dep.Version, dep.name)
				log.Warnf("Fallback to the latest version installed.")
				continue
			}
			log.Infof("       plugin: [%s] updated", dep.name)
			continue
		}
		if dep.standaloneImpl {
			fileutils.DownloadFile(dep.pluginGoSource, pluginImportOutPath)
		} else {
			autoImportTemplatePath := filepath.Join(dep.pluginGoSource, "import.go.tmpl")
			fileutils.CopyFile(autoImportTemplatePath, pluginImportOutPath)
			if fileutils.IsDirectory(filepath.Join(dep.pluginGoSource, "dlib")) {
				dlibPath, err := filepath.Abs(filepath.Join(dep.pluginGoSource, "dlib"))
				if err != nil {
					log.Errorf("Failed to resolve absolute path for dlib directory: %v", err)
					os.Exit(1)
				}
				intermediatesDirectoryPath, err := filepath.Abs(filepath.Join(build.BuildPath, "build", "intermediates"))
				if err != nil {
					log.Errorf("Failed to resolve absolute path for intermediates directory: %v", err)
					os.Exit(1)
				}
				fileutils.CopyDir(dlibPath, intermediatesDirectoryPath)
				if fileutils.IsFileExists(filepath.Join(dlibPath, "README.md")) {
					readmeName := fmt.Sprintf("README-%s.md", dep.name)
					fileutils.CopyFile(filepath.Join(dlibPath, "README.md"), filepath.Join(intermediatesDirectoryPath, readmeName))
					_ = os.Remove(filepath.Join(intermediatesDirectoryPath, "README.md"))
				}
			}
			pluginImportStr, err := readPluginGoImport(pluginImportOutPath, dep.name)
			if err != nil {
				log.Warnf("Couldn't read the plugin '%s' import URL", dep.name)
				log.Warnf("Fallback to the latest version available on github.")
				continue
			}
			if dep.path == "" {
				if !goGetModuleSuccess(pluginImportStr, dep.Version) {
					log.Warnf("Couldn't download version '%s' of plugin '%s'", dep.Version, dep.name)
					log.Warnf("Fallback to the latest version available on github.")
				}
			}
			if dep.path != "" {
				path, err := filepath.Abs(filepath.Join(dep.path, build.BuildPath))
				if err != nil {
					log.Errorf("Failed to resolve absolute path for plugin '%s': %v", dep.name, err)
					os.Exit(1)
				}
				mut := func(gomod *modfile.File) error {
					return gomod.AddReplace(pluginImportStr, "", path, "")
				}
				if err = modx.Mutate(build.BuildPath, mut); err != nil {
					log.Errorf("failed to update go.mod: %v", err)
					os.Exit(1)
				}
			}
			log.Infof("       plugin: [%s] imported", dep.name)
		}
	}
	return len(dependencyList) != 0
}
func listPlatformPlugin() ([]PubDep, error) {
	onlineList, err := fetchStandaloneImplementationList()
	if err != nil {
		log.Warnf("Warning, couldn't read the online plugin list: %v", err)
	}
	pubcachePath, err := findPubcachePath()
	if err != nil {
		return nil, errors.Wrap(err, "failed to find path for pub-cache")
	}
	var list []PubDep
	pubLock, err := readPubSpecLock()
	if err != nil {
		log.Infof("Run `%s` (or equivalent) first", log.Au().Magenta("flutter build bundle"))
		return nil, err
	}
	for name, entry := range pubLock.Packages {
		entry.name = name
		switch i := entry.Description.(type) {
		case string:
			if i == "flutter" {
				continue
			}
		case map[interface{}]interface{}:
			if value, ok := i["path"]; ok {
				entry.path = value.(string)
			}
			if value, ok := i["url"]; ok {
				url, err := url.Parse(value.(string))
				if err != nil {
					return nil, errors.Wrap(err, "failed to parse URL from string %s"+value.(string))
				}
				entry.host = url.Host
			}
		}
		pluginPath := filepath.Join(pubcachePath, "hosted", entry.host, entry.name+"-"+entry.Version)
		if entry.path != "" {
			pluginPath = entry.path
		}
		pluginPubspecPath := filepath.Join(pluginPath, "pubspec.yaml")
		pluginPubspec, err := pubspec.ReadPubSpecFile(pluginPubspecPath)
		if err != nil {
			continue
		}
		if _, ok := pluginPubspec.Flutter["plugin"]; !ok {
			continue
		}
		detectPlatformPlugin := func(platform string) (bool, error) {
			platformPath := filepath.Join(pluginPath, platform)
			stat, err := os.Stat(platformPath)
			if err != nil {
				if os.IsNotExist(err) {
					return false, nil
				}
				return false, errors.Wrapf(err, "failed to stat %s", platformPath)
			}
			return stat.IsDir(), nil
		}
		entry.android, err = detectPlatformPlugin("android")
		if err != nil {
			return nil, err
		}
		entry.ios, err = detectPlatformPlugin("ios")
		if err != nil {
			return nil, err
		}
		entry.desktop, err = detectPlatformPlugin(build.BuildPath)
		if err != nil {
			return nil, err
		}
		if entry.desktop {
			entry.pluginGoSource = filepath.Join(pluginPath, build.BuildPath)
			autoImportTemplate := filepath.Join(entry.pluginGoSource, "import.go.tmpl")
			_, err := os.Stat(autoImportTemplate)
			entry.autoImport = true
			if err != nil {
				entry.autoImport = false
				if !os.IsNotExist(err) {
					return nil, errors.Wrapf(err, "failed to stat %s", autoImportTemplate)
				}
			}
		} else {
			for _, plugin := range onlineList {
				if entry.name == plugin.Name {
					entry.desktop = true
					entry.standaloneImpl = true
					entry.autoImport = true
					entry.pluginGoSource = plugin.ImportFile
					break
				}
			}
		}
		list = append(list, entry)
	}
	return list, nil
}