// 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 template

import (
	"bytes"
	"context"
	"fmt"
	"text/template/parse"
	"unicode/utf8"

	"golang.org/x/tools/gopls/internal/lsp/protocol"
	"golang.org/x/tools/gopls/internal/lsp/source"
	"golang.org/x/tools/internal/event"
)

// in local coordinates, to be translated to protocol.DocumentSymbol
type symbol struct {
	start  int // for sorting
	length int // in runes (unicode code points)
	name   string
	kind   protocol.SymbolKind
	vardef bool // is this a variable definition?
	// do we care about selection range, or children?
	// no children yet, and selection range is the same as range
}

func (s symbol) String() string {
	return fmt.Sprintf("{%d,%d,%s,%s,%v}", s.start, s.length, s.name, s.kind, s.vardef)
}

// for FieldNode or VariableNode (or ChainNode?)
func (p *Parsed) fields(flds []string, x parse.Node) []symbol {
	ans := []symbol{}
	// guessing that there are no embedded blanks allowed. The doc is unclear
	lookfor := ""
	switch x.(type) {
	case *parse.FieldNode:
		for _, f := range flds {
			lookfor += "." + f // quadratic, but probably ok
		}
	case *parse.VariableNode:
		lookfor = flds[0]
		for i := 1; i < len(flds); i++ {
			lookfor += "." + flds[i]
		}
	case *parse.ChainNode: // PJW, what are these?
		for _, f := range flds {
			lookfor += "." + f // quadratic, but probably ok
		}
	default:
		// If these happen they will happen even if gopls is restarted
		// and the users does the same thing, so it is better not to panic.
		// context.Background() is used because we don't have access
		// to any other context. [we could, but it would be complicated]
		event.Log(context.Background(), fmt.Sprintf("%T unexpected in fields()", x))
		return nil
	}
	if len(lookfor) == 0 {
		event.Log(context.Background(), fmt.Sprintf("no strings in fields() %#v", x))
		return nil
	}
	startsAt := int(x.Position())
	ix := bytes.Index(p.buf[startsAt:], []byte(lookfor)) // HasPrefix? PJW?
	if ix < 0 || ix > len(lookfor) {                     // lookfor expected to be at start (or so)
		// probably golang.go/#43388, so back up
		startsAt -= len(flds[0]) + 1
		ix = bytes.Index(p.buf[startsAt:], []byte(lookfor)) // ix might be 1? PJW
		if ix < 0 {
			return ans
		}
	}
	at := ix + startsAt
	for _, f := range flds {
		at += 1 // .
		kind := protocol.Method
		if f[0] == '$' {
			kind = protocol.Variable
		}
		sym := symbol{name: f, kind: kind, start: at, length: utf8.RuneCount([]byte(f))}
		if kind == protocol.Variable && len(p.stack) > 1 {
			if pipe, ok := p.stack[len(p.stack)-2].(*parse.PipeNode); ok {
				for _, y := range pipe.Decl {
					if x == y {
						sym.vardef = true
					}
				}
			}
		}
		ans = append(ans, sym)
		at += len(f)
	}
	return ans
}

func (p *Parsed) findSymbols() {
	if len(p.stack) == 0 {
		return
	}
	n := p.stack[len(p.stack)-1]
	pop := func() {
		p.stack = p.stack[:len(p.stack)-1]
	}
	if n == nil { // allowing nil simplifies the code
		pop()
		return
	}
	nxt := func(nd parse.Node) {
		p.stack = append(p.stack, nd)
		p.findSymbols()
	}
	switch x := n.(type) {
	case *parse.ActionNode:
		nxt(x.Pipe)
	case *parse.BoolNode:
		// need to compute the length from the value
		msg := fmt.Sprintf("%v", x.True)
		p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(msg), kind: protocol.Boolean})
	case *parse.BranchNode:
		nxt(x.Pipe)
		nxt(x.List)
		nxt(x.ElseList)
	case *parse.ChainNode:
		p.symbols = append(p.symbols, p.fields(x.Field, x)...)
		nxt(x.Node)
	case *parse.CommandNode:
		for _, a := range x.Args {
			nxt(a)
		}
	//case *parse.CommentNode: // go 1.16
	//	log.Printf("implement %d", x.Type())
	case *parse.DotNode:
		sym := symbol{name: "dot", kind: protocol.Variable, start: int(x.Pos), length: 1}
		p.symbols = append(p.symbols, sym)
	case *parse.FieldNode:
		p.symbols = append(p.symbols, p.fields(x.Ident, x)...)
	case *parse.IdentifierNode:
		sym := symbol{name: x.Ident, kind: protocol.Function, start: int(x.Pos),
			length: utf8.RuneCount([]byte(x.Ident))}
		p.symbols = append(p.symbols, sym)
	case *parse.IfNode:
		nxt(&x.BranchNode)
	case *parse.ListNode:
		if x != nil { // wretched typed nils. Node should have an IfNil
			for _, nd := range x.Nodes {
				nxt(nd)
			}
		}
	case *parse.NilNode:
		sym := symbol{name: "nil", kind: protocol.Constant, start: int(x.Pos), length: 3}
		p.symbols = append(p.symbols, sym)
	case *parse.NumberNode:
		// no name; ascii
		p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(x.Text), kind: protocol.Number})
	case *parse.PipeNode:
		if x == nil { // {{template "foo"}}
			return
		}
		for _, d := range x.Decl {
			nxt(d)
		}
		for _, c := range x.Cmds {
			nxt(c)
		}
	case *parse.RangeNode:
		nxt(&x.BranchNode)
	case *parse.StringNode:
		// no name
		sz := utf8.RuneCount([]byte(x.Text))
		p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.String})
	case *parse.TemplateNode: // invoking a template
		// x.Pos points to the quote before the name
		p.symbols = append(p.symbols, symbol{name: x.Name, kind: protocol.Package, start: int(x.Pos) + 1,
			length: utf8.RuneCount([]byte(x.Name))})
		nxt(x.Pipe)
	case *parse.TextNode:
		if len(x.Text) == 1 && x.Text[0] == '\n' {
			break
		}
		// nothing to report, but build one for hover
		sz := utf8.RuneCount([]byte(x.Text))
		p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.Constant})
	case *parse.VariableNode:
		p.symbols = append(p.symbols, p.fields(x.Ident, x)...)
	case *parse.WithNode:
		nxt(&x.BranchNode)

	}
	pop()
}

// DocumentSymbols returns a hierarchy of the symbols defined in a template file.
// (The hierarchy is flat. SymbolInformation might be better.)
func DocumentSymbols(snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentSymbol, error) {
	buf, err := fh.Content()
	if err != nil {
		return nil, err
	}
	p := parseBuffer(buf)
	if p.ParseErr != nil {
		return nil, p.ParseErr
	}
	var ans []protocol.DocumentSymbol
	for _, s := range p.symbols {
		if s.kind == protocol.Constant {
			continue
		}
		d := kindStr(s.kind)
		if d == "Namespace" {
			d = "Template"
		}
		if s.vardef {
			d += "(def)"
		} else {
			d += "(use)"
		}
		r := p.Range(s.start, s.length)
		y := protocol.DocumentSymbol{
			Name:           s.name,
			Detail:         d,
			Kind:           s.kind,
			Range:          r,
			SelectionRange: r, // or should this be the entire {{...}}?
		}
		ans = append(ans, y)
	}
	return ans, nil
}
