package builder

import (
	"go/build"
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"sync"

	"golang.org/x/tools/go/buildutil"
)

var (
	parallelGoCount      = runtime.NumCPU()
	parallelCompileCount = runtime.NumCPU()
)

type Job struct {
	Gobin string

	RunFmt bool
	RunVet bool
}

func (j *Job) Run() {
	err := j.doit()
	if err != nil {
		log.Printf("doit err=%q", err)
	}
}

func (j *Job) doit() error {
	var findCtxt, ctxt build.Context

	for _, ctxt := range [...]*build.Context{&findCtxt, &ctxt} {
		ctxt.GOARCH = "amd64"
		ctxt.GOOS = "linux"
		ctxt.GOROOT = runtime.GOROOT()
		ctxt.GOPATH = build.Default.GOPATH
		ctxt.Compiler = "gc"

		ctxt.ReleaseTags = build.Default.ReleaseTags

		ctxt.CgoEnabled = build.Default.CgoEnabled

		ctxt.BuildTags = []string{}
	}

	pwd, err := os.Getwd()
	if err != nil {
		log.Fatalf("os.Getwd err=%q", err)
	}
	ctxt.GOPATH = strings.Join([]string{
		filepath.Join(pwd, "Godeps", "_workspace"),
		ctxt.GOPATH,
	}, string(filepath.ListSeparator))

	s := &goEnv{
		find: &findCtxt,
		ctxt: &ctxt,
	}

	all, mains, err := s.findPackages()
	if err != nil {
		log.Fatalf("findPackages err=%q", err)
	}
	log.Printf("findPackages all %v", all)
	log.Printf("findPackages mains %v", mains)

	if j.RunFmt {
		err := j.vet()
		if err != nil {
			log.Fatalf("vet err=%q", err)
		}
	}

	trim := ""
	err = s.buildMains(filepath.Join(pwd, j.Gobin), trim)
	if err != nil {
		return err
	}
	err = s.buildTests(filepath.Join(pwd, j.Gobin), trim)
	if err != nil {
		return err
	}

	log.Printf("done")

	return nil
}

func (j *Job) vet() error {
	args := []string{"go", "vet", "./..."}
	cmd := exec.Command(args[0], args[1:]...)
	cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
	err := cmd.Run()
	return err
}

type goEnv struct {
	find *build.Context
	ctxt *build.Context
}

func (env *goEnv) findPackages() (all, mains []string, err error) {
	var (
		mu       sync.Mutex
		pkgNames []string
	)

	ctxt := env.find

	buildutil.ForEachPackage(ctxt, func(importPath string, err error) {
		if err != nil {
			return
		}
		defer mu.Unlock()
		mu.Lock()
		pkgNames = append(pkgNames, importPath)
	})

	sort.Strings(pkgNames)

	var pkgs []*build.Package
	for _, pkgName := range pkgNames {
		pkg, err := ctxt.Import(pkgName, "", build.ImportComment)
		if err != nil {
			switch err.(type) {
			case *build.NoGoError:
				continue
			default:
				return nil, nil, err
			}
		}
		pkgs = append(pkgs, pkg)
		switch {
		case pkg.Goroot:
		case strings.Contains(pkg.Root, "/Godeps/"):
		case strings.Contains(pkg.Root, "/vendor/"):
		default:
			all = append(all, pkgName)
			if pkg.IsCommand() {
				mains = append(mains, pkgName)
			}
		}
	}

	return all, mains, nil
}

func (env *goEnv) buildMains(outPrefix string, trim string) error {
	_, mains, err := env.findPackages()
	if err != nil {
		return err
	}

	err = parallel(func(pkgName string) error {
		err := (&gocmd{
			ctxt:      env.ctxt,
			outPrefix: filepath.Join(outPrefix, ".bin"),
			linkDir:   outPrefix,
			trim:      trim,
		}).run([]string{"go", "build"}, pkgName, flagSymlink)
		if err != nil {
			log.Printf("buildBinary err=%q", err)
		}
		return err
	}, mains, parallelGoCount)

	return err
}

func (env *goEnv) buildTests(outPrefix string, trim string) error {
	all, _, err := env.findPackages()
	if err != nil {
		return err
	}

	err = parallel(func(pkgName string) error {
		err := (&gocmd{
			ctxt:      env.ctxt,
			outPrefix: filepath.Join(outPrefix, ".test"),
			trim:      trim,
		}).run([]string{"go", "test", "-c"}, pkgName, 0)
		if err != nil {
			log.Printf("buildTest err=%q", err)
		}
		return err
	}, all, parallelGoCount)

	return err
}

func parallel(fn func(string) error, jobs []string, n int) error {
	sem := make(chan struct{}, n)
	errs := make(chan error, 1)
	var wg sync.WaitGroup
	for _, job := range jobs {
		wg.Add(1)
		go func(job string) {
			defer wg.Done()
			sem <- struct{}{}
			defer func() {
				<-sem
			}()

			err := fn(job)
			if err != nil {
				select {
				case errs <- err:
				default:
				}
			}
		}(job)
	}

	wg.Wait()
	close(errs)
	err := <-errs

	return err
}
