// 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 typesinternal provides helpful operators for dealing with
// go/types:
//
//   - operators for querying typed syntax trees (e.g. [Imports], [IsFunctionNamed]);
//   - functions for converting types to strings or syntax (e.g. [TypeExpr], FileQualifier]);
//   - helpers for working with the [go/types] API (e.g. [NewTypesInfo]);
//   - access to internal go/types APIs that are not yet
//     exported (e.g. [SetUsesCgo], [ErrorCodeStartEnd], [VarKind]); and
//   - common algorithms related to types (e.g. [TooNewStdSymbols]).
//
// See also:
//   - [golang.org/x/tools/internal/astutil], for operations on untyped syntax;
//   - [golang.org/x/tools/internal/analysisinernal], for helpers for analyzers;
//   - [golang.org/x/tools/internal/refactor], for operators to compute text edits.
package typesinternal

import (
	"go/ast"
	"go/token"
	"go/types"
	"reflect"

	"golang.org/x/tools/go/ast/inspector"
	"golang.org/x/tools/internal/aliases"
)

func SetUsesCgo(conf *types.Config) bool {
	v := reflect.ValueOf(conf).Elem()

	f := v.FieldByName("go115UsesCgo")
	if !f.IsValid() {
		f = v.FieldByName("UsesCgo")
		if !f.IsValid() {
			return false
		}
	}

	*(*bool)(f.Addr().UnsafePointer()) = true

	return true
}

// ErrorCodeStartEnd extracts additional information from types.Error values
// generated by Go version 1.16 and later: the error code, start position, and
// end position. If all positions are valid, start <= err.Pos <= end.
//
// If the data could not be read, the final result parameter will be false.
//
// TODO(adonovan): eliminate start/end when proposal #71803 is accepted.
func ErrorCodeStartEnd(err types.Error) (code ErrorCode, start, end token.Pos, ok bool) {
	var data [3]int
	// By coincidence all of these fields are ints, which simplifies things.
	v := reflect.ValueOf(err)
	for i, name := range []string{"go116code", "go116start", "go116end"} {
		f := v.FieldByName(name)
		if !f.IsValid() {
			return 0, 0, 0, false
		}
		data[i] = int(f.Int())
	}
	return ErrorCode(data[0]), token.Pos(data[1]), token.Pos(data[2]), true
}

// NameRelativeTo returns a types.Qualifier that qualifies members of
// all packages other than pkg, using only the package name.
// (By contrast, [types.RelativeTo] uses the complete package path,
// which is often excessive.)
//
// If pkg is nil, it is equivalent to [*types.Package.Name].
//
// TODO(adonovan): all uses of this with TypeString should be
// eliminated when https://go.dev/issues/75604 is resolved.
func NameRelativeTo(pkg *types.Package) types.Qualifier {
	return func(other *types.Package) string {
		if pkg != nil && pkg == other {
			return "" // same package; unqualified
		}
		return other.Name()
	}
}

// TypeNameFor returns the type name symbol for the specified type, if
// it is a [*types.Alias], [*types.Named], [*types.TypeParam], or a
// [*types.Basic] representing a type.
//
// For all other types, and for Basic types representing a builtin,
// constant, or nil, it returns nil. Be careful not to convert the
// resulting nil pointer to a [types.Object]!
//
// If t is the type of a constant, it may be an "untyped" type, which
// has no TypeName. To access the name of such types (e.g. "untyped
// int"), use [types.Basic.Name].
func TypeNameFor(t types.Type) *types.TypeName {
	switch t := t.(type) {
	case *types.Alias:
		return t.Obj()
	case *types.Named:
		return t.Obj()
	case *types.TypeParam:
		return t.Obj()
	case *types.Basic:
		// See issues #71886 and #66890 for some history.
		if tname, ok := types.Universe.Lookup(t.Name()).(*types.TypeName); ok {
			return tname
		}
	}
	return nil
}

// A NamedOrAlias is a [types.Type] that is named (as
// defined by the spec) and capable of bearing type parameters: it
// abstracts aliases ([types.Alias]) and defined types
// ([types.Named]).
//
// Every type declared by an explicit "type" declaration is a
// NamedOrAlias. (Built-in type symbols may additionally
// have type [types.Basic], which is not a NamedOrAlias,
// though the spec regards them as "named"; see [TypeNameFor].)
//
// NamedOrAlias cannot expose the Origin method, because
// [types.Alias.Origin] and [types.Named.Origin] have different
// (covariant) result types; use [Origin] instead.
type NamedOrAlias interface {
	types.Type
	Obj() *types.TypeName
	TypeArgs() *types.TypeList
	TypeParams() *types.TypeParamList
	SetTypeParams(tparams []*types.TypeParam)
}

var (
	_ NamedOrAlias = (*types.Alias)(nil)
	_ NamedOrAlias = (*types.Named)(nil)
)

// Origin returns the generic type of the Named or Alias type t if it
// is instantiated, otherwise it returns t.
func Origin(t NamedOrAlias) NamedOrAlias {
	switch t := t.(type) {
	case *types.Alias:
		return aliases.Origin(t)
	case *types.Named:
		return t.Origin()
	}
	return t
}

// IsPackageLevel reports whether obj is a package-level symbol.
func IsPackageLevel(obj types.Object) bool {
	return obj.Pkg() != nil && obj.Parent() == obj.Pkg().Scope()
}

// NewTypesInfo returns a *types.Info with all maps populated.
func NewTypesInfo() *types.Info {
	return &types.Info{
		Types:        map[ast.Expr]types.TypeAndValue{},
		Instances:    map[*ast.Ident]types.Instance{},
		Defs:         map[*ast.Ident]types.Object{},
		Uses:         map[*ast.Ident]types.Object{},
		Implicits:    map[ast.Node]types.Object{},
		Selections:   map[*ast.SelectorExpr]*types.Selection{},
		Scopes:       map[ast.Node]*types.Scope{},
		FileVersions: map[*ast.File]string{},
	}
}

// EnclosingScope returns the innermost block logically enclosing the cursor.
func EnclosingScope(info *types.Info, cur inspector.Cursor) *types.Scope {
	for cur := range cur.Enclosing() {
		n := cur.Node()
		// A function's Scope is associated with its FuncType.
		switch f := n.(type) {
		case *ast.FuncDecl:
			n = f.Type
		case *ast.FuncLit:
			n = f.Type
		}
		if b := info.Scopes[n]; b != nil {
			return b
		}
	}
	panic("no Scope for *ast.File")
}

// Imports reports whether path is imported by pkg.
func Imports(pkg *types.Package, path string) bool {
	for _, imp := range pkg.Imports() {
		if imp.Path() == path {
			return true
		}
	}
	return false
}
