/*
Copyright 2016 Google Inc. All Rights Reserved.

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

    http://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.
*/

// Rule-level API for inspecting and modifying a build.File syntax tree.

package build

import (
	"strings"
	"path/filepath"
)

// A Rule represents a single BUILD rule.
type Rule struct {
	Call         *CallExpr
	ImplicitName string // The name which should be used if the name attribute is not set. See the comment on File.implicitRuleName.
}

func (f *File) Rule(call *CallExpr) *Rule {
	r := &Rule{call, ""}
	if r.AttrString("name") == "" {
		r.ImplicitName = f.implicitRuleName()
	}
	return r
}

// Rules returns the rules in the file of the given kind (such as "go_library").
// If kind == "", Rules returns all rules in the file.
func (f *File) Rules(kind string) []*Rule {
	var all []*Rule

	for _, stmt := range f.Stmt {
		call, ok := stmt.(*CallExpr)
		if !ok {
			continue
		}
		rule := f.Rule(call)
		if kind != "" && rule.Kind() != kind {
			continue
		}
		all = append(all, rule)
	}

	return all
}

// RuleAt returns the rule in the file that starts at the specified line, or null if no such rule.
func (f *File) RuleAt(linenum int) *Rule {

	for _, stmt := range f.Stmt {
		call, ok := stmt.(*CallExpr)
		if !ok {
			continue
		}
		start, end := call.X.Span()
		if start.Line <= linenum && linenum <= end.Line {
			return f.Rule(call)
		}
	}
	return nil
}

// DelRules removes rules with the given kind and name from the file.
// An empty kind matches all kinds; an empty name matches all names.
// It returns the number of rules that were deleted.
func (f *File) DelRules(kind, name string) int {
	var i int
	for _, stmt := range f.Stmt {
		if call, ok := stmt.(*CallExpr); ok {
			r := f.Rule(call)
			if (kind == "" || r.Kind() == kind) &&
				(name == "" || r.Name() == name) {
				continue
			}
		}
		f.Stmt[i] = stmt
		i++
	}
	n := len(f.Stmt) - i
	f.Stmt = f.Stmt[:i]
	return n
}

// If a build file contains exactly one unnamed rule, and no rules in the file explicitly have the
// same name as the name of the directory the build file is in, we treat the unnamed rule as if it
// had the name of the directory containing the BUILD file.
// This is following a convention used in the Pants build system to cut down on boilerplate.
func (f *File) implicitRuleName() string {
	// We disallow empty names in the top-level BUILD files.
	dir := filepath.Dir(f.Path)
	if dir == "." {
		return ""
	}
	sawAnonymousRule := false
	possibleImplicitName := filepath.Base(dir)

	for _, stmt := range f.Stmt {
		call, ok := stmt.(*CallExpr)
		if !ok {
			continue
		}
		temp := &Rule{call, ""}
		if temp.AttrString("name") == possibleImplicitName {
			// A target explicitly has the name of the dir, so no implicit targets are allowed.
			return ""
		}
		if temp.Kind() != "" && temp.AttrString("name") == "" {
			if sawAnonymousRule {
				return ""
			}
			sawAnonymousRule = true
		}
	}
	if sawAnonymousRule {
		return possibleImplicitName
	}
	return ""
}

// Kind returns the rule's kind (such as "go_library").
// The kind of the rule may be given by a literal or it may be a sequence of dot expressions that
// begins with a literal, if the call expression does not conform to either of these forms, an
// empty string will be returned
func (r *Rule) Kind() string {
	var names []string
	expr := r.Call.X
	for {
		x, ok := expr.(*DotExpr)
		if !ok {
			break
		}
		names = append(names, x.Name)
		expr = x.X
	}
	x, ok := expr.(*LiteralExpr)
	if !ok {
		return ""
	}
	names = append(names, x.Token)
	// Reverse the elements since the deepest expression contains the leading literal
	for l, r := 0, len(names)-1; l < r; l, r = l+1, r-1 {
		names[l], names[r] = names[r], names[l]
	}
	return strings.Join(names, ".")
}

// SetKind changes rule's kind (such as "go_library").
func (r *Rule) SetKind(kind string) {
	names := strings.Split(kind, ".")
	var expr Expr
	expr = &LiteralExpr{Token: names[0]}
	for _, name := range names[1:] {
		expr = &DotExpr{X: expr, Name: name}
	}
	r.Call.X = expr
}

