// Copyright 2017 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.

package androidmk

import (
	"bytes"
	"fmt"
	"strings"
	"text/scanner"

	"android/soong/bpfix/bpfix"

	mkparser "android/soong/androidmk/parser"

	bpparser "github.com/google/blueprint/parser"
)

// TODO: non-expanded variables with expressions

type bpFile struct {
	comments          []*bpparser.CommentGroup
	defs              []bpparser.Definition
	localAssignments  map[string]*bpparser.Property
	globalAssignments map[string]*bpparser.Expression
	variableRenames   map[string]string
	scope             mkparser.Scope
	module            *bpparser.Module

	mkPos scanner.Position // Position of the last handled line in the makefile
	bpPos scanner.Position // Position of the last emitted line to the blueprint file

	inModule bool
}

var invalidVariableStringToReplacement = map[string]string{
	"-": "_dash_",
}

// Fix steps that should only run in the androidmk tool, i.e. should only be applied to
// newly-converted Android.bp files.
var fixSteps = bpfix.FixStepsExtension{
	Name: "androidmk",
	Steps: []bpfix.FixStep{
		{
			Name: "RewriteRuntimeResourceOverlay",
			Fix:  bpfix.RewriteRuntimeResourceOverlay,
		},
	},
}

func init() {
	bpfix.RegisterFixStepExtension(&fixSteps)
}

func (f *bpFile) insertComment(s string) {
	f.comments = append(f.comments, &bpparser.CommentGroup{
		Comments: []*bpparser.Comment{
			&bpparser.Comment{
				Comment: []string{s},
				Slash:   f.bpPos,
			},
		},
	})
	f.bpPos.Offset += len(s)
}

func (f *bpFile) insertExtraComment(s string) {
	f.insertComment(s)
	f.bpPos.Line++
}

// records that the given node failed to be converted and includes an explanatory message
func (f *bpFile) errorf(failedNode mkparser.Node, message string, args ...interface{}) {
	orig := failedNode.Dump()
	message = fmt.Sprintf(message, args...)
	f.addErrorText(fmt.Sprintf("// ANDROIDMK TRANSLATION ERROR: %s", message))

	lines := strings.Split(orig, "\n")
	for _, l := range lines {
		f.insertExtraComment("// " + l)
	}
}

// records that something unexpected occurred
func (f *bpFile) warnf(message string, args ...interface{}) {
	message = fmt.Sprintf(message, args...)
	f.addErrorText(fmt.Sprintf("// ANDROIDMK TRANSLATION WARNING: %s", message))
}

// adds the given error message as-is to the bottom of the (in-progress) file
func (f *bpFile) addErrorText(message string) {
	f.insertExtraComment(message)
}

func (f *bpFile) setMkPos(pos, end scanner.Position) {
	// It is unusual but not forbidden for pos.Line to be smaller than f.mkPos.Line
	// For example:
	//
	// if true                       # this line is emitted 1st
	// if true                       # this line is emitted 2nd
	// some-target: some-file        # this line is emitted 3rd
	//         echo doing something  # this recipe is emitted 6th
	// endif #some comment           # this endif is emitted 4th; this comment is part of the recipe
	//         echo doing more stuff # this is part of the recipe
	// endif                         # this endif is emitted 5th
	//
	// However, if pos.Line < f.mkPos.Line, we treat it as though it were equal
	if pos.Line >= f.mkPos.Line {
		f.bpPos.Line += (pos.Line - f.mkPos.Line)
		f.mkPos = end
	}

}

type conditional struct {
	cond string
	eq   bool
}

