// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"context"
	"io/ioutil"
	"log"

	"github.com/golang/dep"
	"github.com/golang/dep/gps"
	fb "github.com/golang/dep/internal/feedback"
	"github.com/golang/dep/internal/importers"
	"golang.org/x/sync/errgroup"
)

// rootAnalyzer supplies manifest/lock data from both dep and external tool's
// configuration files.
// * When used on the root project, it imports only from external tools.
// * When used by the solver for dependencies, it first looks for dep config,
//   then external tools.
type rootAnalyzer struct {
	skipTools  bool
	ctx        *dep.Ctx
	sm         gps.SourceManager
	directDeps map[gps.ProjectRoot]bool
}

func newRootAnalyzer(skipTools bool, ctx *dep.Ctx, directDeps map[gps.ProjectRoot]bool, sm gps.SourceManager) *rootAnalyzer {
	return &rootAnalyzer{
		skipTools:  skipTools,
		ctx:        ctx,
		sm:         sm,
		directDeps: directDeps,
	}
}

func (a *rootAnalyzer) InitializeRootManifestAndLock(dir string, pr gps.ProjectRoot) (rootM *dep.Manifest, rootL *dep.Lock, err error) {
	if !a.skipTools {
		rootM, rootL = a.importManifestAndLock(dir, pr, false)
	}

	if rootM == nil {
		rootM = dep.NewManifest()

		// Since we didn't find anything to import, dep's cache is empty.
		// We are prefetching dependencies and logging so that the subsequent solve step
		// doesn't spend a long time retrieving dependencies without feedback for the user.
		if err := a.cacheDeps(pr); err != nil {
			return nil, nil, err
		}
	}
	if rootL == nil {
		rootL = &dep.Lock{}
	}

	return
}

func (a *rootAnalyzer) cacheDeps(pr gps.ProjectRoot) error {
	logger := a.ctx.Err
	g, _ := errgroup.WithContext(context.TODO())
	concurrency := 4

	syncDep := func(pr gps.ProjectRoot, sm gps.SourceManager) error {
		if err := sm.SyncSourceFor(gps.ProjectIdentifier{ProjectRoot: pr}); err != nil {
			logger.Printf("Unable to cache %s - %s", pr, err)
			return err
		}
		return nil
	}

	deps := make(chan gps.ProjectRoot)

	for i := 0; i < concurrency; i++ {
		g.Go(func() error {
			for d := range deps {
				err := syncDep(gps.ProjectRoot(d), a.sm)
				if err != nil {
					return err
				}
			}
			return nil
		})
	}

	g.Go(func() error {
		defer close(deps)
		for pr := range a.directDeps {
			logger.Printf("Caching package %q", pr)
			deps <- pr
		}
		return nil
	})

	if err := g.Wait(); err != nil {
		return err
	}
	logger.Printf("Successfully cached all deps.")
	return nil
}

func (a *rootAnalyzer) importManifestAndLock(dir string, pr gps.ProjectRoot, suppressLogs bool) (*dep.Manifest, *dep.Lock) {
	logger := a.ctx.Err
	if suppressLogs {
		logger = log.New(ioutil.Discard, "", 0)
	}

	for _, i := range importers.BuildAll(logger, a.ctx.Verbose, a.sm) {
		if i.HasDepMetadata(dir) {
			a.ctx.Err.Printf("Importing configuration from %s. These are only initial constraints, and are further refined during the solve process.", i.Name())
			m, l, err := i.Import(dir, pr)
			if err != nil {
				a.ctx.Err.Printf(
					"Warning: Encountered an unrecoverable error while trying to import %s config from %q: %s",
					i.Name(), dir, err,
				)
				break
			}
			a.removeTransitiveDependencies(m)
			return m, l
		}
	}

	var emptyManifest = dep.NewManifest()

	return emptyManifest, nil
}

func (a *rootAnalyzer) removeTransitiveDependencies(m *dep.Manifest) {
	for pr := range m.Constraints {
		if _, isDirect := a.directDeps[pr]; !isDirect {
			delete(m.Constraints, pr)
		}
	}
}

// DeriveManifestAndLock evaluates a dependency for existing dependency manager
// configuration (ours or external) and passes any configuration found back
// to the solver.
func (a *rootAnalyzer) DeriveManifestAndLock(dir string, pr gps.ProjectRoot) (gps.Manifest, gps.Lock, error) {
	// Ignore other tools if we find dep configuration
	var depAnalyzer dep.Analyzer
	if depAnalyzer.HasDepMetadata(dir) {
		return depAnalyzer.DeriveManifestAndLock(dir, pr)
	}

	if !a.skipTools {
		// The assignment back to an interface prevents interface-based nil checks from failing later
		var manifest gps.Manifest = gps.SimpleManifest{}
		var lock gps.Lock
		im, il := a.importManifestAndLock(dir, pr, true)
		if im != nil {
			manifest = im
		}
		if il != nil {
			lock = il
		}
		return manifest, lock, nil
	}

	return gps.SimpleManifest{}, nil, nil
}

func (a *rootAnalyzer) FinalizeRootManifestAndLock(m *dep.Manifest, l *dep.Lock, ol dep.Lock) {
	// Iterate through the new projects in solved lock and add them to manifest
	// if they are direct deps and log feedback for all the new projects.
	diff := fb.DiffLocks(&ol, l)
	bi := fb.NewBrokenImportFeedback(diff)
	bi.LogFeedback(a.ctx.Err)
	for _, y := range l.Projects() {
		var f *fb.ConstraintFeedback
		pr := y.Ident().ProjectRoot
		// New constraints: in new lock and dir dep but not in manifest
		if _, ok := a.directDeps[pr]; ok {
			if _, ok := m.Constraints[pr]; !ok {
				pp := getProjectPropertiesFromVersion(y.Version())
				if pp.Constraint != nil {
					m.Constraints[pr] = pp
					pc := gps.ProjectConstraint{Ident: y.Ident(), Constraint: pp.Constraint}
					f = fb.NewConstraintFeedback(pc, fb.DepTypeDirect)
					f.LogFeedback(a.ctx.Err)
				}
				f = fb.NewLockedProjectFeedback(y, fb.DepTypeDirect)
				f.LogFeedback(a.ctx.Err)
			}
		} else {
			// New locked projects: in new lock but not in old lock
			newProject := true
			for _, opl := range ol.Projects() {
				if pr == opl.Ident().ProjectRoot {
					newProject = false
				}
			}
			if newProject {
				f = fb.NewLockedProjectFeedback(y, fb.DepTypeTransitive)
				f.LogFeedback(a.ctx.Err)
			}
		}
	}
}

// Info provides metadata on the analyzer algorithm used during solve.
func (a *rootAnalyzer) Info() gps.ProjectAnalyzerInfo {
	return gps.ProjectAnalyzerInfo{
		Name:    "dep",
		Version: 1,
	}
}
