/*
Copyright 2016 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package edit provides high-level auxiliary functions for AST manipulation
// on BUILD files.
package edit

import (
	"fmt"
	"path"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	"github.com/bazelbuild/buildtools/build"
	"github.com/bazelbuild/buildtools/labels"
	"github.com/bazelbuild/buildtools/wspace"
)

var (
	// ShortenLabelsFlag if true converts added labels to short form , e.g. //foo:bar => :bar
	ShortenLabelsFlag = true
	// DeleteWithComments if true a list attribute will be be deleted in ListDelete, even if there is a comment attached to it
	DeleteWithComments = true
)

// InterpretLabelForWorkspaceLocation returns the name of the BUILD file to
// edit, the full package name, and the rule. It takes a workspace-rooted
// directory to use.
func InterpretLabelForWorkspaceLocation(root, target string) (buildFile, repo, pkg, rule string) {
	label := labels.Parse(target)
	repo = label.Repository
	pkg = label.Package
	rule = label.Target
	rootDir, relativePath := wspace.FindWorkspaceRoot(root)
	if repo != "" {
		files, err := wspace.FindRepoBuildFiles(rootDir)
		if err == nil {
			if buildFile, ok := files[repo]; ok {
				return buildFile, repo, pkg, rule
			}
		}
		return "", repo, pkg, rule
	}

	defaultBuildFileName := "BUILD"
	if strings.HasPrefix(target, "//") {
		pkgPath := filepath.Join(rootDir, filepath.FromSlash(pkg))
		if wspace.IsRegularFile(pkgPath) {
			// allow operation on other files like WORKSPACE
			buildFile = pkgPath
			pkg = path.Dir(pkg)
			return
		}
		for _, buildFileName := range BuildFileNames {
			buildFile = filepath.Join(pkgPath, buildFileName)
			if wspace.IsRegularFile(buildFile) {
				return
			}
		}
		buildFile = filepath.Join(pkgPath, defaultBuildFileName)
		return
	}
	if wspace.IsRegularFile(filepath.FromSlash(pkg)) {
		// allow operation on other files like WORKSPACE
		buildFile = pkg
		pkg = filepath.Join(relativePath, filepath.FromSlash(path.Dir(pkg)))
		return
	}

	found := false
	for _, buildFileName := range BuildFileNames {
		buildFile = filepath.Join(pkg, buildFileName)
		if wspace.IsRegularFile(buildFile) {
			found = true
			break
		}
	}
	if !found {
		buildFile = filepath.Join(pkg, defaultBuildFileName)
	}

	pkg = filepath.Join(relativePath, filepath.FromSlash(pkg))
	return
}

// InterpretLabel returns the name of the BUILD file to edit, the full
// package name, and the rule. It uses the pwd for resolving workspace file paths.
func InterpretLabel(target string) (buildFile string, pkg string, rule string) {
	buildFile, _, pkg, rule = InterpretLabelForWorkspaceLocation("", target)
	return buildFile, pkg, rule
}

// InterpretLabelWithRepo returns the name of the BUILD file to edit, repo name,
// the full package name, and the rule. It uses the pwd for resolving workspace
// file paths.
func InterpretLabelWithRepo(target string) (buildFile string, repo string, pkg string, rule string) {
	return InterpretLabelForWorkspaceLocation("", target)
}

// ExprToRule returns a Rule from an Expr.
// The boolean is false iff the Expr is not a function call, or does not have
// the expected kind.
func ExprToRule(expr build.Expr, kind string) (*build.Rule, bool) {
	call, ok := expr.(*build.CallExpr)
	if !ok {
		return nil, false
	}
	k, ok := call.X.(*build.Ident)
	if !ok || k.Name != kind {
		return nil, false
	}
	return &build.Rule{Call: call, ImplicitName: ""}, true
}

// ExistingPackageDeclaration returns the package declaration, or nil if there is none.
func ExistingPackageDeclaration(f *build.File) *build.Rule {
	for _, stmt := range f.Stmt {
		if rule, ok := ExprToRule(stmt, "package"); ok {
			return rule
		}
	}
	return nil
}

// PackageDeclaration returns the package declaration. If it doesn't
// exist, it is created at the top of the BUILD file, after optional
// docstring, comments, and load statements.
func PackageDeclaration(f *build.File) *build.Rule {
	if pkg := ExistingPackageDeclaration(f); pkg != nil {
		return pkg
	}
	insertAfter := -1
	call := &build.CallExpr{X: &build.Ident{Name: "package"}}
	for i, stmt := range f.Stmt {
		switch stmt.(type) {
		case *build.CommentBlock, *build.LoadStmt, *build.StringExpr:
			// Skip docstring, comments, and load statements to
			// find a place to insert the package declaration.
			insertAfter = i
			continue
		case *build.CallExpr:
			// Skip `workspace()` calls which have to be the very first statements
			// of workspace files
			if isWorkspaceCall(stmt) {
				insertAfter = i
				continue
			}
		default:
		}
		break
	}
	var all []build.Expr
	all = append(all, f.Stmt[:insertAfter+1]...)
	all = append(all, call)
	all = append(all, f.Stmt[insertAfter+1:]...)
	f.Stmt = all

	return &build.Rule{Call: call, ImplicitName: ""}
}

// RemoveEmptyPackage removes empty package declarations from the file, i.e.:
//
//	package()
//
// This might appear because of a buildozer transformation (e.g. when removing a package
// attribute). Removing it is required for the file to be valid.
func RemoveEmptyPackage(f *build.File) *build.File {
	var all []build.Expr
	for _, stmt := range f.Stmt {
		if isEmptyPackage(stmt) {
			continue
		}
		all = append(all, stmt)
	}
	return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: f.Type}
}

func isEmptyPackage(expr build.Expr) bool {
	if call, ok := expr.(*build.CallExpr); ok {
		functionName, ok := call.X.(*build.Ident)
		if ok && functionName.Name == "package" && len(call.List) == 0 {
			return true
		}
	}
	return false
}

func isWorkspaceCall(expr build.Expr) bool {
	if call, ok := expr.(*build.CallExpr); ok {
		if ident, ok := call.X.(*build.Ident); ok && ident.Name == "workspace" {
			return true
		}
	}
	return false
}

// RemoveEmptyUseRepoCalls removes empty use repo declarations from the file.
func RemoveEmptyUseRepoCalls(f *build.File) *build.File {
	if f.Type != build.TypeModule {
		return f
	}
	var all []build.Expr
	for _, stmt := range f.Stmt {
		if isEmptyUseRepoCall(stmt) {
			continue
		}
		all = append(all, stmt)
	}
	return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: build.TypeModule}
}

func isEmptyUseRepoCall(expr build.Expr) bool {
	if call, ok := expr.(*build.CallExpr); ok {
		functionName, ok := call.X.(*build.Ident)
		if ok && functionName.Name == "use_repo" && len(call.List) == 1 {
			return true
		}
	}
	return false
}

// InsertAfter inserts an expression after index i.
func InsertAfter(i int, stmt []build.Expr, expr build.Expr) []build.Expr {
	i = i + 1 // index after the element at i
	result := make([]build.Expr, len(stmt)+1)
	copy(result[0:i], stmt[0:i])
	result[i] = expr
	copy(result[i+1:], stmt[i:])
	return result
}

// IndexOfLast finds the index of the last expression of a specific kind.
func IndexOfLast(stmt []build.Expr, Kind string) int {
	lastIndex := -1
	for i, s := range stmt {
		sAsCallExpr, ok := s.(*build.CallExpr)
		if !ok {
			continue
		}
		literal, ok := sAsCallExpr.X.(*build.Ident)
		if ok && literal.Name == Kind {
			lastIndex = i
		}
	}
	return lastIndex
}

// InsertAfterLastOfSameKind inserts an expression after the last expression of the same kind.
func InsertAfterLastOfSameKind(stmt []build.Expr, expr *build.CallExpr) []build.Expr {
	index := IndexOfLast(stmt, expr.X.(*build.Ident).Name)
	if index == -1 {
		return InsertAtEnd(stmt, expr)
	}
	return InsertAfter(index, stmt, expr)
}

// InsertAtEnd inserts an expression at the end of a list, before trailing comments.
func InsertAtEnd(stmt []build.Expr, expr build.Expr) []build.Expr {
	var i int
	for i = len(stmt) - 1; i >= 0; i-- {
		_, ok := stmt[i].(*build.CommentBlock)
		if !ok {
			break
		}
	}
	return InsertAfter(i, stmt, expr)
}

// FindRuleByName returns the rule in the file that has the given name.
// If the name is "__pkg__", it returns the global package declaration.
func FindRuleByName(f *build.File, name string) *build.Rule {
	if name == "__pkg__" {
		return PackageDeclaration(f)
	}
	_, rule := IndexOfRuleByName(f, name)
	return rule
}

// IndexOfRuleByName returns the index (in f.Stmt) of the CallExpr which defines a rule named `name`, or -1 if it doesn't exist.
func IndexOfRuleByName(f *build.File, name string) (int, *build.Rule) {
	linenum := -1
	if strings.HasPrefix(name, "%") {
		// "%<LINENUM>" will match the rule which begins at LINENUM.
		// This is for convenience, "%" is not a valid character in bazel targets.
		if result, err := strconv.Atoi(name[1:]); err == nil {
			linenum = result
		}
	}

	for i, stmt := range f.Stmt {
		call, ok := stmt.(*build.CallExpr)
		if !ok {
			continue
		}
		r := f.Rule(call)
		start, _ := call.X.Span()
		if r.Name() == name || start.Line == linenum {
			return i, r
		}

		// Allow for precisely targeting the package declaration. This
		// helps adding new load() and license() rules
		if name == "__pkg__" {
			if rule, ok := ExprToRule(stmt, "package"); ok {
				return i, rule
			}
		}
	}
	return -1, nil
}

// FindExportedFile returns the first exports_files call which contains the
// file 'name', or nil if not found
func FindExportedFile(f *build.File, name string) *build.Rule {
	for _, r := range f.Rules("exports_files") {
		if len(r.Call.List) == 0 {
			continue
		}
		pkg := "" // Files are not affected by the package name
		if ListFind(r.Call.List[0], name, pkg) != nil {
			return r
		}
	}
	return nil
}

// DeleteRule returns the AST without the specified rule
func DeleteRule(f *build.File, rule *build.Rule) *build.File {
	var all []build.Expr
	for _, stmt := range f.Stmt {
		if stmt == rule.Call {
			continue
		}
		all = append(all, stmt)
	}
	return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: f.Type}
}

// DeleteRuleByName returns the AST without the rules that have the
// given name.
func DeleteRuleByName(f *build.File, name string) *build.File {
	var all []build.Expr
	for _, stmt := range f.Stmt {
		call, ok := stmt.(*build.CallExpr)
		if !ok {
			all = append(all, stmt)
			continue
		}
		r := f.Rule(call)
		if r.Name() != name {
			all = append(all, stmt)
		}
	}
	return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: f.Type}
}

// DeleteRuleByKind removes the rules of the specified kind from the AST.
// Returns an updated copy of f.
func DeleteRuleByKind(f *build.File, kind string) *build.File {
	var all []build.Expr
	for _, stmt := range f.Stmt {
		call, ok := stmt.(*build.CallExpr)
		if !ok {
			all = append(all, stmt)
			continue
		}
		k, ok := call.X.(*build.Ident)
		if !ok || k.Name != kind {
			all = append(all, stmt)
		}
	}
	return &build.File{Path: f.Path, Comments: f.Comments, Stmt: all, Type: f.Type}
}

// AllLists returns all the lists concatenated in an expression.
// For example, in: glob(["*.go"]) + [":rule"]
// the function will return [[":rule"]].
func AllLists(e build.Expr) []*build.ListExpr {
	switch e := e.(type) {
	case *build.ListExpr:
		return []*build.ListExpr{e}
	case *build.BinaryExpr:
		if e.Op == "+" {
			return append(AllLists(e.X), AllLists(e.Y)...)
		}
	}
	return nil
}

// AllSelects returns all the selects concatenated in an expression.
func AllSelects(e build.Expr) []*build.CallExpr {
	switch e := e.(type) {
	case *build.BinaryExpr:
		if e.Op == "+" {
			return append(AllSelects(e.X), AllSelects(e.Y)...)
		}
	case *build.CallExpr:
		if x, ok := e.X.(*build.Ident); ok && x.Name == "select" {
			return []*build.CallExpr{e}
		}
	}
	return nil
}

// allListsFromSelects returns all ListExpr nodes from all select statements
// in an expression
func allListsFromSelects(e build.Expr) []*build.ListExpr {
	var lists []*build.ListExpr

	for _, s := range AllSelects(e) {
		if len(s.List) != 1 {
			return nil
		}
		dict, ok := s.List[0].(*build.DictExpr)
		if !ok {
			return nil
		}
		for _, kv := range dict.List {
			list, ok := kv.Value.(*build.ListExpr)
			if !ok {
				continue
			}
			lists = append(lists, list)
		}
	}
	return lists
}

// allListsIncludingSelects returns all the lists concatenated in an expression
// including lists inside select statements.
// For example, in: glob(["*.go"]) + [":rule"] + select({"foo": [":bar"]})
// the function will return [[":rule", ":bar"]].
func allListsIncludingSelects(e build.Expr) []*build.ListExpr {
	return append(AllLists(e), allListsFromSelects(e)...)
}

// FirstList works in the same way as AllLists, except that it
// returns only one list, or nil.
func FirstList(e build.Expr) *build.ListExpr {
	switch e := e.(type) {
	case *build.ListExpr:
		return e
	case *build.BinaryExpr:
		if e.Op == "+" {
			li := FirstList(e.X)
			if li == nil {
				return FirstList(e.Y)
			}
			return li
		}
	}
	return nil
}

// AllStrings returns all the string literals concatenated in an expression.
// For example, in: "foo" + x + "bar"
// the function will return ["foo", "bar"].
func AllStrings(e build.Expr) []*build.StringExpr {
	switch e := e.(type) {
	case *build.StringExpr:
		return []*build.StringExpr{e}
	case *build.BinaryExpr:
		if e.Op == "+" {
			return append(AllStrings(e.X), AllStrings(e.Y)...)
		}
	}
	return nil
}

// listsFind looks for a string in list expressions
func listsFind(lists []*build.ListExpr, item string, pkg string) *build.StringExpr {
	for _, list := range lists {
		for _, elem := range list.List {
			str, ok := elem.(*build.StringExpr)
			if ok && labels.Equal(str.Value, item, pkg) {
				return str
			}
		}
	}
	return nil
}

// ListFind looks for a string in the list expression (which may be a
// concatenation of lists). It returns the element if it is found. nil
// otherwise.
func ListFind(e build.Expr, item string, pkg string) *build.StringExpr {
	return listsFind(AllLists(e), item, pkg)
}

// listOrSelectFind looks for a string in the list expression (which may be a
// concatenation of lists and select statements). It returns the element
// if it is found. nil otherwise.
func listOrSelectFind(e build.Expr, item string, pkg string) *build.StringExpr {
	return listsFind(allListsIncludingSelects(e), item, pkg)
}

// hasComments returns whether the StringExpr literal has a comment attached to it.
func hasComments(literal *build.StringExpr) bool {
	return len(literal.Before) > 0 || len(literal.Suffix) > 0
}

// ContainsComments returns whether the expr has a comment that includes str.
func ContainsComments(expr build.Expr, str string) bool {
	str = strings.ToLower(str)
	com := expr.Comment()
	comments := append(com.Before, com.Suffix...)
	comments = append(comments, com.After...)
	for _, c := range comments {
		if strings.Contains(strings.ToLower(c.Token), str) {
			return true
		}
	}
	return false
}

// RemoveEmptySelectsAndConcatLists iterates the tree in order to turn
// empty selects into empty lists and adjacent lists are concatenated
func RemoveEmptySelectsAndConcatLists(e build.Expr) build.Expr {
	switch e := e.(type) {
	case *build.BinaryExpr:
		if e.Op == "+" {
			e.X = RemoveEmptySelectsAndConcatLists(e.X)
			e.Y = RemoveEmptySelectsAndConcatLists(e.Y)

			x, xIsList := e.X.(*build.ListExpr)
			y, yIsList := e.Y.(*build.ListExpr)

			if xIsList && yIsList {
				return &build.ListExpr{List: append(x.List, y.List...)}
			}

			if xIsList && len(x.List) == 0 {
				return e.Y
			}

			if yIsList && len(y.List) == 0 {
				return e.X
			}
		}
	case *build.CallExpr:
		if x, ok := e.X.(*build.Ident); ok && x.Name == "select" {
			if len(e.List) == 0 {
				return &build.ListExpr{List: []build.Expr{}}
			}

			if dict, ok := e.List[0].(*build.DictExpr); ok {
				for _, keyVal := range dict.List {
					val, ok := keyVal.Value.(*build.ListExpr)
					if !ok || len(val.List) > 0 {
						return e
					}
				}

				return &build.ListExpr{List: []build.Expr{}}
			}
		}
	}

	return e
}

// ComputeIntersection returns the intersection of the two lists given as parameters;
// if the containing elements are not build.StringExpr, the result will be nil.
func ComputeIntersection(list1, list2 []build.Expr) []build.Expr {
	if list1 == nil || list2 == nil {
		return nil
	}

	if len(list2) == 0 {
		return []build.Expr{}
	}

	i := 0
	for j, common := range list1 {
		if common, ok := common.(*build.StringExpr); ok {
			found := false
			for _, elem := range list2 {
				if str, ok := elem.(*build.StringExpr); ok {
					if str.Value == common.Value {
						found = true
						break
					}
				} else {
					return nil
				}
			}

			if found {
				list1[i] = list1[j]
				i++
			}
		} else {
			return nil
		}
	}
	return list1[:i]
}

// SelectListsIntersection returns the intersection of the lists of strings inside
// the dictionary argument of the select expression given as a parameter
func SelectListsIntersection(sel *build.CallExpr, pkg string) (intersection []build.Expr) {
	if len(sel.List) == 0 || len(sel.List) > 1 {
		return nil
	}

	dict, ok := sel.List[0].(*build.DictExpr)
	if !ok || len(dict.List) == 0 {
		return nil
	}

	if val, ok := dict.List[0].Value.(*build.ListExpr); ok {
		intersection = make([]build.Expr, len(val.List))
		copy(intersection, val.List)
	}

	for _, keyVal := range dict.List[1:] {
		val, ok := keyVal.Value.(*build.ListExpr)
		if !ok {
			return nil
		}
		intersection = ComputeIntersection(intersection, val.List)
		if len(intersection) == 0 {
			return intersection
		}
	}

	return intersection
}

// ResolveAttr extracts common elements of the lists inside select dictionaries
// and adds them at attribute level rather than select level, as well as turns
// empty selects into empty lists and concatenates adjacent lists
func ResolveAttr(r *build.Rule, attr, pkg string) {
	var toExtract []build.Expr

	e := r.Attr(attr)
	if e == nil {
		return
	}

	for _, sel := range AllSelects(e) {
		intersection := SelectListsIntersection(sel, pkg)
		if intersection != nil {
			toExtract = append(toExtract, intersection...)
		}
	}

	for _, common := range toExtract {
		e = AddValueToList(e, pkg, common, false) // this will also remove them from selects
	}

	r.SetAttr(attr, RemoveEmptySelectsAndConcatLists(e))
}

// SelectDelete removes the item from all the lists which are values
// in the dictionary of every select
func SelectDelete(e build.Expr, item, pkg string, deleted **build.StringExpr) {
	for _, sel := range AllSelects(e) {
		if len(sel.List) == 0 {
			continue
		}

		if dict, ok := sel.List[0].(*build.DictExpr); ok {
			for _, keyVal := range dict.List {
				if val, ok := keyVal.Value.(*build.ListExpr); ok {
					RemoveFromList(val, item, pkg, deleted)
				}
			}
		}
	}
}

// RemoveFromList removes one element from a ListExpr and stores
// the deleted StringExpr at the address pointed by the last parameter
func RemoveFromList(li *build.ListExpr, item, pkg string, deleted **build.StringExpr) {
	var all []build.Expr
	for _, elem := range li.List {
		if str, ok := elem.(*build.StringExpr); ok {
			if labels.Equal(str.Value, item, pkg) && (DeleteWithComments || !hasComments(str)) {
				if deleted != nil {
					*deleted = str
				}

				continue
			}
		}
		all = append(all, elem)
	}
	li.List = all
}

// ListDelete deletes the item from a list expression in e and returns
// the StringExpr deleted, or nil otherwise.
func ListDelete(e build.Expr, item, pkg string) (deleted *build.StringExpr) {
	if unquoted, _, err := build.Unquote(item); err == nil {
		item = unquoted
	}
	deleted = nil
	for _, li := range AllLists(e) {
		RemoveFromList(li, item, pkg, &deleted)
	}

	SelectDelete(e, item, pkg, &deleted)

	return deleted
}

// ListAttributeDelete deletes string item from list attribute attr, deletes attr if empty,
// and returns the StringExpr deleted, or nil otherwise.
func ListAttributeDelete(rule *build.Rule, attr, item, pkg string) *build.StringExpr {
	deleted := ListDelete(rule.Attr(attr), item, pkg)
	if deleted != nil {
		if listExpr, ok := rule.Attr(attr).(*build.ListExpr); ok && len(listExpr.List) == 0 {
			rule.DelAttr(attr)
		}
	}
	return deleted
}

// ListReplace replaces old with value in all lists in e and returns a Boolean
// to indicate whether the replacement was successful.
func ListReplace(e build.Expr, old, value, pkg string) bool {
	replaced := false
	for _, li := range allListsIncludingSelects(e) {
		for k, elem := range li.List {
			str, ok := elem.(*build.StringExpr)
			if !ok || !labels.Equal(str.Value, old, pkg) {
				continue
			}
			li.List[k] = &build.StringExpr{Value: ShortenLabel(value, pkg), Comments: *elem.Comment()}
			replaced = true
		}
	}
	return replaced
}

// ListSubstitute replaces strings matching a regular expression in all lists
// in e and returns a Boolean to indicate whether the replacement was
// successful.
func ListSubstitute(e build.Expr, oldRegexp *regexp.Regexp, newTemplate string) bool {
	substituted := false
	for _, li := range allListsIncludingSelects(e) {
		for k, elem := range li.List {
			str, ok := elem.(*build.StringExpr)
			if !ok {
				continue
			}
			newValue, ok := stringSubstitute(str.Value, oldRegexp, newTemplate)
			if ok {
				li.List[k] = &build.StringExpr{Value: newValue, Comments: *elem.Comment()}
				substituted = true
			}
		}
	}
	return substituted
}

func stringSubstitute(oldValue string, oldRegexp *regexp.Regexp, newTemplate string) (string, bool) {
	match := oldRegexp.FindStringSubmatchIndex(oldValue)
	if match == nil {
		return oldValue, false
	}
	newValue := string(oldRegexp.ExpandString(nil, newTemplate, oldValue, match))
	if match[0] > 0 {
		newValue = oldValue[:match[0]] + newValue
	}
	if match[1] < len(oldValue) {
		newValue = newValue + oldValue[match[1]:]
	}
	return newValue, true
}

// isExprLessThan compares two Expr statements. Currently, only labels are supported.
func isExprLessThan(x1, x2 build.Expr) bool {
	str1, ok1 := x1.(*build.StringExpr)
	str2, ok2 := x2.(*build.StringExpr)
	if ok1 != ok2 {
		return ok2
	}
	if ok1 && ok2 {
		// Labels starting with // are put at the end.
		pre1 := strings.HasPrefix(str1.Value, "//")
		pre2 := strings.HasPrefix(str2.Value, "//")
		if pre1 != pre2 {
			return pre2
		}
		return str1.Value < str2.Value
	}
	return false
}

func sortedInsert(list []build.Expr, item build.Expr) []build.Expr {
	i := 0
	for ; i < len(list); i++ {
		if isExprLessThan(item, list[i]) {
			break
		}
	}
	res := make([]build.Expr, 0, len(list)+1)
	res = append(res, list[:i]...)
	res = append(res, item)
	res = append(res, list[i:]...)
	return res
}

// attributeMustNotBeSorted returns true if the list in the attribute cannot be
// sorted. For some attributes, it makes sense to try to do a sorted insert
// (e.g. deps), even when buildifier will not sort it for conservative reasons.
// For a few attributes, sorting will never make sense.
func attributeMustNotBeSorted(rule, attr string) bool {
	// TODO(bazel-team): Come up with a more complete list.
	return attr == "args"
}

// getVariable returns the binary expression that assignes a variable to expr, if expr is
// an identifier of a variable that vars contains a mapping for.
func getVariable(expr build.Expr, vars *map[string]*build.AssignExpr) (varAssignment *build.AssignExpr) {
	if vars == nil {
		return nil
	}

	if literal, ok := expr.(*build.Ident); ok {
		if varAssignment = (*vars)[literal.Name]; varAssignment != nil {
			return varAssignment
		}
	}
	return nil
}

// AddValueToList adds a value to a list. If the expression is
// not a list, a list with a single element is appended to the original
// expression.
func AddValueToList(oldList build.Expr, pkg string, item build.Expr, sorted bool) build.Expr {
	if oldList == nil {
		return &build.ListExpr{List: []build.Expr{item}}
	}

	str, ok := item.(*build.StringExpr)
	if ok {
		if ListFind(oldList, str.Value, pkg) != nil {
			// The value is already in the list.
			return oldList
		}
		SelectDelete(oldList, str.Value, pkg, nil)
	}

	li := FirstList(oldList)
	if li != nil {
		if sorted {
			li.List = sortedInsert(li.List, item)
		} else {
			li.List = append(li.List, item)
		}
		return oldList
	}
	list := &build.ListExpr{List: []build.Expr{item}}
	concat := &build.BinaryExpr{Op: "+", X: oldList, Y: list}
	return concat
}

// AddValueToListAttribute adds the given item to the list attribute identified by name and pkg.
func AddValueToListAttribute(r *build.Rule, name string, pkg string, item build.Expr, vars *map[string]*build.AssignExpr) {
	old := r.Attr(name)
	sorted := !attributeMustNotBeSorted(r.Kind(), name)
	if varAssignment := getVariable(old, vars); varAssignment != nil {
		varAssignment.RHS = AddValueToList(varAssignment.RHS, pkg, item, sorted)
	} else {
		r.SetAttr(name, AddValueToList(old, pkg, item, sorted))
	}
}

// MoveAllListAttributeValues moves all values from list attribute oldAttr to newAttr,
// and deletes oldAttr.
func MoveAllListAttributeValues(rule *build.Rule, oldAttr, newAttr, pkg string, vars *map[string]*build.AssignExpr) error {
	if rule.Attr(oldAttr) == nil {
		return fmt.Errorf("no attribute %s found in %s", oldAttr, rule.Name())
	}
	if rule.Attr(newAttr) == nil {
		RenameAttribute(rule, oldAttr, newAttr)
		return nil
	}
	if listExpr, ok := rule.Attr(oldAttr).(*build.ListExpr); ok {
		for _, val := range listExpr.List {
			AddValueToListAttribute(rule, newAttr, pkg, val, vars)
		}
		rule.DelAttr(oldAttr)
		return nil
	}
	return fmt.Errorf("%s already exists and %s is not a simple list", newAttr, oldAttr)
}

// DictionarySet looks for the key in the dictionary expression. If value is not nil,
// it replaces the current value with it. In all cases, it returns the current value.
func DictionarySet(dict *build.DictExpr, key string, value build.Expr) build.Expr {
	for _, kv := range dict.List {
		if k, ok := kv.Key.(*build.StringExpr); ok && k.Value == key {
			if value != nil {
				kv.Value = value
			}
			return kv.Value
		}
	}
	if value != nil {
		kv := &build.KeyValueExpr{Key: &build.StringExpr{Value: key}, Value: value}
		dict.List = append(dict.List, kv)
	}
	return nil
}

// DictionaryGet looks for the key in the dictionary expression, and returns the
// current value. If it is unset, it returns nil.
func DictionaryGet(dict *build.DictExpr, key string) build.Expr {
	for _, kv := range dict.List {
		if k, ok := kv.Key.(*build.StringExpr); ok && k.Value == key {
			return kv.Value
		}
	}
	return nil
}

// DictionaryDelete looks for the key in the dictionary expression. If the key exists,
// it removes the key-value pair and returns it. Otherwise it returns nil.
func DictionaryDelete(dict *build.DictExpr, key string) (deleted build.Expr) {
	if unquoted, _, err := build.Unquote(key); err == nil {
		key = unquoted
	}
	deleted = nil
	var all []*build.KeyValueExpr
	for _, kv := range dict.List {
		if k, ok := kv.Key.(*build.StringExpr); ok {
			if k.Value == key {
				deleted = kv
			} else {
				all = append(all, kv)
			}
		}
	}
	dict.List = all
	return deleted
}

// RenameAttribute renames an attribute in a rule.
func RenameAttribute(r *build.Rule, oldName, newName string) error {
	if r.Attr(newName) != nil {
		return fmt.Errorf("attribute %s already exists in rule %s", newName, r.Name())
	}
	for _, kv := range r.Call.List {
		as, ok := kv.(*build.AssignExpr)
		if !ok {
			continue
		}
		k, ok := as.LHS.(*build.Ident)
		if !ok || k.Name != oldName {
			continue
		}
		k.Name = newName
		return nil
	}
	return fmt.Errorf("no attribute %s found in rule %s", oldName, r.Name())
}

// EditFunction is a wrapper around build.Edit. The callback is called only on
// functions 'name'.
func EditFunction(v build.Expr, name string, f func(x *build.CallExpr, stk []build.Expr) build.Expr) build.Expr {
	return build.Edit(v, func(expr build.Expr, stk []build.Expr) build.Expr {
		call, ok := expr.(*build.CallExpr)
		if !ok {
			return nil
		}
		fct, ok := call.X.(*build.Ident)
		if !ok || fct.Name != name {
			return nil
		}
		return f(call, stk)
	})
}

// UsedSymbols returns the set of symbols used in the BUILD file (variables, function names).
func UsedSymbols(stmt build.Expr) map[string]bool {
	symbols := make(map[string]bool)
	build.Walk(stmt, func(expr build.Expr, stack []build.Expr) {
		// Don't traverse inside load statements
		if len(stack) > 0 {
			if _, ok := stack[len(stack)-1].(*build.LoadStmt); ok {
				return
			}
		}

		literal, ok := expr.(*build.Ident)
		if !ok {
			return
		}
		// Check if we are on the left-side of an assignment
		for _, e := range stack {
			if as, ok := e.(*build.AssignExpr); ok {
				if as.LHS == expr {
					return
				}
			}
		}
		symbols[literal.Name] = true
	})
	return symbols
}

// UsedTypes returns the set of types used in the BUILD file (variables, function names).
func UsedTypes(stmt build.Expr) map[string]bool {
	symbols := make(map[string]bool)
	build.Walk(stmt, func(expr build.Expr, stack []build.Expr) {
		// Don't traverse inside load statements
		if len(stack) > 0 {
			if _, ok := stack[len(stack)-1].(*build.LoadStmt); ok {
				return
			}
		}
		// Types can only be found in method declarations and
		switch expr := expr.(type) {
		case *build.TypedIdent:
			for _, t := range build.GetTypes(expr) {
				symbols[t] = true
			}
		case *build.DefStmt:
			for _, t := range build.GetTypes(expr) {
				symbols[t] = true
			}
		}
	})
	return symbols
}

// NewLoad creates a new LoadStmt node
func NewLoad(location string, from, to []string) *build.LoadStmt {
	load := &build.LoadStmt{
		Module: &build.StringExpr{
			Value: location,
		},
		ForceCompact: true,
	}
	for i := range from {
		load.From = append(load.From, &build.Ident{Name: from[i]})
		load.To = append(load.To, &build.Ident{Name: to[i]})
	}
	return load
}

// AppendToLoad appends symbols to an existing load statement
// Returns true if the statement was acually edited (if the required symbols haven't been
// loaded yet)
func AppendToLoad(load *build.LoadStmt, from, to []string) bool {
	symbolsToLoad := make(map[string]string)
	for i, s := range to {
		symbolsToLoad[s] = from[i]
	}
	for _, ident := range load.To {
		delete(symbolsToLoad, ident.Name) // Already loaded.
	}

	if len(symbolsToLoad) == 0 {
		return false
	}

	// Append the remaining loads to the load statement.
	for s := range symbolsToLoad {
		load.From = append(load.From, &build.Ident{Name: symbolsToLoad[s]})
		load.To = append(load.To, &build.Ident{Name: s})
	}
	return true
}

// appendLoad tries to find an existing load location and append symbols to it.
func appendLoad(stmts []build.Expr, location string, from, to []string) bool {
	symbolsToLoad := make(map[string]string)
	for i, s := range to {
		symbolsToLoad[s] = from[i]
	}
	var lastLoad *build.LoadStmt
	for _, s := range stmts {
		load, ok := s.(*build.LoadStmt)
		if !ok {
			continue
		}
		if load.Module.Value != location {
			continue // Loads a different file.
		}
		for _, ident := range load.To {
			delete(symbolsToLoad, ident.Name) // Already loaded.
		}
		// Remember the last insert location, but potentially remove more symbols
		// that are already loaded in other subsequent calls.
		lastLoad = load
	}
	if lastLoad == nil {
		return false
	}

	// Append the remaining loads to the last load location.
	from = []string{}
	to = []string{}
	for t, f := range symbolsToLoad {
		from = append(from, f)
		to = append(to, t)
	}
	AppendToLoad(lastLoad, from, to)
	return true
}

// InsertLoad inserts a load statement at the top of the list of statements.
// The load statement is constructed using a string location and two slices of from- and to-symbols.
// The function panics if the slices aren't of the same length. Symbols that are already loaded
// from the given filepath are ignored. If stmts already contains a load for the
// location in arguments, appends the symbols to load to it.
func InsertLoad(stmts []build.Expr, location string, from, to []string) []build.Expr {
	if len(from) != len(to) {
		panic(fmt.Errorf("length mismatch: %v (from) and %v (to)", len(from), len(to)))
	}

	if appendLoad(stmts, location, from, to) {
		return stmts
	}

	load := NewLoad(location, from, to)

	insertAfter := -1
	for i, stmt := range stmts {
		_, isComment := stmt.(*build.CommentBlock)
		_, isString := stmt.(*build.StringExpr)
		isDocString := isString && i == 0

		// We add synthetic package() calls when called with :__pkg__ labels.
		// We strip them out later when saving the fixed file because they're
		// not valid.
		// Pretend they're not there and skip past them while we look for
		// possible workspace calls.
		// isSyntheticPackage := isEmptyPackage(stmt)

		// If we're editing a WORKSPACE file, bazel requires that the workspace
		// declaration must be the very first expression in the WORKSPACE file,
		// before any loads.
		isWorkspaceCallStmt := isWorkspaceCall(stmt)

		if isComment || isDocString || isWorkspaceCallStmt {
			insertAfter = i
			continue
		}
		break
	}

	var all []build.Expr
	all = append(all, stmts[:insertAfter+1]...)
	all = append(all, load)
	all = append(all, stmts[insertAfter+1:]...)
	return all

}

// ReplaceLoad removes load statements for passed to-symbols and replaces them with a new
// load at the top of the list of statements. The new load statement is constructed using
// a string location and two slices of from- and to-symbols. If stmts already contains a
// load for the location in arguments, appends the symbols to load to it.
// The function panics if the slices aren't of the same lentgh.
func ReplaceLoad(stmts []build.Expr, location string, from, to []string) []build.Expr {
	if len(from) != len(to) {
		panic(fmt.Errorf("length mismatch: %v (from) and %v (to)", len(from), len(to)))
	}

	toSymbols := make(map[string]bool, len(to))
	for _, name := range to {
		toSymbols[name] = true
	}

	// 1. Remove loads that will be replaced.
	var all []build.Expr
	for _, stmt := range stmts {
		load, ok := stmt.(*build.LoadStmt)
		if !ok {
			all = append(all, stmt)
			continue
		}

		for i, to := range load.To {
			if toSymbols[to.Name] {
				if i < len(load.From)-1 {
					load.From = append(load.From[:i], load.From[i+1:]...)
					load.To = append(load.To[:i], load.To[i+1:]...)
				} else {
					load.From = load.From[:i]
					load.To = load.To[:i]
				}
			}
		}

		if len(load.To) > 0 {
			all = append(all, load)
		}
	}

	// 2. Insert new loads.
	return InsertLoad(all, location, from, to)
}

// ParseLabel parses a Blaze label (eg. //devtools/buildozer:rule), and returns
// the repo name ("" for the main repo), package (with leading slashes trimmed)
// and rule name (e.g. ["", "devtools/buildozer", "rule"]).
// Deprecated; use `labels.Parse` instead
func ParseLabel(target string) (string, string, string) {
	label := labels.Parse(target)
	return label.Repository, label.Package, label.Target
}

// ShortenLabel rewrites labels to use the canonical form (the form
// recommended by build-style).
// Doesn't do anything if `--shorten_label=false` flag is provided.
// Use `labels.Shorten` to shorten labels unconditionally.
func ShortenLabel(label, pkg string) string {
	if !ShortenLabelsFlag {
		return label
	}
	return labels.Shorten(label, pkg)
}

// LabelsEqual returns true if label1 and label2 are equal.
// Deprecated; use `labels.Equal` instead
func LabelsEqual(label1, label2, pkg string) bool {
	return labels.Equal(label1, label2, pkg)
}
