/* Copyright 2018 The Bazel Authors. 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.
*/

package rule

import (
	"fmt"
	"log"
	"strings"

	"github.com/bazelbuild/bazel-gazelle/label"
	bzl "github.com/bazelbuild/buildtools/build"
)

// MapExprStrings applies a function to string sub-expressions within e.
// An expression containing the results with the same structure as e is
// returned.
func MapExprStrings(e bzl.Expr, f func(string) string) bzl.Expr {
	if e == nil {
		return nil
	}
	switch expr := e.(type) {
	case *bzl.StringExpr:
		s := f(expr.Value)
		if s == "" {
			return nil
		}
		ret := *expr
		ret.Value = s
		return &ret

	case *bzl.ListExpr:
		var list []bzl.Expr
		for _, elem := range expr.List {
			elem = MapExprStrings(elem, f)
			if elem != nil {
				list = append(list, elem)
			}
		}
		if len(list) == 0 && len(expr.List) > 0 {
			return nil
		}
		ret := *expr
		ret.List = list
		return &ret

	case *bzl.DictExpr:
		var cases []*bzl.KeyValueExpr
		isEmpty := true
		for _, kv := range expr.List {
			value := MapExprStrings(kv.Value, f)
			if value != nil {
				cases = append(cases, &bzl.KeyValueExpr{Key: kv.Key, Value: value})
				if key, ok := kv.Key.(*bzl.StringExpr); !ok || key.Value != "//conditions:default" {
					isEmpty = false
				}
			}
		}
		if isEmpty {
			return nil
		}
		ret := *expr
		ret.List = cases
		return &ret

	case *bzl.CallExpr:
		if x, ok := expr.X.(*bzl.Ident); !ok || x.Name != "select" || len(expr.List) != 1 {
			log.Panicf("unexpected call expression in generated imports: %#v", e)
		}
		arg := MapExprStrings(expr.List[0], f)
		if arg == nil {
			return nil
		}
		call := *expr
		call.List[0] = arg
		return &call

	case *bzl.BinaryExpr:
		x := MapExprStrings(expr.X, f)
		y := MapExprStrings(expr.Y, f)
		if x == nil {
			return y
		}
		if y == nil {
			return x
		}
		binop := *expr
		binop.X = x
		binop.Y = y
		return &binop

	default:
		return nil
	}
}

// FlattenExpr takes an expression that may have been generated from
// PlatformStrings and returns its values in a flat, sorted, de-duplicated
// list. Comments are accumulated and de-duplicated across duplicate
// expressions. If the expression could not have been generted by
// PlatformStrings, the expression will be returned unmodified.
func FlattenExpr(e bzl.Expr) bzl.Expr {
	ps, err := extractPlatformStringsExprs(e)
	if err != nil {
		return e
	}

	ls := makeListSquasher()
	addElem := func(e bzl.Expr) bool {
		s, ok := e.(*bzl.StringExpr)
		if !ok {
			return false
		}
		ls.add(s)
		return true
	}
	addList := func(e bzl.Expr) bool {
		l, ok := e.(*bzl.ListExpr)
		if !ok {
			return false
		}
		for _, elem := range l.List {
			if !addElem(elem) {
				return false
			}
		}
		return true
	}
	addDict := func(d *bzl.DictExpr) bool {
		for _, kv := range d.List {
			if !addList(kv.Value) {
				return false
			}
		}
		return true
	}

	if ps.generic != nil {
		if !addList(ps.generic) {
			return e
		}
	}
	for _, d := range []*bzl.DictExpr{ps.os, ps.arch, ps.platform} {
		if d == nil {
			continue
		}
		if !addDict(d) {
			return e
		}
	}

	return ls.list()
}

func isScalar(e bzl.Expr) bool {
	switch e.(type) {
	case *bzl.StringExpr, *bzl.LiteralExpr, *bzl.Ident:
		return true
	default:
		return false
	}
}

func dictEntryKeyValue(e bzl.Expr) (string, *bzl.ListExpr, error) {
	kv, ok := e.(*bzl.KeyValueExpr)
	if !ok {
		return "", nil, fmt.Errorf("dict entry was not a key-value pair: %#v", e)
	}
	k, ok := kv.Key.(*bzl.StringExpr)
	if !ok {
		return "", nil, fmt.Errorf("dict key was not string: %#v", kv.Key)
	}
	v, ok := kv.Value.(*bzl.ListExpr)
	if !ok {
		return "", nil, fmt.Errorf("dict value was not list: %#v", kv.Value)
	}
	return k.Value, v, nil
}

func stringValue(e bzl.Expr) string {
	s, ok := e.(*bzl.StringExpr)
	if !ok {
		return ""
	}
	return s.Value
}

