package builder

import (
	"context"
	"os"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"text/template"

	"github.com/pkg/errors"
	"golang.org/x/sync/errgroup"
)

var DebugLog func(string, ...interface{})

func init() {
	DebugLog = func(string, ...interface{}) {}
}

var invalidFilePattern = regexp.MustCompile(`(_test|-packr).go$`)

// Builder scans folders/files looking for `packr.NewBox` and then compiling
// the required static files into `<package-name>-packr.go` files so they can
// be built into Go binaries.
type Builder struct {
	context.Context
	RootPath       string
	IgnoredBoxes   []string
	IgnoredFolders []string
	pkgs           map[string]pkg
	moot           *sync.Mutex
	Compress       bool
}

// Run the builder.
func (b *Builder) Run() error {
	wg := &errgroup.Group{}
	root, err := filepath.EvalSymlinks(b.RootPath)
	if err != nil {
		return errors.WithStack(err)
	}
	err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
		if info == nil {
			return filepath.SkipDir
		}

		base := strings.ToLower(filepath.Base(path))
		if strings.HasPrefix(base, "_") {
			return filepath.SkipDir
		}
		for _, f := range b.IgnoredFolders {
			if strings.ToLower(f) == base {
				return filepath.SkipDir
			}
		}
		if !info.IsDir() {
			wg.Go(func() error {
				return b.process(path)
			})
		}
		return nil
	})
	if err != nil {
		return errors.WithStack(err)
	}
	if err := wg.Wait(); err != nil {
		return errors.WithStack(err)
	}
	return b.dump()
}

func (b *Builder) dump() error {
	for _, p := range b.pkgs {
		name := filepath.Join(p.Dir, "a_"+p.Name+"-packr.go")
		f, err := os.Create(name)
		defer f.Close()
		if err != nil {
			return errors.WithStack(err)
		}
		t, err := template.New("").Parse(tmpl)

		if err != nil {
			return errors.WithStack(err)
		}
		err = t.Execute(f, p)
		if err != nil {
			return errors.WithStack(err)
		}
	}
	return nil
}

func (b *Builder) process(path string) error {
	ext := filepath.Ext(path)
	if ext != ".go" || invalidFilePattern.MatchString(path) {
		return nil
	}

	v := newVisitor(path)
	if err := v.Run(); err != nil {
		return errors.WithStack(err)
	}

	pk := pkg{
		Dir:   filepath.Dir(path),
		Boxes: []box{},
		Name:  v.Package,
	}

	for _, n := range v.Boxes {
		var ignored bool
		for _, i := range b.IgnoredBoxes {
			if n == i {
				// this is an ignored box
				ignored = true
				break
			}
		}
		if ignored {
			continue
		}
		bx := &box{
			Name:     n,
			Files:    []file{},
			compress: b.Compress,
		}
		DebugLog("building box %s\n", bx.Name)
		p := filepath.Join(pk.Dir, bx.Name)
		if err := bx.Walk(p); err != nil {
			return errors.WithStack(err)
		}
		if len(bx.Files) > 0 {
			pk.Boxes = append(pk.Boxes, *bx)
		}
		DebugLog("built box %s with %q\n", bx.Name, bx.Files)
	}

	if len(pk.Boxes) > 0 {
		b.addPkg(pk)
	}
	return nil
}

func (b *Builder) addPkg(p pkg) {
	b.moot.Lock()
	defer b.moot.Unlock()
	if _, ok := b.pkgs[p.Name]; !ok {
		b.pkgs[p.Name] = p
		return
	}
	pp := b.pkgs[p.Name]
	pp.Boxes = append(pp.Boxes, p.Boxes...)
	b.pkgs[p.Name] = pp
}

// New Builder with a given context and path
func New(ctx context.Context, path string) *Builder {
	return &Builder{
		Context:        ctx,
		RootPath:       path,
		IgnoredBoxes:   []string{},
		IgnoredFolders: []string{"vendor", ".git", "node_modules", ".idea"},
		pkgs:           map[string]pkg{},
		moot:           &sync.Mutex{},
	}
}
