// Copyright 2023 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 (
	"bytes"
	"go/build"
	"go/parser"
	"go/token"
	"io"
	"path/filepath"
	"strings"

	"golang.org/x/tools/gopls/internal/util/bug"
)

type port struct{ GOOS, GOARCH string }

var (
	// preferredPorts holds GOOS/GOARCH combinations for which we dynamically
	// create new Views, by setting GOOS=... and GOARCH=... on top of
	// user-provided configuration when we detect that the default build
	// configuration does not match an open file. Ports are matched in the order
	// defined below, so that when multiple ports match a file we use the port
	// occurring at a lower index in the slice. For that reason, we sort first
	// class ports ahead of secondary ports, and (among first class ports) 64-bit
	// ports ahead of the less common 32-bit ports.
	preferredPorts = []port{
		// First class ports, from https://go.dev/wiki/PortingPolicy.
		{"darwin", "amd64"},
		{"darwin", "arm64"},
		{"linux", "amd64"},
		{"linux", "arm64"},
		{"windows", "amd64"},
		{"linux", "arm"},
		{"linux", "386"},
		{"windows", "386"},

		// Secondary ports, from GOROOT/src/internal/platform/zosarch.go.
		// (First class ports are commented out.)
		{"aix", "ppc64"},
		{"dragonfly", "amd64"},
		{"freebsd", "386"},
		{"freebsd", "amd64"},
		{"freebsd", "arm"},
		{"freebsd", "arm64"},
		{"illumos", "amd64"},
		{"linux", "ppc64"},
		{"linux", "ppc64le"},
		{"linux", "mips"},
		{"linux", "mipsle"},
		{"linux", "mips64"},
		{"linux", "mips64le"},
		{"linux", "riscv64"},
		{"linux", "s390x"},
		{"android", "386"},
		{"android", "amd64"},
		{"android", "arm"},
		{"android", "arm64"},
		{"ios", "arm64"},
		{"ios", "amd64"},
		{"js", "wasm"},
		{"netbsd", "386"},
		{"netbsd", "amd64"},
		{"netbsd", "arm"},
		{"netbsd", "arm64"},
		{"openbsd", "386"},
		{"openbsd", "amd64"},
		{"openbsd", "arm"},
		{"openbsd", "arm64"},
		{"openbsd", "mips64"},
		{"plan9", "386"},
		{"plan9", "amd64"},
		{"plan9", "arm"},
		{"solaris", "amd64"},
		{"windows", "arm"},
		{"windows", "arm64"},

		{"aix", "ppc64"},
		{"android", "386"},
		{"android", "amd64"},
		{"android", "arm"},
		{"android", "arm64"},
		// {"darwin", "amd64"},
		// {"darwin", "arm64"},
		{"dragonfly", "amd64"},
		{"freebsd", "386"},
		{"freebsd", "amd64"},
		{"freebsd", "arm"},
		{"freebsd", "arm64"},
		{"freebsd", "riscv64"},
		{"illumos", "amd64"},
		{"ios", "amd64"},
		{"ios", "arm64"},
		{"js", "wasm"},
		// {"linux", "386"},
		// {"linux", "amd64"},
		// {"linux", "arm"},
		// {"linux", "arm64"},
		{"linux", "loong64"},
		{"linux", "mips"},
		{"linux", "mips64"},
		{"linux", "mips64le"},
		{"linux", "mipsle"},
		{"linux", "ppc64"},
		{"linux", "ppc64le"},
		{"linux", "riscv64"},
		{"linux", "s390x"},
		{"linux", "sparc64"},
		{"netbsd", "386"},
		{"netbsd", "amd64"},
		{"netbsd", "arm"},
		{"netbsd", "arm64"},
		{"openbsd", "386"},
		{"openbsd", "amd64"},
		{"openbsd", "arm"},
		{"openbsd", "arm64"},
		{"openbsd", "mips64"},
		{"openbsd", "ppc64"},
		{"openbsd", "riscv64"},
		{"plan9", "386"},
		{"plan9", "amd64"},
		{"plan9", "arm"},
		{"solaris", "amd64"},
		{"wasip1", "wasm"},
		// {"windows", "386"},
		// {"windows", "amd64"},
		{"windows", "arm"},
		{"windows", "arm64"},
	}
)

// matches reports whether the port matches a file with the given absolute path
// and content.
//
// Note that this function accepts content rather than e.g. a file.Handle,
// because we trim content before matching for performance reasons, and
// therefore need to do this outside of matches when considering multiple ports.
func (p port) matches(path string, content []byte) bool {
	ctxt := build.Default // make a copy
	ctxt.UseAllFiles = false
	path = filepath.Clean(path)
	if !filepath.IsAbs(path) {
		bug.Reportf("non-abs file path %q", path)
		return false // fail closed
	}
	dir, name := filepath.Split(path)

	// The only virtualized operation called by MatchFile is OpenFile.
	ctxt.OpenFile = func(p string) (io.ReadCloser, error) {
		if p != path {
			return nil, bug.Errorf("unexpected file %q", p)
		}
		return io.NopCloser(bytes.NewReader(content)), nil
	}

	ctxt.GOOS = p.GOOS
	ctxt.GOARCH = p.GOARCH
	ok, err := ctxt.MatchFile(dir, name)
	return err == nil && ok
}

// trimContentForPortMatch trims the given Go file content to a minimal file
// containing the same build constraints, if any.
//
// This is an unfortunate but necessary optimization, as matching build
// constraints using go/build has significant overhead, and involves parsing
// more than just the build constraint.
//
// TestMatchingPortsConsistency enforces consistency by comparing results
// without trimming content.
func trimContentForPortMatch(content []byte) []byte {
	buildComment := buildComment(content)
	return []byte(buildComment + "\npackage p") // package name does not matter
}

// buildComment returns the first matching //go:build comment in the given
// content, or "" if none exists.
func buildComment(content []byte) string {
	f, err := parser.ParseFile(token.NewFileSet(), "", content, parser.PackageClauseOnly|parser.ParseComments)
	if err != nil {
		return ""
	}

	for _, cg := range f.Comments {
		for _, c := range cg.List {
			if isGoBuildComment(c.Text) {
				return c.Text
			}
		}
	}
	return ""
}

// Adapted from go/build/build.go.
//
// TODO(rfindley): use constraint.IsGoBuild once we are on 1.19+.
func isGoBuildComment(line string) bool {
	const goBuildComment = "//go:build"
	if !strings.HasPrefix(line, goBuildComment) {
		return false
	}
	// Report whether //go:build is followed by a word boundary.
	line = strings.TrimSpace(line)
	rest := line[len(goBuildComment):]
	return len(rest) == 0 || len(strings.TrimSpace(rest)) < len(rest)
}
