// Copyright 2019 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 source

import (
	"context"
	"fmt"
	"go/ast"
	"go/doc"
	"go/format"
	"go/token"
	"go/types"
	"strings"
)

// formatter returns the a hover value formatted with its documentation.
type formatter func(interface{}, *ast.CommentGroup) (string, error)

func (i *IdentifierInfo) Hover(ctx context.Context, qf types.Qualifier, markdownSupported, wantComments bool) (string, error) {
	file := i.File.GetAST(ctx)
	if qf == nil {
		pkg := i.File.GetPackage(ctx)
		qf = qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo())
	}
	var b strings.Builder
	f := func(x interface{}, c *ast.CommentGroup) (string, error) {
		if !wantComments {
			c = nil
		}
		return writeHover(x, i.File.FileSet(), &b, c, markdownSupported, qf)
	}
	obj := i.Declaration.Object
	switch node := i.Declaration.Node.(type) {
	case *ast.GenDecl:
		switch obj := obj.(type) {
		case *types.TypeName, *types.Var, *types.Const, *types.Func:
			return formatGenDecl(node, obj, obj.Type(), f)
		}
	case *ast.TypeSpec:
		if obj.Parent() == types.Universe {
			if obj.Name() == "error" {
				return f(node, nil)
			}
			return f(node.Name, nil) // comments not needed for builtins
		}
	case *ast.FuncDecl:
		switch obj.(type) {
		case *types.Func:
			return f(obj, node.Doc)
		case *types.Builtin:
			return f(node.Type, node.Doc)
		}
	}
	return f(obj, nil)
}

func formatGenDecl(node *ast.GenDecl, obj types.Object, typ types.Type, f formatter) (string, error) {
	if _, ok := typ.(*types.Named); ok {
		switch typ.Underlying().(type) {
		case *types.Interface, *types.Struct:
			return formatGenDecl(node, obj, typ.Underlying(), f)
		}
	}
	var spec ast.Spec
	for _, s := range node.Specs {
		if s.Pos() <= obj.Pos() && obj.Pos() <= s.End() {
			spec = s
			break
		}
	}
	if spec == nil {
		return "", fmt.Errorf("no spec for node %v at position %v", node, obj.Pos())
	}
	// If we have a field or method.
	switch obj.(type) {
	case *types.Var, *types.Const, *types.Func:
		return formatVar(spec, obj, f)
	}
	// Handle types.
	switch spec := spec.(type) {
	case *ast.TypeSpec:
		if len(node.Specs) > 1 {
			// If multiple types are declared in the same block.
			return f(spec.Type, spec.Doc)
		} else {
			return f(spec, node.Doc)
		}
	case *ast.ValueSpec:
		return f(spec, spec.Doc)
	case *ast.ImportSpec:
		return f(spec, spec.Doc)
	}
	return "", fmt.Errorf("unable to format spec %v (%T)", spec, spec)
}

func formatVar(node ast.Spec, obj types.Object, f formatter) (string, error) {
	var fieldList *ast.FieldList
	if spec, ok := node.(*ast.TypeSpec); ok {
		switch t := spec.Type.(type) {
		case *ast.StructType:
			fieldList = t.Fields
		case *ast.InterfaceType:
			fieldList = t.Methods
		}
	}
	// If we have a struct or interface declaration,
	// we need to match the object to the corresponding field or method.
	if fieldList != nil {
		for i := 0; i < len(fieldList.List); i++ {
			field := fieldList.List[i]
			if field.Pos() <= obj.Pos() && obj.Pos() <= field.End() {
				if field.Doc.Text() != "" {
					return f(obj, field.Doc)
				} else if field.Comment.Text() != "" {
					return f(obj, field.Comment)
				}
			}
		}
	}
	// If we weren't able to find documentation for the object.
	return f(obj, nil)
}

// writeHover writes the hover for a given node and its documentation.
func writeHover(x interface{}, fset *token.FileSet, b *strings.Builder, c *ast.CommentGroup, markdownSupported bool, qf types.Qualifier) (string, error) {
	if c != nil {
		// TODO(rstambler): Improve conversion from Go docs to markdown.
		b.WriteString(doc.Synopsis(c.Text()))
		b.WriteRune('\n')
	}
	if markdownSupported {
		b.WriteString("```go\n")
	}
	switch x := x.(type) {
	case ast.Node:
		if err := format.Node(b, fset, x); err != nil {
			return "", err
		}
	case types.Object:
		b.WriteString(types.ObjectString(x, qf))
	}
	if markdownSupported {
		b.WriteString("\n```")
	}
	return b.String(), nil
}
