// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cache

import (
	"context"
	"errors"
	"fmt"
	"path/filepath"

	"golang.org/x/mod/modfile"
	"golang.org/x/tools/gopls/internal/file"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/test/integration/fake/glob"
)

// isGoWork reports if uri is a go.work file.
func isGoWork(uri protocol.DocumentURI) bool {
	return uri.Base() == "go.work"
}

// goWorkModules returns the URIs of go.mod files named by the go.work file.
func goWorkModules(ctx context.Context, gowork protocol.DocumentURI, fs file.Source) (map[protocol.DocumentURI]unit, error) {
	fh, err := fs.ReadFile(ctx, gowork)
	if err != nil {
		return nil, err // canceled
	}
	content, err := fh.Content()
	if err != nil {
		return nil, err
	}
	filename := gowork.Path()
	dir := filepath.Dir(filename)
	workFile, err := modfile.ParseWork(filename, content, nil)
	if err != nil {
		return nil, fmt.Errorf("parsing go.work: %w", err)
	}
	var usedDirs []string
	for _, use := range workFile.Use {
		usedDirs = append(usedDirs, use.Path)
	}
	return localModFiles(dir, usedDirs), nil
}

// localModFiles builds a set of local go.mod files referenced by
// goWorkOrModPaths, which is a slice of paths as contained in a go.work 'use'
// directive or go.mod 'replace' directive (and which therefore may use either
// '/' or '\' as a path separator).
func localModFiles(relativeTo string, goWorkOrModPaths []string) map[protocol.DocumentURI]unit {
	modFiles := make(map[protocol.DocumentURI]unit)
	for _, path := range goWorkOrModPaths {
		modDir := filepath.FromSlash(path)
		if !filepath.IsAbs(modDir) {
			modDir = filepath.Join(relativeTo, modDir)
		}
		modURI := protocol.URIFromPath(filepath.Join(modDir, "go.mod"))
		modFiles[modURI] = unit{}
	}
	return modFiles
}

// isGoMod reports if uri is a go.mod file.
func isGoMod(uri protocol.DocumentURI) bool {
	return uri.Base() == "go.mod"
}

// isWorkspaceFile reports if uri matches a set of globs defined in workspaceFiles
func isWorkspaceFile(uri protocol.DocumentURI, workspaceFiles []string) bool {
	for _, workspaceFile := range workspaceFiles {
		g, err := glob.Parse(workspaceFile)
		if err != nil {
			continue
		}

		if g.Match(uri.Path()) {
			return true
		}
	}
	return false
}

// goModModules returns the URIs of "workspace" go.mod files defined by a
// go.mod file. This set is defined to be the given go.mod file itself, as well
// as the modfiles of any locally replaced modules in the go.mod file.
func goModModules(ctx context.Context, gomod protocol.DocumentURI, fs file.Source) (map[protocol.DocumentURI]unit, error) {
	fh, err := fs.ReadFile(ctx, gomod)
	if err != nil {
		return nil, err // canceled
	}
	content, err := fh.Content()
	if err != nil {
		return nil, err
	}
	filename := gomod.Path()
	dir := filepath.Dir(filename)
	modFile, err := modfile.Parse(filename, content, nil)
	if err != nil {
		return nil, err
	}
	var localReplaces []string
	for _, replace := range modFile.Replace {
		if modfile.IsDirectoryPath(replace.New.Path) {
			localReplaces = append(localReplaces, replace.New.Path)
		}
	}
	modFiles := localModFiles(dir, localReplaces)
	modFiles[gomod] = unit{}
	return modFiles, nil
}

// fileExists reports whether the file has a Content (which may be empty).
// An overlay exists even if it is not reflected in the file system.
func fileExists(fh file.Handle) bool {
	_, err := fh.Content()
	return err == nil
}

// errExhausted is returned by findModules if the file scan limit is reached.
var errExhausted = errors.New("exhausted")

// Limit go.mod search to 1 million files. As a point of reference,
// Kubernetes has 22K files (as of 2020-11-24).
//
// Note: per golang/go#56496, the previous limit of 1M files was too slow, at
// which point this limit was decreased to 100K.
const fileLimit = 100_000
