// Copyright 2021 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"
	"go/ast"
	"go/parser"
	"go/token"
	"go/types"
	"strings"

	"golang.org/x/tools/internal/lsp/lsppos"
	"golang.org/x/tools/internal/lsp/protocol"
	"golang.org/x/tools/internal/lsp/source"
	"golang.org/x/tools/internal/memoize"
)

// symbolize returns the result of symbolizing the file identified by fh, using a cache.
func (s *snapshot) symbolize(ctx context.Context, fh source.FileHandle) ([]source.Symbol, error) {
	uri := fh.URI()

	s.mu.Lock()
	entry, hit := s.symbolizeHandles.Get(uri)
	s.mu.Unlock()

	type symbolizeResult struct {
		symbols []source.Symbol
		err     error
	}

	// Cache miss?
	if !hit {
		type symbolHandleKey source.Hash
		key := symbolHandleKey(fh.FileIdentity().Hash)
		promise, release := s.store.Promise(key, func(_ context.Context, arg interface{}) interface{} {
			symbols, err := symbolizeImpl(arg.(*snapshot), fh)
			return symbolizeResult{symbols, err}
		})

		entry = promise

		s.mu.Lock()
		s.symbolizeHandles.Set(uri, entry, func(_, _ interface{}) { release() })
		s.mu.Unlock()
	}

	// Await result.
	v, err := s.awaitPromise(ctx, entry.(*memoize.Promise))
	if err != nil {
		return nil, err
	}
	res := v.(symbolizeResult)
	return res.symbols, res.err
}

// symbolizeImpl reads and parses a file and extracts symbols from it.
// It may use a parsed file already present in the cache but
// otherwise does not populate the cache.
func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) {
	src, err := fh.Read()
	if err != nil {
		return nil, err
	}

	var (
		file     *ast.File
		fileDesc *token.File
	)

	// If the file has already been fully parsed through the
	// cache, we can just use the result. But we don't want to
	// populate the cache after a miss.
	snapshot.mu.Lock()
	pgf, _ := snapshot.peekParseGoLocked(fh, source.ParseFull)
	snapshot.mu.Unlock()
	if pgf != nil {
		file = pgf.File
		fileDesc = pgf.Tok
	}

	// Otherwise, we parse the file ourselves. Notably we don't use parseGo here,
	// so that we can avoid parsing comments and can skip object resolution,
	// which has a meaningful impact on performance. Neither comments nor objects
	// are necessary for symbol construction.
	if file == nil {
		fset := token.NewFileSet()
		file, err = parser.ParseFile(fset, fh.URI().Filename(), src, skipObjectResolution)
		if file == nil {
			return nil, err
		}
		fileDesc = fset.File(file.Package)
	}

	w := &symbolWalker{
		mapper: lsppos.NewTokenMapper(src, fileDesc),
	}

	w.fileDecls(file.Decls)

	return w.symbols, w.firstError
}

type symbolWalker struct {
	mapper *lsppos.TokenMapper // for computing positions

	symbols    []source.Symbol
	firstError error
}

func (w *symbolWalker) atNode(node ast.Node, name string, kind protocol.SymbolKind, path ...*ast.Ident) {
	var b strings.Builder
	for _, ident := range path {
		if ident != nil {
			b.WriteString(ident.Name)
			b.WriteString(".")
		}
	}
	b.WriteString(name)

	rng, err := w.mapper.Range(node.Pos(), node.End())
	if err != nil {
		w.error(err)
		return
	}
	sym := source.Symbol{
		Name:  b.String(),
		Kind:  kind,
		Range: rng,
	}
	w.symbols = append(w.symbols, sym)
}

func (w *symbolWalker) error(err error) {
	if err != nil && w.firstError == nil {
		w.firstError = err
	}
}

func (w *symbolWalker) fileDecls(decls []ast.Decl) {
	for _, decl := range decls {
		switch decl := decl.(type) {
		case *ast.FuncDecl:
			kind := protocol.Function
			var recv *ast.Ident
			if decl.Recv.NumFields() > 0 {
				kind = protocol.Method
				recv = unpackRecv(decl.Recv.List[0].Type)
			}
			w.atNode(decl.Name, decl.Name.Name, kind, recv)
		case *ast.GenDecl:
			for _, spec := range decl.Specs {
				switch spec := spec.(type) {
				case *ast.TypeSpec:
					kind := guessKind(spec)
					w.atNode(spec.Name, spec.Name.Name, kind)
					w.walkType(spec.Type, spec.Name)
				case *ast.ValueSpec:
					for _, name := range spec.Names {
						kind := protocol.Variable
						if decl.Tok == token.CONST {
							kind = protocol.Constant
						}
						w.atNode(name, name.Name, kind)
					}
				}
			}
		}
	}
}

func guessKind(spec *ast.TypeSpec) protocol.SymbolKind {
	switch spec.Type.(type) {
	case *ast.InterfaceType:
		return protocol.Interface
	case *ast.StructType:
		return protocol.Struct
	case *ast.FuncType:
		return protocol.Function
	}
	return protocol.Class
}

func unpackRecv(rtyp ast.Expr) *ast.Ident {
	// Extract the receiver identifier. Lifted from go/types/resolver.go
L:
	for {
		switch t := rtyp.(type) {
		case *ast.ParenExpr:
			rtyp = t.X
		case *ast.StarExpr:
			rtyp = t.X
		default:
			break L
		}
	}
	if name, _ := rtyp.(*ast.Ident); name != nil {
		return name
	}
	return nil
}

// walkType processes symbols related to a type expression. path is path of
// nested type identifiers to the type expression.
func (w *symbolWalker) walkType(typ ast.Expr, path ...*ast.Ident) {
	switch st := typ.(type) {
	case *ast.StructType:
		for _, field := range st.Fields.List {
			w.walkField(field, protocol.Field, protocol.Field, path...)
		}
	case *ast.InterfaceType:
		for _, field := range st.Methods.List {
			w.walkField(field, protocol.Interface, protocol.Method, path...)
		}
	}
}

// walkField processes symbols related to the struct field or interface method.
//
// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed
// or named. path is the path of nested identifiers containing the field.
func (w *symbolWalker) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) {
	if len(field.Names) == 0 {
		switch typ := field.Type.(type) {
		case *ast.SelectorExpr:
			// embedded qualified type
			w.atNode(field, typ.Sel.Name, unnamedKind, path...)
		default:
			w.atNode(field, types.ExprString(field.Type), unnamedKind, path...)
		}
	}
	for _, name := range field.Names {
		w.atNode(name, name.Name, namedKind, path...)
		w.walkType(field.Type, append(path, name)...)
	}
}
