/* Copyright 2018 The Bazel Authors. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package resolve

import (
	"log"

	"github.com/bazelbuild/bazel-gazelle/config"
	"github.com/bazelbuild/bazel-gazelle/label"
	"github.com/bazelbuild/bazel-gazelle/repo"
	"github.com/bazelbuild/bazel-gazelle/rule"
)

// ImportSpec describes a library to be imported. Imp is an import string for
// the library. Lang is the language in which the import string appears (this
// should match Resolver.Name).
type ImportSpec struct {
	Lang, Imp string
}

// Resolver is an interface that language extensions can implement to resolve
// dependencies in rules they generate.
type Resolver interface {
	// Name returns the name of the language. This should be a prefix of the
	// kinds of rules generated by the language, e.g., "go" for the Go extension
	// since it generates "go_library" rules.
	Name() string

	// Imports returns a list of ImportSpecs that can be used to import the rule
	// r. This is used to populate RuleIndex.
	//
	// If nil is returned, the rule will not be indexed. If any non-nil slice is
	// returned, including an empty slice, the rule will be indexed.
	Imports(c *config.Config, r *rule.Rule, f *rule.File) []ImportSpec

	// Embeds returns a list of labels of rules that the given rule embeds. If
	// a rule is embedded by another importable rule of the same language, only
	// the embedding rule will be indexed. The embedding rule will inherit
	// the imports of the embedded rule.
	Embeds(r *rule.Rule, from label.Label) []label.Label

	// Resolve translates imported libraries for a given rule into Bazel
	// dependencies. Information about imported libraries is returned for each
	// rule generated by language.GenerateRules in
	// language.GenerateResult.Imports. Resolve generates a "deps" attribute (or
	// the appropriate language-specific equivalent) for each import according to
	// language-specific rules and heuristics.
	Resolve(c *config.Config, ix *RuleIndex, rc *repo.RemoteCache, r *rule.Rule, imports interface{}, from label.Label)
}

// CrossResolver is an interface that language extensions can implement to provide
// custom dependency resolution logic for other languages.
type CrossResolver interface {
	// CrossResolve attempts to resolve an import string to a rule for languages
	// other than the implementing extension. lang is the langauge of the rule
	// with the dependency.
	CrossResolve(c *config.Config, ix *RuleIndex, imp ImportSpec, lang string) []FindResult
}

// RuleIndex is a table of rules in a workspace, indexed by label and by
// import path. Used by Resolver to map import paths to labels.
type RuleIndex struct {
	mrslv          func(r *rule.Rule, pkgRel string) Resolver
	crossResolvers []CrossResolver

	// The underlying state of rules. All indexing should be reproducible from this.
	rules []*ruleRecord

	// If indexing of rules has occurred already
	indexed bool

	// Rules indexed by label.
	// Computed from `rules` when indexing.
	labelMap map[label.Label]*ruleRecord

	// Imports specs mapping to records producing those.
	// Computed from `rules` when indexing.
	importMap map[ImportSpec][]*ruleRecord

	// Whether another rule of the same language embeds this rule.
	// Embedded rules should not be indexed.
	// Computed from `rules` when indexing.
	embedded map[label.Label]struct{}

	// The transitive closure of labels embedded within rules (as determined by the Embeds method).
	// This only includes rules in the same language (i.e., it includes a go_library embedding
	// a go_proto_library, but not a go_proto_library embedding a proto_library).
	// Computed from `rules` when indexing.
	embeds map[label.Label][]label.Label

	// The transitive closure of all imports produced by each label.
	// This includes transitive imports from embedded labels (as determined by
	// the Embeds method). This may include imports of other languages.
	// Computed from `rules` when indexing.
	imports map[label.Label][]ImportSpec
}

// ruleRecord contains information about a rule relevant to import indexing.
type ruleRecord struct {
	rule  *rule.Rule

	Kind  string      `json:"kind"`
	Label label.Label `json:"label"`

	Pkg string `json:"pkg"`

	// A list of ImportSpecs by which this rule may be imported.
	ImportedAs []ImportSpec `json:"importedAs"`

	// The set of labels (of any language) that this rule directly embeds.
	Embeds []label.Label `json:"embeds"`

	// The language that this rule is relevant for.
	// Due to the presence of mapped kinds, it's otherwise
	// impossible to know the underlying builtin rule type for an
	// arbitrary import.
	Lang string `json:"lang"`
}

// NewRuleIndex creates a new index.
//
// kindToResolver is a map from rule kinds (for example, "go_library") to
// Resolvers that support those kinds.
func NewRuleIndex(mrslv func(r *rule.Rule, pkgRel string) Resolver, exts ...interface{}) *RuleIndex {
	var crossResolvers []CrossResolver
	for _, e := range exts {
		if cr, ok := e.(CrossResolver); ok {
			crossResolvers = append(crossResolvers, cr)
		}
	}
	return &RuleIndex{
		mrslv:          mrslv,
		crossResolvers: crossResolvers,
	}
}

// AddRule adds a rule r to the index. The rule will only be indexed if there
// is a known resolver for the rule's kind and Resolver.Imports returns a
// non-nil slice.
//
// AddRule may only be called before Finish.
func (ix *RuleIndex) AddRule(c *config.Config, r *rule.Rule, f *rule.File) {
	if ix.indexed {
		log.Fatal("AddRule called after Finish")
	}

	var lang string
	var imps []ImportSpec
	var embeds []label.Label

	l := label.New(c.RepoName, f.Pkg, r.Name())

	if rslv := ix.mrslv(r, f.Pkg); rslv != nil {
		lang = rslv.Name()
		if passesLanguageFilter(c.Langs, lang) {
			imps = rslv.Imports(c, r, f)

			for _, e := range rslv.Embeds(r, l) {
				embeds = append(embeds, e.Abs(l.Repo, l.Pkg))
			}
		}
	}
	// If imps == nil, the rule is not importable. If imps is the empty slice,
	// it may still be importable if it embeds importable libraries.
	if imps == nil {
		return
	}

	record := &ruleRecord{
		rule:       r,
		Kind:       r.Kind(),
		Pkg:        f.Pkg,
		Label:      l,
		ImportedAs: imps,
		Embeds:     embeds,
		Lang:       lang,
	}
	ix.rules = append(ix.rules, record)
}

// Finish constructs the import index and performs any other necessary indexing
// actions after all rules have been added. This step is necessary because
// a rule may be indexed differently based on what rules are added later.
//
// Finish must be called after all AddRule calls and before any
// FindRulesByImport calls.
func (ix *RuleIndex) Finish() {
	ix.labelMap = make(map[label.Label]*ruleRecord)
	ix.imports = make(map[label.Label][]ImportSpec)

	for _, r := range ix.rules {
		if _, ok := ix.labelMap[r.Label]; ok {
			log.Printf("multiple rules found with label %s", r.Label)
			continue
		}

		ix.labelMap[r.Label] = r
		ix.imports[r.Label] = r.ImportedAs
	}

	ix.collectEmbeds()
	ix.buildImportIndex()

	ix.indexed = true
}

func (ix *RuleIndex) collectEmbeds() {
	ix.embeds = make(map[label.Label][]label.Label)
	ix.embedded = make(map[label.Label]struct{})

	didCollectEmbeds := make(map[label.Label]bool)

	for _, r := range ix.rules {
		ix.collectRecordEmbeds(r, didCollectEmbeds)
	}
}
func (ix *RuleIndex) collectRecordEmbeds(r *ruleRecord, didCollectEmbeds map[label.Label]bool) {
	if _, ok := didCollectEmbeds[r.Label]; ok {
		return
	}
	resolver := ix.mrslv(r.rule, r.Pkg)
	didCollectEmbeds[r.Label] = true
	ix.embeds[r.Label] = r.Embeds
	for _, e := range r.Embeds {
		er, ok := ix.labelMap[e]
		if !ok {
			continue
		}
		ix.collectRecordEmbeds(er, didCollectEmbeds)
		erResolver := ix.mrslv(er.rule, er.Pkg)
		if resolver.Name() == erResolver.Name() {
			ix.embedded[er.Label] = struct{}{}
			ix.embeds[r.Label] = append(ix.embeds[r.Label], ix.embeds[er.Label]...)
		}
		ix.imports[r.Label] = append(ix.imports[r.Label], ix.imports[er.Label]...)
	}
}

// buildImportIndex constructs the map used by FindRulesByImport.
func (ix *RuleIndex) buildImportIndex() {
	ix.importMap = make(map[ImportSpec][]*ruleRecord)
	for _, r := range ix.rules {
		if _, embedded := ix.embedded[r.Label]; embedded {
			continue
		}
		indexed := make(map[ImportSpec]bool)
		for _, imp := range ix.imports[r.Label] {
			if indexed[imp] {
				continue
			}
			indexed[imp] = true
			ix.importMap[imp] = append(ix.importMap[imp], r)
		}
	}
}

type FindResult struct {
	// Label is the absolute label (including repository and package name) for
	// a matched rule.
	Label label.Label

	// Embeds is the transitive closure of labels for rules that the matched
	// rule embeds. It may contains duplicates and does not include the label
	// for the rule itself.
	Embeds []label.Label
}

// FindRulesByImport attempts to resolve an import string to a rule record.
// imp is the import to resolve (which includes the target language). lang is
// the language of the rule with the dependency (for example, in
// go_proto_library, imp will have ProtoLang and lang will be GoLang).
// from is the rule which is doing the dependency. This is used to check
// vendoring visibility and to check for self-imports.
//
// FindRulesByImport returns a list of rules, since any number of rules may
// provide the same import. Callers may need to resolve ambiguities using
// language-specific heuristics.
//
// DEPRECATED: use FindRulesByImportWithConfig instead
func (ix *RuleIndex) FindRulesByImport(imp ImportSpec, lang string) []FindResult {
	matches := ix.importMap[imp]
	results := make([]FindResult, 0, len(matches))
	for _, m := range matches {
		if m.Lang != lang {
			continue
		}
		results = append(results, FindResult{
			Label:  m.Label,
			Embeds: ix.embeds[m.Label],
		})
	}
	return results
}

// FindRulesByImportWithConfig attempts to resolve an import to a rule first by
// checking the rule index, then if no matches are found any registered
// CrossResolve implementations are called.
func (ix *RuleIndex) FindRulesByImportWithConfig(c *config.Config, imp ImportSpec, lang string) []FindResult {
	results := ix.FindRulesByImport(imp, lang)
	if len(results) > 0 {
		return results
	}
	for _, cr := range ix.crossResolvers {
		results = append(results, cr.CrossResolve(c, ix, imp, lang)...)
	}
	return results
}

// IsSelfImport returns true if the result's label matches the given label
// or the result's rule transitively embeds the rule with the given label.
// Self imports cause cyclic dependencies, so the caller may want to omit
// the dependency or report an error.
func (r FindResult) IsSelfImport(from label.Label) bool {
	if from.Equal(r.Label) {
		return true
	}
	for _, e := range r.Embeds {
		if from.Equal(e) {
			return true
		}
	}
	return false
}

// passesLanguageFilter returns true if the filter is empty (disabled) or if the
// given language name appears in it.
func passesLanguageFilter(langFilter []string, langName string) bool {
	if len(langFilter) == 0 {
		return true
	}
	for _, l := range langFilter {
		if l == langName {
			return true
		}
	}
	return false
}