// platformStringsExprs is a set of sub-expressions that match the structure
// of package.PlatformStrings. ExprFromValue produces expressions that
// follow this structure for srcs, deps, and other attributes, so this matches
// all non-scalar expressions generated by Gazelle.
//
// The matched expression has the form:
//
// [] + select({}) + select({}) + select({})
//
// The four collections may appear in any order, and some or all of them may
// be omitted (all fields are nil for a nil expression).
type platformStringsExprs struct {
	generic            *bzl.ListExpr
	os, arch, platform *bzl.DictExpr
}

// extractPlatformStringsExprs matches an expression and attempts to extract
// sub-expressions in platformStringsExprs. The sub-expressions can then be
// merged with corresponding sub-expressions. Any field in the returned
// structure may be nil. An error is returned if the given expression does
// not follow the pattern described by platformStringsExprs.
func extractPlatformStringsExprs(expr bzl.Expr) (platformStringsExprs, error) {
	var ps platformStringsExprs
	if expr == nil {
		return ps, nil
	}

	// Break the expression into a sequence of expressions combined with +.
	var parts []bzl.Expr
	for {
		binop, ok := expr.(*bzl.BinaryExpr)
		if !ok {
			parts = append(parts, expr)
			break
		}
		parts = append(parts, binop.Y)
		expr = binop.X
	}

	// Process each part. They may be in any order.
	for _, part := range parts {
		switch part := part.(type) {
		case *bzl.ListExpr:
			if ps.generic != nil {
				return platformStringsExprs{}, fmt.Errorf("expression could not be matched: multiple list expressions")
			}
			ps.generic = part

		case *bzl.CallExpr:
			x, ok := part.X.(*bzl.Ident)
			if !ok || x.Name != "select" || len(part.List) != 1 {
				return platformStringsExprs{}, fmt.Errorf("expression could not be matched: callee other than select or wrong number of args")
			}
			arg, ok := part.List[0].(*bzl.DictExpr)
			if !ok {
				return platformStringsExprs{}, fmt.Errorf("expression could not be matched: select argument not dict")
			}
			var dict **bzl.DictExpr
			for _, kv := range arg.List {
				k, ok := kv.Key.(*bzl.StringExpr)
				if !ok {
					return platformStringsExprs{}, fmt.Errorf("expression could not be matched: dict keys are not all strings")
				}
				if k.Value == "//conditions:default" {
					continue
				}
				key, err := label.Parse(k.Value)
				if err != nil {
					return platformStringsExprs{}, fmt.Errorf("expression could not be matched: dict key is not label: %q", k.Value)
				}
				if KnownOSSet[key.Name] {
					dict = &ps.os
					break
				}
				if KnownArchSet[key.Name] {
					dict = &ps.arch
					break
				}
				osArch := strings.Split(key.Name, "_")
				if len(osArch) != 2 || !KnownOSSet[osArch[0]] || !KnownArchSet[osArch[1]] {
					return platformStringsExprs{}, fmt.Errorf("expression could not be matched: dict key contains unknown platform: %q", k.Value)
				}
				dict = &ps.platform
				break
			}
			if dict == nil {
				// We could not identify the dict because it's empty or only contains
				// //conditions:default. We'll call it the platform dict to avoid
				// dropping it.
				dict = &ps.platform
			}
			if *dict != nil {
				return platformStringsExprs{}, fmt.Errorf("expression could not be matched: multiple selects that are either os-specific, arch-specific, or platform-specific")
			}
			*dict = arg
		}
	}
	return ps, nil
}

// makePlatformStringsExpr constructs a single expression from the
// sub-expressions in ps.
func makePlatformStringsExpr(ps platformStringsExprs) bzl.Expr {
	makeSelect := func(dict *bzl.DictExpr) bzl.Expr {
		return &bzl.CallExpr{
			X:    &bzl.Ident{Name: "select"},
			List: []bzl.Expr{dict},
		}
	}
	forceMultiline := func(e bzl.Expr) {
		switch e := e.(type) {
		case *bzl.ListExpr:
			e.ForceMultiLine = true
		case *bzl.CallExpr:
			e.List[0].(*bzl.DictExpr).ForceMultiLine = true
		}
	}

	var parts []bzl.Expr
	if ps.generic != nil {
		parts = append(parts, ps.generic)
	}
	if ps.os != nil {
		parts = append(parts, makeSelect(ps.os))
	}
	if ps.arch != nil {
		parts = append(parts, makeSelect(ps.arch))
	}
	if ps.platform != nil {
		parts = append(parts, makeSelect(ps.platform))
	}

	if len(parts) == 0 {
		return nil
	}
	if len(parts) == 1 {
		return parts[0]
	}
	expr := parts[0]
	forceMultiline(expr)
	for _, part := range parts[1:] {
		forceMultiline(part)
		expr = &bzl.BinaryExpr{
			Op: "+",
			X:  expr,
			Y:  part,
		}
	}
	return expr
}
