// Copyright 2013 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.

// This file implements LinkifyText which introduces
// links for identifiers pointing to their declarations.
// The approach does not cover all cases because godoc
// doesn't have complete type information, but it's
// reasonably good for browsing.

package main

import (
	"fmt"
	"go/ast"
	"go/token"
	"io"
	"strconv"
)

// LinkifyText HTML-escapes source text and writes it to w.
// Identifiers that are in a "use" position (i.e., that are
// not being declared), are wrapped with HTML links pointing
// to the respective declaration, if possible. Comments are
// formatted the same way as with FormatText.
//
func LinkifyText(w io.Writer, text []byte, n ast.Node) {
	links := linksFor(n)

	i := 0     // links index
	prev := "" // prev HTML tag
	linkWriter := func(w io.Writer, _ int, start bool) {
		// end tag
		if !start {
			if prev != "" {
				fmt.Fprintf(w, `</%s>`, prev)
				prev = ""
			}
			return
		}

		// start tag
		prev = ""
		if i < len(links) {
			switch info := links[i]; {
			case info.path != "" && info.name == "":
				// package path
				fmt.Fprintf(w, `<a href="/pkg/%s/">`, info.path)
				prev = "a"
			case info.path != "" && info.name != "":
				// qualified identifier
				fmt.Fprintf(w, `<a href="/pkg/%s/#%s">`, info.path, info.name)
				prev = "a"
			case info.path == "" && info.name != "":
				// local identifier
				if info.mode == identVal {
					fmt.Fprintf(w, `<span id="%s">`, info.name)
					prev = "span"
				} else {
					fmt.Fprintf(w, `<a href="#%s">`, info.name)
					prev = "a"
				}
			}
			i++
		}
	}

	idents := tokenSelection(text, token.IDENT)
	comments := tokenSelection(text, token.COMMENT)
	FormatSelections(w, text, linkWriter, idents, selectionTag, comments)
}

// A link describes the (HTML) link information for an identifier.
// The zero value of a link represents "no link".
//
type link struct {
	mode       identMode
	path, name string // package path, identifier name
}

// linksFor returns the list of links for the identifiers used
// by node in the same order as they appear in the source.
//
func linksFor(node ast.Node) (list []link) {
	modes := identModesFor(node)

	// NOTE: We are expecting ast.Inspect to call the
	//       callback function in source text order.
	ast.Inspect(node, func(node ast.Node) bool {
		switch n := node.(type) {
		case *ast.Ident:
			m := modes[n]
			info := link{mode: m}
			switch m {
			case identUse:
				if n.Obj == nil && predeclared[n.Name] {
					info.path = builtinPkgPath
				}
				info.name = n.Name
			case identDef:
				// any declaration expect const or var - empty link
			case identVal:
				// const or var declaration
				info.name = n.Name
			}
			list = append(list, info)
			return false
		case *ast.SelectorExpr:
			// Detect qualified identifiers of the form pkg.ident.
			// If anything fails we return true and collect individual
			// identifiers instead.
			if x, _ := n.X.(*ast.Ident); x != nil {
				// x must be a package for a qualified identifier
				if obj := x.Obj; obj != nil && obj.Kind == ast.Pkg {
					if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil {
						// spec.Path.Value is the import path
						if path, err := strconv.Unquote(spec.Path.Value); err == nil {
							// Register two links, one for the package
							// and one for the qualified identifier.
							info := link{path: path}
							list = append(list, info)
							info.name = n.Sel.Name
							list = append(list, info)
							return false
						}
					}
				}
			}
		}
		return true
	})

	return
}

// The identMode describes how an identifier is "used" at its source location.
type identMode int

const (
	identUse identMode = iota // identifier is used (must be zero value for identMode)
	identDef                  // identifier is defined
	identVal                  // identifier is defined in a const or var declaration
)

// identModesFor returns a map providing the identMode for each identifier used by node.
func identModesFor(node ast.Node) map[*ast.Ident]identMode {
	m := make(map[*ast.Ident]identMode)

	ast.Inspect(node, func(node ast.Node) bool {
		switch n := node.(type) {
		case *ast.Field:
			for _, n := range n.Names {
				m[n] = identDef
			}
		case *ast.ImportSpec:
			if name := n.Name; name != nil {
				m[name] = identDef
			}
		case *ast.ValueSpec:
			for _, n := range n.Names {
				m[n] = identVal
			}
		case *ast.TypeSpec:
			m[n.Name] = identDef
		case *ast.FuncDecl:
			m[n.Name] = identDef
		case *ast.AssignStmt:
			// Short variable declarations only show up if we apply
			// this code to all source code (as opposed to exported
			// declarations only).
			if n.Tok == token.DEFINE {
				// Some of the lhs variables may be re-declared,
				// so technically they are not defs. We don't
				// care for now.
				for _, x := range n.Lhs {
					// Each lhs expression should be an
					// ident, but we are conservative and check.
					if n, _ := x.(*ast.Ident); n != nil {
						m[n] = identVal
					}
				}
			}
		}
		return true
	})

	return m
}

// The predeclared map represents the set of all predeclared identifiers.
// TODO(gri) This information is also encoded in similar maps in go/doc,
//           but not exported. Consider exporting an accessor and using
//           it instead.
var predeclared = map[string]bool{
	"bool":       true,
	"byte":       true,
	"complex64":  true,
	"complex128": true,
	"error":      true,
	"float32":    true,
	"float64":    true,
	"int":        true,
	"int8":       true,
	"int16":      true,
	"int32":      true,
	"int64":      true,
	"rune":       true,
	"string":     true,
	"uint":       true,
	"uint8":      true,
	"uint16":     true,
	"uint32":     true,
	"uint64":     true,
	"uintptr":    true,
	"true":       true,
	"false":      true,
	"iota":       true,
	"nil":        true,
	"append":     true,
	"cap":        true,
	"close":      true,
	"complex":    true,
	"copy":       true,
	"delete":     true,
	"imag":       true,
	"len":        true,
	"make":       true,
	"new":        true,
	"panic":      true,
	"print":      true,
	"println":    true,
	"real":       true,
	"recover":    true,
}
