// 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 (
	"bytes"
	"fmt"
	"go/ast"
	"go/format"
	"go/token"
	"strconv"
	"strings"

	"golang.org/x/tools/internal/lsp/diff"
	"golang.org/x/tools/internal/span"
)

// Taken and then modified from golang.org/x/tools/go/ast/astutil.
//
// We currently choose to create our own version of AddNamedImport for the following reasons:
// 	1. 	We do not want to edit the current ast. This is so that we can use the same ast
// 		to get the changes from multiple distinct modifications.
//  2.	We need the changes that *only* affect the import declarations, because the edits
// 		are not allowed to overlap with the position in the source that is being edited.
//		astutil.AddNamedImport makes it hard to determine what changes need to be made
//		to the source document from the ast, as astutil.AddNamedImport includes a merging pass.

// addNamedImport adds the import with the given name and path to the file f, if absent.
// If name is not empty, it is used to rename the import.
//
// For example, calling
//	addNamedImport(fset, f, "pathpkg", "path")
// adds
//	import pathpkg "path"
//
// addNamedImport only returns edits that affect the import declarations.
func addNamedImport(fset *token.FileSet, f *ast.File, name, path string) (edits []diff.TextEdit, err error) {
	if alreadyImportsNamed(f, name, path) {
		return nil, nil
	}

	newImport := &ast.ImportSpec{
		Path: &ast.BasicLit{
			Kind:  token.STRING,
			Value: strconv.Quote(path),
		},
	}
	if name != "" {
		newImport.Name = &ast.Ident{Name: name}
	}

	// Find an import decl to add to.
	// The goal is to find an existing import
	// whose import path has the longest shared
	// prefix with path.
	var (
		bestMatch  = -1         // length of longest shared prefix
		lastImport = -1         // index in f.Decls of the file's final import decl
		impDecl    *ast.GenDecl // import decl containing the best match
		impIndex   = -1         // spec index in impDecl containing the best match

		isThirdPartyPath = isThirdParty(path)
	)
	for i, decl := range f.Decls {
		gen, ok := decl.(*ast.GenDecl)
		if ok && gen.Tok == token.IMPORT {
			lastImport = i
			// Do not add to import "C", to avoid disrupting the
			// association with its doc comment, breaking cgo.
			if declImports(gen, "C") {
				continue
			}

			// Do not add to single imports.
			if !gen.Lparen.IsValid() {
				continue
			}

			// Match an empty import decl if that's all that is available.
			if len(gen.Specs) == 0 && bestMatch == -1 {
				impDecl = gen
			}

			// Compute longest shared prefix with imports in this group and find best
			// matched import spec.
			// 1. Always prefer import spec with longest shared prefix.
			// 2. While match length is 0,
			// - for stdlib package: prefer first import spec.
			// - for third party package: prefer first third party import spec.
			// We cannot use last import spec as best match for third party package
			// because grouped imports are usually placed last by goimports -local
			// flag.
			// See issue #19190.
			seenAnyThirdParty := false
			for j, spec := range gen.Specs {
				impspec := spec.(*ast.ImportSpec)
				p := importPath(impspec)
				n := matchLen(p, path)
				if n > bestMatch || (bestMatch == 0 && !seenAnyThirdParty && isThirdPartyPath) {
					bestMatch = n
					impDecl = gen
					impIndex = j
				}
				seenAnyThirdParty = seenAnyThirdParty || isThirdParty(p)
			}
		}
	}

	var insertPos token.Pos
	var newText string
	// If no import decl found, add one after the last import.
	if impDecl == nil {
		// Add an import decl after the last import.
		impDecl = &ast.GenDecl{
			Tok: token.IMPORT,
		}
		impDecl.Specs = append(impDecl.Specs, newImport)

		if lastImport >= 0 {
			insertPos = f.Decls[lastImport].End()
		} else {
			// There are no existing imports.
			// Our new import goes after the package declaration.
			insertPos = f.Name.End()
		}

		// Print the whole import declaration.
		newText = fmt.Sprintf("\n\nimport %s", printImportSpec(fset, newImport))
	} else {
		// Insert new import at insertAt.
		insertAt := 0
		if impIndex >= 0 {
			// insert after the found import
			insertAt = impIndex + 1
		}

		insertPos = impDecl.Lparen + 1 // insert after the parenthesis
		if len(impDecl.Specs) > 0 {
			insertPos = impDecl.Specs[0].Pos() // insert at the beginning
		}
		if insertAt > 0 {
			// If there is a comment after an existing import, preserve the comment
			// position by adding the new import after the comment.
			if spec, ok := impDecl.Specs[insertAt-1].(*ast.ImportSpec); ok && spec.Comment != nil {
				insertPos = spec.Comment.End()
			} else {
				// Assign same position as the previous import,
				// so that the sorter sees it as being in the same block.
				insertPos = impDecl.Specs[insertAt-1].End()
			}
		}

		// Print this import declaration.
		newText = fmt.Sprintf("\n\t%s", printImportSpec(fset, newImport))
	}

	// If we didn't find a valid insert position, return no edits.
	if !insertPos.IsValid() {
		return edits, nil
	}

	// Make sure that we are printed after any comments that start on the same line.
	file := fset.File(insertPos)
	pkgLine := file.Line(insertPos)
	for _, c := range f.Comments {
		if file.Line(c.Pos()) > pkgLine {
			break
		}
		if c.End() > insertPos {
			insertPos = c.End()
		}
	}

	rng := span.NewRange(fset, insertPos, insertPos)
	spn, err := rng.Span()
	if err != nil {
		return nil, err
	}

	edits = append(edits, diff.TextEdit{
		Span:    spn,
		NewText: newText,
	})
	return edits, nil
}

func printImportSpec(fset *token.FileSet, spec *ast.ImportSpec) string {
	var buf bytes.Buffer
	format.Node(&buf, fset, spec)
	return buf.String()
}

func isThirdParty(importPath string) bool {
	// Third party package import path usually contains "." (".com", ".org", ...)
	// This logic is taken from golang.org/x/tools/imports package.
	return strings.Contains(importPath, ".")
}

// alreadyImports reports whether f has an import with the specified path.
func alreadyImports(f *ast.File, path string) bool {
	for _, s := range f.Imports {
		if importPath(s) == path {
			return true
		}
	}
	return false
}

// alreadyImportsNamed reports whether f has an import with the specified name
// and path.
func alreadyImportsNamed(f *ast.File, name, path string) bool {
	for _, s := range f.Imports {
		if importName(s) == name && importPath(s) == path {
			return true
		}
	}
	return false
}

// importName returns the name of s,
// or "" if the import is not named.
func importName(s *ast.ImportSpec) string {
	if s.Name == nil {
		return ""
	}
	return s.Name.Name
}

// importPath returns the unquoted import path of s,
// or "" if the path is not properly quoted.
func importPath(s *ast.ImportSpec) string {
	t, err := strconv.Unquote(s.Path.Value)
	if err != nil {
		return ""
	}
	return t
}

// declImports reports whether gen contains an import of path.
func declImports(gen *ast.GenDecl, path string) bool {
	if gen.Tok != token.IMPORT {
		return false
	}
	for _, spec := range gen.Specs {
		impspec := spec.(*ast.ImportSpec)
		if importPath(impspec) == path {
			return true
		}
	}
	return false
}

// matchLen returns the length of the longest path segment prefix shared by x and y.
func matchLen(x, y string) int {
	n := 0
	for i := 0; i < len(x) && i < len(y) && x[i] == y[i]; i++ {
		if x[i] == '/' {
			n++
		}
	}
	return n
}