func ConvertFile(filename string, buffer *bytes.Buffer) (string, []error) {
	p := mkparser.NewParser(filename, buffer)

	nodes, errs := p.Parse()
	if len(errs) > 0 {
		return "", errs
	}

	file := &bpFile{
		scope:             androidScope(),
		localAssignments:  make(map[string]*bpparser.Property),
		globalAssignments: make(map[string]*bpparser.Expression),
		variableRenames:   make(map[string]string),
	}

	var conds []*conditional
	var assignmentCond *conditional
	var tree *bpparser.File

	for _, node := range nodes {
		file.setMkPos(p.Unpack(node.Pos()), p.Unpack(node.End()))

		switch x := node.(type) {
		case *mkparser.Comment:
			// Split the comment on escaped newlines and then
			// add each chunk separately.
			chunks := strings.Split(x.Comment, "\\\n")
			file.insertComment("//" + chunks[0])
			for i := 1; i < len(chunks); i++ {
				file.bpPos.Line++
				file.insertComment("//" + chunks[i])
			}
		case *mkparser.Assignment:
			handleAssignment(file, x, assignmentCond)
		case *mkparser.Directive:
			switch x.Name {
			case "include", "-include":
				module, ok := mapIncludePath(x.Args.Value(file.scope))
				if !ok {
					file.errorf(x, "unsupported include")
					continue
				}
				switch module {
				case clearVarsPath:
					resetModule(file)
				case includeIgnoredPath:
					// subdirs are already automatically included in Soong
					continue
				default:
					handleModuleConditionals(file, x, conds)
					makeModule(file, module)
				}
			case "ifeq", "ifneq", "ifdef", "ifndef":
				args := x.Args.Dump()
				eq := x.Name == "ifeq" || x.Name == "ifdef"
				if _, ok := conditionalTranslations[args]; ok {
					newCond := conditional{args, eq}
					conds = append(conds, &newCond)
					if file.inModule {
						if assignmentCond == nil {
							assignmentCond = &newCond
						} else {
							file.errorf(x, "unsupported nested conditional in module")
						}
					}
				} else {
					file.errorf(x, "unsupported conditional")
					conds = append(conds, nil)
					continue
				}
			case "else":
				if len(conds) == 0 {
					file.errorf(x, "missing if before else")
					continue
				} else if conds[len(conds)-1] == nil {
					file.errorf(x, "else from unsupported conditional")
					continue
				}
				conds[len(conds)-1].eq = !conds[len(conds)-1].eq
			case "endif":
				if len(conds) == 0 {
					file.errorf(x, "missing if before endif")
					continue
				} else if conds[len(conds)-1] == nil {
					file.errorf(x, "endif from unsupported conditional")
					conds = conds[:len(conds)-1]
				} else {
					if assignmentCond == conds[len(conds)-1] {
						assignmentCond = nil
					}
					conds = conds[:len(conds)-1]
				}
			default:
				file.errorf(x, "unsupported directive")
				continue
			}
		default:
			file.errorf(x, "unsupported line")
		}
	}

	tree = &bpparser.File{
		Defs:     file.defs,
		Comments: file.comments,
	}

	// check for common supported but undesirable structures and clean them up
	fixer := bpfix.NewFixer(tree)
	fixedTree, fixerErr := fixer.Fix(bpfix.NewFixRequest().AddAll())
	if fixerErr != nil {
		errs = append(errs, fixerErr)
	} else {
		tree = fixedTree
	}

	out, err := bpparser.Print(tree)
	if err != nil {
		errs = append(errs, err)
		return "", errs
	}

	return string(out), errs
}

func renameVariableWithInvalidCharacters(name string) string {
	renamed := ""
	for invalid, replacement := range invalidVariableStringToReplacement {
		if strings.Contains(name, invalid) {
			renamed = strings.ReplaceAll(name, invalid, replacement)
		}
	}

	return renamed
}

func invalidVariableStrings() string {
	invalidStrings := make([]string, 0, len(invalidVariableStringToReplacement))
	for s := range invalidVariableStringToReplacement {
		invalidStrings = append(invalidStrings, "\""+s+"\"")
	}
	return strings.Join(invalidStrings, ", ")
}

func handleAssignment(file *bpFile, assignment *mkparser.Assignment, c *conditional) {
	if !assignment.Name.Const() {
		file.errorf(assignment, "unsupported non-const variable name")
		return
	}

	if assignment.Target != nil {
		file.errorf(assignment, "unsupported target assignment")
		return
	}

	name := assignment.Name.Value(nil)
	prefix := ""

	if newName := renameVariableWithInvalidCharacters(name); newName != "" {
		file.warnf("Variable names cannot contain: %s. Renamed \"%s\" to \"%s\"", invalidVariableStrings(), name, newName)
		file.variableRenames[name] = newName
		name = newName
	}

	if strings.HasPrefix(name, "LOCAL_") {
		for _, x := range propertyPrefixes {
			if strings.HasSuffix(name, "_"+x.mk) {
				name = strings.TrimSuffix(name, "_"+x.mk)
				prefix = x.bp
				break
			}
		}

		if c != nil {
			if prefix != "" {
				file.errorf(assignment, "prefix assignment inside conditional, skipping conditional")
			} else {
				var ok bool
				if prefix, ok = conditionalTranslations[c.cond][c.eq]; !ok {
					panic("unknown conditional")
				}
			}
		}
	} else {
		if c != nil {
			eq := "eq"
			if !c.eq {
				eq = "neq"
			}
			file.errorf(assignment, "conditional %s %s on global assignment", eq, c.cond)
		}
	}

	appendVariable := assignment.Type == "+="

	var err error
	if prop, ok := rewriteProperties[name]; ok {
		err = prop(variableAssignmentContext{file, prefix, assignment.Value, appendVariable})
	} else {
		switch {
		case name == "LOCAL_ARM_MODE":
			// This is a hack to get the LOCAL_ARM_MODE value inside
			// of an arch: { arm: {} } block.
			armModeAssign := assignment
			armModeAssign.Name = mkparser.SimpleMakeString("LOCAL_ARM_MODE_HACK_arm", assignment.Name.Pos())
			handleAssignment(file, armModeAssign, c)
		case strings.HasPrefix(name, "LOCAL_"):
			file.errorf(assignment, "unsupported assignment to %s", name)
			return
		default:
			var val bpparser.Expression
			val, err = makeVariableToBlueprint(file, assignment.Value, bpparser.ListType)
			if err == nil {
				err = setVariable(file, appendVariable, prefix, name, val, false)
			}
		}
	}
	if err != nil {
		file.errorf(assignment, err.Error())
	}
}

