package main

import (
	"fmt"
	"go/ast"
	"go/build"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"strings"
)

func badArgument(fileset *token.FileSet, p token.Pos) {
	pos := fileset.Position(p)
	filename := pos.Filename
	base, err := os.Getwd()
	if err == nil {
		rpath, perr := filepath.Rel(base, pos.Filename)
		if perr == nil {
			filename = rpath
		}
	}
	msg := fmt.Sprintf("%s:%d: Error: found call to rice.FindBox, "+
		"but argument must be a string literal.\n", filename, pos.Line)
	fmt.Println(msg)
	os.Exit(1)
}

func findBoxes(pkg *build.Package) map[string]bool {
	// create map of boxes to embed
	var boxMap = make(map[string]bool)

	// create one list of files for this package
	filenames := make([]string, 0, len(pkg.GoFiles)+len(pkg.CgoFiles))
	filenames = append(filenames, pkg.GoFiles...)
	filenames = append(filenames, pkg.CgoFiles...)

	// loop over files, search for rice.FindBox(..) calls
	for _, filename := range filenames {
		// find full filepath
		fullpath := filepath.Join(pkg.Dir, filename)
		if strings.HasSuffix(filename, "rice-box.go") {
			// Ignore *.rice-box.go files
			verbosef("skipping file %q\n", fullpath)
			continue
		}
		verbosef("scanning file %q\n", fullpath)

		fset := token.NewFileSet()
		f, err := parser.ParseFile(fset, fullpath, nil, 0)
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}

		var riceIsImported bool
		ricePkgName := "rice"
		for _, imp := range f.Imports {
			if strings.HasSuffix(imp.Path.Value, "go.rice\"") {
				if imp.Name != nil {
					ricePkgName = imp.Name.Name
				}
				riceIsImported = true
				break
			}
		}
		if !riceIsImported {
			// Rice wasn't imported, so we won't find a box.
			continue
		}
		if ricePkgName == "_" {
			// Rice pkg is unnamed, so we won't find a box.
			continue
		}

		// Inspect AST, looking for calls to (Must)?FindBox.
		// First parameter of the func must be a basic literal.
		// Identifiers won't be resolved.
		var nextIdentIsBoxFunc bool
		var nextBasicLitParamIsBoxName bool
		var boxCall token.Pos
		var validVariablesForBoxes = make(map[string]bool)

		ast.Inspect(f, func(node ast.Node) bool {
			if node == nil {
				return false
			}
			switch x := node.(type) {
			// this case fixes the var := func() style assignments, not assignments to vars declared separately from the assignment.
			case *ast.AssignStmt:
				var assign = node.(*ast.AssignStmt)
				name, found := assign.Lhs[0].(*ast.Ident)
				if found {
					composite, first := assign.Rhs[0].(*ast.CompositeLit)
					if first {
						riceSelector, second := composite.Type.(*ast.SelectorExpr)

						if second {
							callCorrect := riceSelector.Sel.Name == "Config"
							packageName, third := riceSelector.X.(*ast.Ident)

							if third && callCorrect && packageName.Name == ricePkgName {
								validVariablesForBoxes[name.Name] = true
								verbosef("\tfound variable, saving to scan for boxes: %q\n", name.Name)
							}
						}
					}
				}
			case *ast.Ident:
				if nextIdentIsBoxFunc || ricePkgName == "." {
					nextIdentIsBoxFunc = false
					if x.Name == "FindBox" || x.Name == "MustFindBox" {
						nextBasicLitParamIsBoxName = true
						boxCall = x.Pos()
					}
				} else {
					if x.Name == ricePkgName || validVariablesForBoxes[x.Name] {
						nextIdentIsBoxFunc = true
					}
				}
			case *ast.BasicLit:
				if nextBasicLitParamIsBoxName {
					if x.Kind == token.STRING {
						nextBasicLitParamIsBoxName = false
						// trim "" or ``
						name := x.Value[1 : len(x.Value)-1]
						boxMap[name] = true
						verbosef("\tfound box %q\n", name)
					} else {
						badArgument(fset, boxCall)
					}
				}

			default:
				if nextIdentIsBoxFunc {
					nextIdentIsBoxFunc = false
				}
				if nextBasicLitParamIsBoxName {
					badArgument(fset, boxCall)
				}
			}
			return true
		})
	}

	return boxMap
}
