// Copyright 2018 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 lsp

import (
	"bytes"
	"context"
	"fmt"
	"strings"

	"golang.org/x/tools/internal/event"
	"golang.org/x/tools/internal/lsp/debug/tag"
	"golang.org/x/tools/internal/lsp/protocol"
	"golang.org/x/tools/internal/lsp/source"
	"golang.org/x/tools/internal/lsp/source/completion"
	"golang.org/x/tools/internal/lsp/template"
	"golang.org/x/tools/internal/span"
)

func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) {
	snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
	defer release()
	if !ok {
		return nil, err
	}
	var candidates []completion.CompletionItem
	var surrounding *completion.Selection
	switch fh.Kind() {
	case source.Go:
		candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context)
	case source.Mod:
		candidates, surrounding = nil, nil
	case source.Tmpl:
		candidates, surrounding, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context)
	}
	if err != nil {
		event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position))
	}
	if candidates == nil {
		return &protocol.CompletionList{
			IsIncomplete: true,
			Items:        []protocol.CompletionItem{},
		}, nil
	}
	// We might need to adjust the position to account for the prefix.
	rng, err := surrounding.Range()
	if err != nil {
		return nil, err
	}

	// internal/span treats end of file as the beginning of the next line, even
	// when it's not newline-terminated. We correct for that behaviour here if
	// end of file is not newline-terminated. See golang/go#41029.
	src, err := fh.Read()
	if err != nil {
		return nil, err
	}
	numLines := len(bytes.Split(src, []byte("\n")))
	tok := snapshot.FileSet().File(surrounding.Start())
	eof := tok.Pos(tok.Size())

	// For newline-terminated files, the line count reported by go/token should
	// be lower than the actual number of lines we see when splitting by \n. If
	// they're the same, the file isn't newline-terminated.
	if tok.Size() > 0 && tok.LineCount() == numLines {
		// Get the span for the last character in the file-1. This is
		// technically incorrect, but will get span to point to the previous
		// line.
		spn, err := span.NewRange(snapshot.FileSet(), eof-1, eof-1).Span()
		if err != nil {
			return nil, err
		}
		m := &protocol.ColumnMapper{
			URI:       fh.URI(),
			Converter: span.NewContentConverter(fh.URI().Filename(), src),
			Content:   src,
		}
		eofRng, err := m.Range(spn)
		if err != nil {
			return nil, err
		}
		// Instead of using the computed range, correct for our earlier
		// position adjustment by adding 1 to the column, not the line number.
		pos := protocol.Position{
			Line:      eofRng.Start.Line,
			Character: eofRng.Start.Character + 1,
		}
		if surrounding.Start() >= eof {
			rng.Start = pos
		}
		if surrounding.End() >= eof {
			rng.End = pos
		}
	}

	// When using deep completions/fuzzy matching, report results as incomplete so
	// client fetches updated completions after every key stroke.
	options := snapshot.View().Options()
	incompleteResults := options.DeepCompletion || options.Matcher == source.Fuzzy

	items := toProtocolCompletionItems(candidates, rng, options)

	return &protocol.CompletionList{
		IsIncomplete: incompleteResults,
		Items:        items,
	}, nil
}

func toProtocolCompletionItems(candidates []completion.CompletionItem, rng protocol.Range, options *source.Options) []protocol.CompletionItem {
	var (
		items                  = make([]protocol.CompletionItem, 0, len(candidates))
		numDeepCompletionsSeen int
	)
	for i, candidate := range candidates {
		// Limit the number of deep completions to not overwhelm the user in cases
		// with dozens of deep completion matches.
		if candidate.Depth > 0 {
			if !options.DeepCompletion {
				continue
			}
			if numDeepCompletionsSeen >= completion.MaxDeepCompletions {
				continue
			}
			numDeepCompletionsSeen++
		}
		insertText := candidate.InsertText
		if options.InsertTextFormat == protocol.SnippetTextFormat {
			insertText = candidate.Snippet()
		}

		// This can happen if the client has snippets disabled but the
		// candidate only supports snippet insertion.
		if insertText == "" {
			continue
		}

		item := protocol.CompletionItem{
			Label:  candidate.Label,
			Detail: candidate.Detail,
			Kind:   candidate.Kind,
			TextEdit: &protocol.TextEdit{
				NewText: insertText,
				Range:   rng,
			},
			InsertTextFormat:    options.InsertTextFormat,
			AdditionalTextEdits: candidate.AdditionalTextEdits,
			// This is a hack so that the client sorts completion results in the order
			// according to their score. This can be removed upon the resolution of
			// https://github.com/Microsoft/language-server-protocol/issues/348.
			SortText: fmt.Sprintf("%05d", i),

			// Trim operators (VSCode doesn't like weird characters in
			// filterText).
			FilterText: strings.TrimLeft(candidate.InsertText, "&*"),

			Preselect:     i == 0,
			Documentation: candidate.Documentation,
			Tags:          candidate.Tags,
			Deprecated:    candidate.Deprecated,
		}
		items = append(items, item)
	}
	return items
}