// Name returns the rule's target name.
// If the rule has no explicit target name, Name returns the implicit name if there is one, else the empty string.
func (r *Rule) Name() string {
	explicitName := r.AttrString("name")
	if explicitName == "" {
		return r.ImplicitName
	}
	return explicitName
}

// AttrKeys returns the keys of all the rule's attributes.
func (r *Rule) AttrKeys() []string {
	var keys []string
	for _, expr := range r.Call.List {
		if binExpr, ok := expr.(*BinaryExpr); ok && binExpr.Op == "=" {
			if keyExpr, ok := binExpr.X.(*LiteralExpr); ok {
				keys = append(keys, keyExpr.Token)
			}
		}
	}
	return keys
}

// AttrDefn returns the BinaryExpr defining the rule's attribute with the given key.
// That is, the result is a *BinaryExpr with Op == "=".
// If the rule has no such attribute, AttrDefn returns nil.
func (r *Rule) AttrDefn(key string) *BinaryExpr {
	for _, kv := range r.Call.List {
		as, ok := kv.(*BinaryExpr)
		if !ok || as.Op != "=" {
			continue
		}
		k, ok := as.X.(*LiteralExpr)
		if !ok || k.Token != key {
			continue
		}
		return as
	}
	return nil
}

// Attr returns the value of the rule's attribute with the given key
// (such as "name" or "deps").
// If the rule has no such attribute, Attr returns nil.
func (r *Rule) Attr(key string) Expr {
	as := r.AttrDefn(key)
	if as == nil {
		return nil
	}
	return as.Y
}

// DelAttr deletes the rule's attribute with the named key.
// It returns the old value of the attribute, or nil if the attribute was not found.
func (r *Rule) DelAttr(key string) Expr {
	list := r.Call.List
	for i, kv := range list {
		as, ok := kv.(*BinaryExpr)
		if !ok || as.Op != "=" {
			continue
		}
		k, ok := as.X.(*LiteralExpr)
		if !ok || k.Token != key {
			continue
		}
		copy(list[i:], list[i+1:])
		r.Call.List = list[:len(list)-1]
		return as.Y
	}
	return nil
}

// SetAttr sets the rule's attribute with the given key to value.
// If the rule has no attribute with the key, SetAttr appends
// one to the end of the rule's attribute list.
func (r *Rule) SetAttr(key string, val Expr) {
	as := r.AttrDefn(key)
	if as != nil {
		as.Y = val
		return
	}

	r.Call.List = append(r.Call.List,
		&BinaryExpr{
			X:  &LiteralExpr{Token: key},
			Op: "=",
			Y:  val,
		},
	)
}

// AttrLiteral returns the literal form of the rule's attribute
// with the given key (such as "cc_api_version"), only when
// that value is an identifier or number.
// If the rule has no such attribute or the attribute is not an identifier or number,
// AttrLiteral returns "".
func (r *Rule) AttrLiteral(key string) string {
	lit, ok := r.Attr(key).(*LiteralExpr)
	if !ok {
		return ""
	}
	return lit.Token
}

// AttrString returns the value of the rule's attribute
// with the given key (such as "name"), as a string.
// If the rule has no such attribute or the attribute has a non-string value,
// Attr returns the empty string.
func (r *Rule) AttrString(key string) string {
	str, ok := r.Attr(key).(*StringExpr)
	if !ok {
		return ""
	}
	return str.Value
}

// AttrStrings returns the value of the rule's attribute
// with the given key (such as "srcs"), as a []string.
// If the rule has no such attribute or the attribute is not
// a list of strings, AttrStrings returns a nil slice.
func (r *Rule) AttrStrings(key string) []string {
	return Strings(r.Attr(key))
}

// Strings returns expr as a []string.
// If expr is not a list of string literals,
// Strings returns a nil slice instead.
// If expr is an empty list of string literals,
// returns a non-nil empty slice.
// (this allows differentiating between these two cases)
func Strings(expr Expr) []string {
	list, ok := expr.(*ListExpr)
	if !ok {
		return nil
	}
	all := []string{} // not nil
	for _, l := range list.List {
		str, ok := l.(*StringExpr)
		if !ok {
			return nil
		}
		all = append(all, str.Value)
	}
	return all
}
