// Copyright 2017 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 gps

import (
	"fmt"
	"os"
	"path/filepath"
	"sync/atomic"

	"github.com/golang/dep/gps/pkgtree"
)

// sourceBridge is an adapter to SourceManagers that tailor operations for a
// single solve run.
type sourceBridge interface {
	// sourceBridge includes many methods from the SourceManager interface.
	SourceExists(ProjectIdentifier) (bool, error)
	SyncSourceFor(ProjectIdentifier) error
	RevisionPresentIn(ProjectIdentifier, Revision) (bool, error)
	ListPackages(ProjectIdentifier, Version) (pkgtree.PackageTree, error)
	GetManifestAndLock(ProjectIdentifier, Version, ProjectAnalyzer) (Manifest, Lock, error)
	ExportProject(ProjectIdentifier, Version, string) error
	DeduceProjectRoot(ip string) (ProjectRoot, error)

	listVersions(ProjectIdentifier) ([]Version, error)
	verifyRootDir(path string) error
	vendorCodeExists(ProjectIdentifier) (bool, error)
	breakLock()
}

// bridge is an adapter around a proper SourceManager. It provides localized
// caching that's tailored to the requirements of a particular solve run.
//
// Finally, it provides authoritative version/constraint operations, ensuring
// that any possible approach to a match - even those not literally encoded in
// the inputs - is achieved.
type bridge struct {
	// The underlying, adapted-to SourceManager
	sm SourceManager

	// The solver which we're assisting.
	//
	// The link between solver and bridge is circular, which is typically a bit
	// awkward, but the bridge needs access to so many of the input arguments
	// held by the solver that it ends up being easier and saner to do this.
	s *solver

	// Map of project root name to their available version list. This cache is
	// layered on top of the proper SourceManager's cache; the only difference
	// is that this keeps the versions sorted in the direction required by the
	// current solve run.
	vlists map[ProjectIdentifier][]Version

	// Indicates whether lock breaking has already been run
	lockbroken int32

	// Whether to sort version lists for downgrade.
	down bool

	// The cancellation context provided to the solver. Threading it through the
	// various solver methods is needlessly verbose so long as we maintain the
	// lifetime guarantees that a solver can only be run once.
	// TODO(sdboyer) uncomment this and thread it through SourceManager methods
	//ctx context.Context
}

// mkBridge creates a bridge
func mkBridge(s *solver, sm SourceManager, down bool) *bridge {
	return &bridge{
		sm:     sm,
		s:      s,
		down:   down,
		vlists: make(map[ProjectIdentifier][]Version),
	}
}

func (b *bridge) GetManifestAndLock(id ProjectIdentifier, v Version, an ProjectAnalyzer) (Manifest, Lock, error) {
	if b.s.rd.isRoot(id.ProjectRoot) {
		return b.s.rd.rm, b.s.rd.rl, nil
	}

	b.s.mtr.push("b-gmal")
	m, l, e := b.sm.GetManifestAndLock(id, v, an)
	b.s.mtr.pop()
	return m, l, e
}

func (b *bridge) listVersions(id ProjectIdentifier) ([]Version, error) {
	if vl, exists := b.vlists[id]; exists {
		return vl, nil
	}

	b.s.mtr.push("b-list-versions")
	pvl, err := b.sm.ListVersions(id)
	if err != nil {
		b.s.mtr.pop()
		return nil, err
	}

	vl := hidePair(pvl)
	if b.down {
		SortForDowngrade(vl)
	} else {
		SortForUpgrade(vl)
	}

	b.vlists[id] = vl
	b.s.mtr.pop()
	return vl, nil
}

func (b *bridge) RevisionPresentIn(id ProjectIdentifier, r Revision) (bool, error) {
	b.s.mtr.push("b-rev-present-in")
	i, e := b.sm.RevisionPresentIn(id, r)
	b.s.mtr.pop()
	return i, e
}

func (b *bridge) SourceExists(id ProjectIdentifier) (bool, error) {
	b.s.mtr.push("b-source-exists")
	i, e := b.sm.SourceExists(id)
	b.s.mtr.pop()
	return i, e
}

func (b *bridge) vendorCodeExists(id ProjectIdentifier) (bool, error) {
	fi, err := os.Stat(filepath.Join(b.s.rd.dir, "vendor", string(id.ProjectRoot)))
	if err != nil {
		return false, err
	} else if fi.IsDir() {
		return true, nil
	}

	return false, nil
}

// listPackages lists all the packages contained within the given project at a
// particular version.
//
// The root project is handled separately, as the source manager isn't
// responsible for that code.
func (b *bridge) ListPackages(id ProjectIdentifier, v Version) (pkgtree.PackageTree, error) {
	if b.s.rd.isRoot(id.ProjectRoot) {
		return b.s.rd.rpt, nil
	}

	b.s.mtr.push("b-list-pkgs")
	pt, err := b.sm.ListPackages(id, v)
	b.s.mtr.pop()
	return pt, err
}

func (b *bridge) ExportProject(id ProjectIdentifier, v Version, path string) error {
	panic("bridge should never be used to ExportProject")
}

// verifyRoot ensures that the provided path to the project root is in good
// working condition. This check is made only once, at the beginning of a solve
// run.
func (b *bridge) verifyRootDir(path string) error {
	if fi, err := os.Stat(path); err != nil {
		return badOptsFailure(fmt.Sprintf("could not read project root (%s): %s", path, err))
	} else if !fi.IsDir() {
		return badOptsFailure(fmt.Sprintf("project root (%s) is a file, not a directory", path))
	}

	return nil
}

func (b *bridge) DeduceProjectRoot(ip string) (ProjectRoot, error) {
	b.s.mtr.push("b-deduce-proj-root")
	pr, e := b.sm.DeduceProjectRoot(ip)
	b.s.mtr.pop()
	return pr, e
}

// breakLock is called when the solver has to break a version recorded in the
// lock file. It prefetches all the projects in the solver's lock, so that the
// information is already on hand if/when the solver needs it.
//
// Projects that have already been selected are skipped, as it's generally unlikely that the
// solver will have to backtrack through and fully populate their version queues.
func (b *bridge) breakLock() {
	// No real conceivable circumstance in which multiple calls are made to
	// this, but being that this is the entrance point to a bunch of async work,
	// protect it with an atomic CAS in case things change in the future.
	//
	// We avoid using a sync.Once here, as there's no reason for other callers
	// to block until completion.
	if !atomic.CompareAndSwapInt32(&b.lockbroken, 0, 1) {
		return
	}

	for _, lp := range b.s.rd.rl.Projects() {
		if _, is := b.s.sel.selected(lp.Ident()); !is {
			pi, v := lp.Ident(), lp.Version()
			go func() {
				// Sync first
				b.sm.SyncSourceFor(pi)
				// Preload the package info for the locked version, too, as
				// we're more likely to need that
				b.sm.ListPackages(pi, v)
			}()
		}
	}
}

func (b *bridge) SyncSourceFor(id ProjectIdentifier) error {
	// we don't track metrics here b/c this is often called in its own goroutine
	// by the solver, and the metrics design is for wall time on a single thread
	return b.sm.SyncSourceFor(id)
}
