package main

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

var exitCode int
var dirs []string

func appendUniq(slice []string, i string) []string {
	for _, ele := range slice {
		if ele == i {
			return slice
		}
	}
	return append(slice, i)
}

// error formats the error to standard error, adding program
// identification and a newline
func errorf(format string, args ...interface{}) {
	fmt.Fprintf(os.Stderr, "verifier: "+format+"\n", args...)
	exitCode = 2
}

type goPackage struct {
	p               *ast.Package
	fs              *token.FileSet
	decl            map[string]ast.Node
	missingcomments map[string]ast.Node
	used            map[string]bool
}

type usedWalker goPackage

// Walks through the AST marking used identifiers.
func (p *usedWalker) Visit(node ast.Node) ast.Visitor {
	// just be stupid and mark all *ast.Ident
	switch n := node.(type) {
	case *ast.Ident:
		p.used[n.Name] = true
	}
	return p
}

type report struct {
	pos  token.Pos
	name string
}
type reports []report

// Len
func (l reports) Len() int { return len(l) }

// Less
func (l reports) Less(i, j int) bool { return l[i].pos < l[j].pos }

// Swap
func (l reports) Swap(i, j int) { l[i], l[j] = l[j], l[i] }

// Visits files for used nodes.
func (p *goPackage) Visit(node ast.Node) ast.Visitor {
	u := usedWalker(*p) // hopefully p fields are references.
	switch n := node.(type) {
	// don't walk whole file, but only:
	case *ast.ValueSpec:
		// - variable initializers
		for _, value := range n.Values {
			ast.Walk(&u, value)
		}
		// variable types.
		if n.Type != nil {
			ast.Walk(&u, n.Type)
		}
	case *ast.BlockStmt:
		// - function bodies
		for _, stmt := range n.List {
			ast.Walk(&u, stmt)
		}
	case *ast.FuncDecl:
		// - function signatures
		ast.Walk(&u, n.Type)
	case *ast.TypeSpec:
		// - type declarations
		ast.Walk(&u, n.Type)
	}
	return p
}

func getAllMinioPkgs(path string, fl os.FileInfo, err error) error {
	if err != nil {
		return err
	}
	if fl.IsDir() {
		// Skip godeps
		if strings.Contains(path, "Godeps") {
			return nil
		}
		dirs = appendUniq(dirs, path)
	}
	return nil
}

func doDir(name string) {
	notests := func(info os.FileInfo) bool {
		if !info.IsDir() && strings.HasSuffix(info.Name(), ".go") &&
			!strings.HasSuffix(info.Name(), "_test.go") {
			return true
		}
		return false
	}
	fs := token.NewFileSet()
	pkgs, err := parser.ParseDir(fs, name, notests, parser.ParseComments|parser.Mode(0))
	if err != nil {
		errorf("%s", err)
		return
	}
	for _, pkg := range pkgs {
		doPackage(fs, pkg)
	}
}

func doDecl(p *goPackage, decl interface{}, cmap ast.CommentMap) bool {
	switch n := decl.(type) {
	case *ast.GenDecl:
		// var, const, types
		for _, spec := range n.Specs {
			switch s := spec.(type) {
			case *ast.ValueSpec:
				// constants and variables.
				for _, name := range s.Names {
					p.decl[name.Name] = n
				}
			case *ast.TypeSpec:
				// type definitions.
				p.decl[s.Name.Name] = n
			}
		}
	case *ast.FuncDecl:
		// if function is 'main', never check
		if n.Name.Name == "main" {
			return true
		}
		// Do not be strict on non-exported functions
		if !ast.IsExported(n.Name.Name) {
			return true
		}
		// Do not be strict for field list functions
		// if n.Recv != nil {
		// continue
		//}
		// Be strict for global functions
		_, ok := cmap[n]
		if ok == false {
			p.missingcomments[n.Name.Name] = n
		}

		// function declarations
		// TODO(remy): do methods
		if n.Recv == nil {
			p.decl[n.Name.Name] = n
		}
	}
	return false
}

func doPackage(fs *token.FileSet, pkg *ast.Package) {
	p := &goPackage{
		p:               pkg,
		fs:              fs,
		decl:            make(map[string]ast.Node),
		missingcomments: make(map[string]ast.Node),
		used:            make(map[string]bool),
	}
	for _, file := range pkg.Files {
		cmap := ast.NewCommentMap(fs, file, file.Comments)
		for _, decl := range file.Decls {
			if doDecl(p, decl, cmap) {
				continue
			}
		}
	}
	// init() is always used
	p.used["init"] = true
	if pkg.Name != "main" {
		// exported names are marked used for non-main packages.
		for name := range p.decl {
			if ast.IsExported(name) {
				p.used[name] = true
			}
		}
	} else {
		// in main programs, main() is called.
		p.used["main"] = true
	}
	for _, file := range pkg.Files {
		// walk file looking for used nodes.
		ast.Walk(p, file)
	}

	// reports.
	reports := reports(nil)
	for name, node := range p.decl {
		if !p.used[name] {
			reports = append(reports, report{node.Pos(), name})
		}
	}
	sort.Sort(reports)
	for _, report := range reports {
		errorf("%s: %s is unused", fs.Position(report.pos), report.name)
	}

	for name, node := range p.missingcomments {
		errorf("%s: comment is missing for 'func %s'", fs.Position(node.Pos()), name)
	}
}

func main() {
	flag.Parse()
	if flag.NArg() == 0 {
		doDir(".")
	} else {
		for _, name := range flag.Args() {
			// Is it a directory?
			if fi, err := os.Stat(name); err == nil && fi.IsDir() {
				err := filepath.Walk(name, getAllMinioPkgs)
				if err != nil {
					errorf(err.Error())
				}
				for _, dir := range dirs {
					doDir(dir)
				}
			} else {
				errorf("not a directory: %s", name)
			}
		}
	}
	os.Exit(exitCode)
}