/* Copyright 2018 The Bazel Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package repos

import (
	"bytes"
	"encoding/json"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"runtime"
	"strings"

	"github.com/bazelbuild/bazel-gazelle/internal/label"
)

type module struct {
	Path, Version string
	Main          bool
}

var regexMixedVersioning = regexp.MustCompile(`^(.*?)-([0-9]{14})-([a-fA-F0-9]{12})$`)

func toRepoRule(mod module) Repo {
	var tag, commit string

	if gr := regexMixedVersioning.FindStringSubmatch(mod.Version); gr != nil {
		commit = gr[3]
	} else {
		tag = strings.TrimSuffix(mod.Version, "+incompatible")
	}

	return Repo{
		Name:     label.ImportPathToBazelRepoName(mod.Path),
		GoPrefix: mod.Path,
		Commit:   commit,
		Tag:      tag,
	}
}

func importRepoRulesModules(filename string) (repos []Repo, err error) {
	tempDir, err := copyGoModToTemp(filename)
	if err != nil {
		return nil, err
	}
	defer os.RemoveAll(tempDir)

	data, err := goListModulesFn(tempDir)
	if err != nil {
		return nil, err
	}

	dec := json.NewDecoder(bytes.NewReader(data))
	for dec.More() {
		var mod module
		if err := dec.Decode(&mod); err != nil {
			return nil, err
		}
		if mod.Main {
			continue
		}

		repos = append(repos, toRepoRule(mod))
	}

	return repos, nil
}

// goListModulesFn may be overridden by tests.
var goListModulesFn = goListModules

// goListModules invokes "go list" in a directory containing a go.mod file.
func goListModules(dir string) ([]byte, error) {
	goTool := findGoTool()
	cmd := exec.Command(goTool, "list", "-m", "-json", "all")
	cmd.Stderr = os.Stderr
	cmd.Dir = dir
	data, err := cmd.Output()
	return data, err
}

// copyGoModToTemp copies to given go.mod file to a temporary directory.
// go list tends to mutate go.mod files, but gazelle shouldn't do that.
func copyGoModToTemp(filename string) (tempDir string, err error) {
	goModOrig, err := os.Open(filename)
	if err != nil {
		return "", err
	}
	defer goModOrig.Close()

	tempDir, err = ioutil.TempDir("", "gazelle-temp-gomod")
	if err != nil {
		return "", err
	}

	goModCopy, err := os.Create(filepath.Join(tempDir, "go.mod"))
	if err != nil {
		os.Remove(tempDir)
		return "", err
	}
	defer func() {
		if cerr := goModCopy.Close(); err == nil && cerr != nil {
			err = cerr
		}
	}()

	_, err = io.Copy(goModCopy, goModOrig)
	if err != nil {
		os.RemoveAll(tempDir)
		return "", err
	}
	return tempDir, err
}

// findGoTool attempts to locate the go executable. If GOROOT is set, we'll
// prefer the one in there; otherwise, we'll rely on PATH. If the wrapper
// script generated by the gazelle rule is invoked by Bazel, it will set
// GOROOT to the configured SDK. We don't want to rely on the host SDK in
// that situation.
func findGoTool() string {
	path := "go" // rely on PATH by default
	if goroot, ok := os.LookupEnv("GOROOT"); ok {
		path = filepath.Join(goroot, "bin", "go")
	}
	if runtime.GOOS == "windows" {
		path += ".exe"
	}
	return path
}
