package codegen

import (
	"errors"
	"go/ast"
	"go/token"
	"strconv"
)

// FileGen is a utility for generating/patching golang file ASTs.
type FileGen struct {
	File           *ast.File
	idents         map[string]bool
	codegenPkgPath string
	pkgImportMap   map[string]*importInfo
}

// NewFileGen creates a new FileGen instance from a file AST with the provided package path.
func NewFileGen(file *ast.File, codegenPkgPath string) (*FileGen, error) {
	g := &FileGen{
		File:           file,
		idents:         map[string]bool{},
		codegenPkgPath: codegenPkgPath,
		pkgImportMap:   map[string]*importInfo{},
	}

	// add all go keywords to reserved idents
	for i := token.Token(0); i <= token.TILDE; i++ {
		name := i.String()
		if token.IsKeyword(name) {
			g.idents[name] = true
		}
	}

	// add all top-level decl idents
	for _, decl := range file.Decls {
		switch decl := decl.(type) {
		case *ast.FuncDecl:
			g.idents[decl.Name.Name] = true
		case *ast.GenDecl:
			for _, spec := range decl.Specs {
				switch specType := spec.(type) {
				case *ast.TypeSpec:
					g.idents[specType.Name.Name] = true
				case *ast.ValueSpec:
					for _, name := range specType.Names {
						g.idents[name.Name] = true
					}
				}
			}
		}
	}

	for _, spec := range file.Imports {
		pkgPath, err := strconv.Unquote(spec.Path.Value)
		if err != nil {
			return nil, err
		}

		var info *importInfo
		if spec.Name != nil {
			name := spec.Name.Name
			if name == "." {
				return nil, errors.New(". package imports are not allowed")
			}

			info = &importInfo{importPrefix: name, ImportSpec: spec}
		} else {
			prefix := defaultPkgPrefix(pkgPath)
			info = &importInfo{importPrefix: prefix, ImportSpec: spec}
		}
		g.pkgImportMap[pkgPath] = info
		g.idents[info.importPrefix] = true
	}

	return g, nil
}

// PatchFuncDecl returns a FuncGen instance for the function declaration with the given name or returns nil.
func (g *FileGen) PatchFuncDecl(name string) *FuncGen {
	for _, decl := range g.File.Decls {
		funcDecl, ok := decl.(*ast.FuncDecl)
		if ok {
			if funcDecl.Name.Name == name {
				return newFuncGen(g, funcDecl)
			}
		}
	}
	return nil
}