func handleModuleConditionals(file *bpFile, directive *mkparser.Directive, conds []*conditional) {
	for _, c := range conds {
		if c == nil {
			continue
		}

		if _, ok := conditionalTranslations[c.cond]; !ok {
			panic("unknown conditional " + c.cond)
		}

		disabledPrefix := conditionalTranslations[c.cond][!c.eq]

		// Create a fake assignment with enabled = false
		val, err := makeVariableToBlueprint(file, mkparser.SimpleMakeString("false", mkparser.NoPos), bpparser.BoolType)
		if err == nil {
			err = setVariable(file, false, disabledPrefix, "enabled", val, true)
		}
		if err != nil {
			file.errorf(directive, err.Error())
		}
	}
}

func makeModule(file *bpFile, t string) {
	file.module.Type = t
	file.module.TypePos = file.module.LBracePos
	file.module.RBracePos = file.bpPos
	file.defs = append(file.defs, file.module)
	file.inModule = false
}

func resetModule(file *bpFile) {
	file.module = &bpparser.Module{}
	file.module.LBracePos = file.bpPos
	file.localAssignments = make(map[string]*bpparser.Property)
	file.inModule = true
}

func makeVariableToBlueprint(file *bpFile, val *mkparser.MakeString,
	typ bpparser.Type) (bpparser.Expression, error) {

	var exp bpparser.Expression
	var err error
	switch typ {
	case bpparser.ListType:
		exp, err = makeToListExpression(val, file)
	case bpparser.StringType:
		exp, err = makeToStringExpression(val, file)
	case bpparser.BoolType:
		exp, err = makeToBoolExpression(val, file)
	default:
		panic("unknown type")
	}

	if err != nil {
		return nil, err
	}

	return exp, nil
}

func setVariable(file *bpFile, plusequals bool, prefix, name string, value bpparser.Expression, local bool) error {
	if prefix != "" {
		name = prefix + "." + name
	}

	pos := file.bpPos

	var oldValue *bpparser.Expression
	if local {
		oldProp := file.localAssignments[name]
		if oldProp != nil {
			oldValue = &oldProp.Value
		}
	} else {
		oldValue = file.globalAssignments[name]
	}

	if local {
		if oldValue != nil && plusequals {
			val, err := addValues(*oldValue, value)
			if err != nil {
				return fmt.Errorf("unsupported addition: %s", err.Error())
			}
			val.(*bpparser.Operator).OperatorPos = pos
			*oldValue = val
		} else {
			names := strings.Split(name, ".")
			if file.module == nil {
				file.warnf("No 'include $(CLEAR_VARS)' detected before first assignment; clearing vars now")
				resetModule(file)
			}
			container := &file.module.Properties

			for i, n := range names[:len(names)-1] {
				fqn := strings.Join(names[0:i+1], ".")
				prop := file.localAssignments[fqn]
				if prop == nil {
					prop = &bpparser.Property{
						Name:    n,
						NamePos: pos,
						Value: &bpparser.Map{
							Properties: []*bpparser.Property{},
						},
					}
					file.localAssignments[fqn] = prop
					*container = append(*container, prop)
				}
				container = &prop.Value.(*bpparser.Map).Properties
			}

			prop := &bpparser.Property{
				Name:    names[len(names)-1],
				NamePos: pos,
				Value:   value,
			}
			file.localAssignments[name] = prop
			*container = append(*container, prop)
		}
	} else {
		if oldValue != nil && plusequals {
			a := &bpparser.Assignment{
				Name:      name,
				NamePos:   pos,
				Value:     value,
				OrigValue: value,
				EqualsPos: pos,
				Assigner:  "+=",
			}
			file.defs = append(file.defs, a)
		} else {
			if _, ok := file.globalAssignments[name]; ok {
				return fmt.Errorf("cannot assign a variable multiple times: \"%s\"", name)
			}
			a := &bpparser.Assignment{
				Name:      name,
				NamePos:   pos,
				Value:     value,
				OrigValue: value,
				EqualsPos: pos,
				Assigner:  "=",
			}
			file.globalAssignments[name] = &a.Value
			file.defs = append(file.defs, a)
		}
	}
	return nil
}
